From 53f5a0cbc498a39477ace7c77325740b3108098f Mon Sep 17 00:00:00 2001 From: Hiroshi Inoue Date: Sun, 25 Oct 2009 13:36:31 +0000 Subject: [PATCH] Cleanups about the handing of unnamed parsed statements and the handling of ODBC escape { . --- bind.c | 2 +- bind.h | 2 +- connection.c | 21 ++++ connection.h | 1 + convert.c | 345 +++++++++++++++++++++++++++++++++++++++++++-------- execute.c | 201 +++++++++++++++++++++--------- info.c | 2 +- parse.c | 2 +- psqlodbc.h | 4 +- statement.c | 109 +++++++++++----- statement.h | 6 +- version.h | 2 +- 12 files changed, 551 insertions(+), 146 deletions(-) diff --git a/bind.c b/bind.c index 32a9170..4c29e29 100644 --- a/bind.c +++ b/bind.c @@ -348,7 +348,7 @@ inolog("howTo=%d\n", SC_get_prepare_method(stmt)); case NAMED_PARSE_REQUEST: case PARSE_TO_EXEC_ONCE: case PARSE_REQ_FOR_INFO: - if (ret = prepareParameters(stmt), SQL_ERROR == ret) + if (ret = prepareParameters(stmt, TRUE), SQL_ERROR == ret) goto cleanup; } } diff --git a/bind.h b/bind.h index e36ef29..9aee864 100644 --- a/bind.h +++ b/bind.h @@ -111,7 +111,7 @@ void reset_a_putdata_info(PutDataInfo *pdata, int ipar); void PDATA_free_params(PutDataInfo *pdata, char option); void SC_param_next(const StatementClass*, int *param_number, ParameterInfoClass **, ParameterImplClass **); -RETCODE prepareParameters(StatementClass *stmt); +RETCODE prepareParameters(StatementClass *stmt, BOOL sync); int decideHowToPrepare(StatementClass *stmt, BOOL force); #endif diff --git a/connection.c b/connection.c index 3080c37..dda67d6 100644 --- a/connection.c +++ b/connection.c @@ -307,6 +307,7 @@ CC_Constructor() { rv->status = CONN_NOT_CONNECTED; rv->transact_status = CONN_IN_AUTOCOMMIT; /* autocommit by default */ + rv->stmt_in_extquery = NULL; CC_conninfo_init(&(rv->connInfo)); rv->sock = SOCK_Constructor(rv); @@ -601,6 +602,7 @@ CC_cleanup(ConnectionClass *self) self->status = CONN_NOT_CONNECTED; self->transact_status = CONN_IN_AUTOCOMMIT; + self->stmt_in_extquery = NULL; CC_conninfo_init(&(self->connInfo)); if (self->original_client_encoding) { @@ -765,6 +767,7 @@ EatReadyForQuery(ConnectionClass *conn) CC_set_in_error_trans(conn); break; } + conn->stmt_in_extquery = NULL; } return id; } @@ -2412,6 +2415,15 @@ CC_send_query_append(ConnectionClass *self, const char *query, QueryInfo *qi, UD return NULL; } + /* Finish the pending extended query first */ + if (!SyncParseRequest(self)) + { + if (CC_get_errornumber(self) <= 0) + { + CC_set_error(self, CONN_EXEC_ERROR, "error occured while calling SyncParseRequest() in CC_send_query_append()", func); + return NULL; + } + } /* Indicate that we are sending a query to the backend */ maxlen = CC_get_max_query_len(self); qrylen = strlen(query); @@ -2992,6 +3004,15 @@ CC_send_function(ConnectionClass *self, int fnid, void *result_buf, int *actual_ return FALSE; } + /* Finish the pending extended query first */ + if (!SyncParseRequest(self)) + { + if (CC_get_errornumber(self) <= 0) + { + CC_set_error(self, CONN_EXEC_ERROR, "error occured while calling SyncParseRequest() in CC_send_function()", func); + return FALSE; + } + } #define return DONT_CALL_RETURN_FROM_HERE??? ENTER_INNER_CONN_CS(self, func_cs_count); ci = &(self->connInfo); diff --git a/connection.h b/connection.h index f0d6d04..51044f8 100644 --- a/connection.h +++ b/connection.h @@ -462,6 +462,7 @@ struct ConnectionClass_ int be_key; /* auth code needed to send cancel */ UInt4 isolation; char *current_schema; + StatementClass *stmt_in_extquery; Int2 max_identifier_length; Int2 num_discardp; char **discardp; diff --git a/convert.c b/convert.c index 4ac95d3..6b5fab3 100644 --- a/convert.c +++ b/convert.c @@ -1654,7 +1654,6 @@ typedef struct _QueryParse { char token_save[64]; int token_len; BOOL prev_token_end; - BOOL proc_no_param; size_t declare_pos; UInt4 flags; encoded_str encstr; @@ -1675,7 +1674,6 @@ QP_initialize(QueryParse *q, const StatementClass *stmt) q->token_save[0] = '\0'; q->token_len = 0; q->prev_token_end = TRUE; - q->proc_no_param = TRUE; q->declare_pos = 0; q->flags = 0; make_encoded_str(&q->encstr, SC_get_conn(stmt), q->statement); @@ -1707,6 +1705,8 @@ typedef struct _QueryBuild { Int2 num_output_params; Int2 num_discard_params; Int2 proc_return; + Int2 brace_level; + char parenthesize_the_first; APDFields *apdopts; IPDFields *ipdopts; PutDataInfo *pdata; @@ -1736,6 +1736,8 @@ QB_initialize(QueryBuild *qb, size_t size, StatementClass *stmt, ConnectionClass qb->num_io_params = 0; qb->num_output_params = 0; qb->num_discard_params = 0; + qb->brace_level = 0; + qb->parenthesize_the_first = FALSE; if (conn) qb->conn = conn; else if (stmt) @@ -2046,6 +2048,73 @@ do { \ } while (0) #endif /* NOT_USED */ +static RETCODE +QB_start_brace(QueryBuild *qb) +{ + BOOL replace_by_parenthesis = TRUE; + + if (0 == qb->brace_level) + { + if (0 == F_NewPos(qb)) + { + qb->parenthesize_the_first = FALSE; + replace_by_parenthesis = FALSE; + } + else + qb->parenthesize_the_first = TRUE; + } + if (replace_by_parenthesis) + CVT_APPEND_CHAR(qb, '('); + qb->brace_level++; + return SQL_SUCCESS; +} + +static RETCODE +QB_end_brace(QueryBuild *qb) +{ + BOOL replace_by_parenthesis = TRUE; + + if (qb->brace_level <= 1 && + !qb->parenthesize_the_first) + replace_by_parenthesis = FALSE; + if (replace_by_parenthesis) + CVT_APPEND_CHAR(qb, ')'); + qb->brace_level--; + return SQL_SUCCESS; +} + +static RETCODE QB_append_space_to_separate_identifiers(QueryBuild *qb, const QueryParse *qp) +{ + unsigned char tchar = F_OldChar(qp); + encoded_str encstr; + BOOL add_space = FALSE; + + if (ODBC_ESCAPE_END != tchar) + return SQL_SUCCESS; + encoded_str_constr(&encstr, qb->ccsc, F_OldPtr(qp) + 1); + tchar = encoded_nextchar(&encstr); + if (ENCODE_STATUS(encstr) != 0) + add_space = TRUE; + else + { + if (isalnum(tchar)) + add_space = TRUE; + else + { + switch (tchar) + { + case '_': + case '$': + add_space = TRUE; + } + } + } + if (add_space) + CVT_APPEND_CHAR(qb, ' '); + + return SQL_SUCCESS; +} + /*---------- * Check if the statement is * SELECT ... INTO table FROM ..... @@ -2220,7 +2289,7 @@ insert_without_target(const char *stmt, int *endpos) } static -RETCODE prep_params(StatementClass *stmt, QueryParse *qp, QueryBuild *qb); +RETCODE prep_params(StatementClass *stmt, QueryParse *qp, QueryBuild *qb, BOOL sync); static int Prepare_and_convert(StatementClass *stmt, QueryParse *qp, QueryBuild *qb) { @@ -2230,6 +2299,7 @@ Prepare_and_convert(StatementClass *stmt, QueryParse *qp, QueryBuild *qb) ConnectionClass *conn = SC_get_conn(stmt); ConnInfo *ci = &(conn->connInfo); BOOL discardOutput, outpara; + BOOL sync = FALSE; if (PROTOCOL_74(ci)) { @@ -2245,7 +2315,7 @@ Prepare_and_convert(StatementClass *stmt, QueryParse *qp, QueryBuild *qb) if (QB_initialize(qb, qp->stmt_len, stmt, NULL) < 0) return SQL_ERROR; if (PROTOCOL_74(ci)) - return prep_params(stmt, qp, qb); + return prep_params(stmt, qp, qb, sync); discardOutput = (0 != (qb->flags & FLGB_DISCARD_OUTPUT)); if (NOT_YET_PREPARED == stmt->prepared) /* not yet prepared */ { @@ -2264,7 +2334,7 @@ Prepare_and_convert(StatementClass *stmt, QueryParse *qp, QueryBuild *qb) #endif /* NOT_USED */ CVT_APPEND_STR(qb, "PREPARE \""); CVT_APPEND_STR(qb, plan_name); - CVT_APPEND_CHAR(qb, '"'); + CVT_APPEND_CHAR(qb, IDENTIFIER_QUOTE); marker_count = stmt->num_params - qb->num_discard_params; if (!ipdopts || ipdopts->allocated < marker_count) { @@ -2290,7 +2360,7 @@ Prepare_and_convert(StatementClass *stmt, QueryParse *qp, QueryBuild *qb) if (outpara) CVT_APPEND_STR(qb, "void"); else - CVT_APPEND_STR(qb, pgtype_to_name(stmt, ipdopts->parameters[i].PGType, FALSE)); + CVT_APPEND_STR(qb, pgtype_to_name(stmt, PIC_dsp_pgtype(stmt, ipdopts->parameters[i]), FALSE)); oc++; } CVT_APPEND_CHAR(qb, ')'); @@ -2359,7 +2429,7 @@ inolog("exe_statement=%s\n", exe_statement); #define my_strchr(conn, s1,c1) pg_mbschr(conn->ccsc, s1,c1) static -RETCODE prep_params(StatementClass *stmt, QueryParse *qp, QueryBuild *qb) +RETCODE prep_params(StatementClass *stmt, QueryParse *qp, QueryBuild *qb, BOOL sync) { CSTR func = "prep_params"; RETCODE retval; @@ -2411,9 +2481,15 @@ inolog("prep_params\n"); ret = SendParseRequest(stmt, plan_name, qb->query_statement, SQL_NTS, -1); if (!ret) goto cleanup; - if (!once_descr && (!SendDescribeRequest(stmt, plan_name))) + if (!once_descr && (!SendDescribeRequest(stmt, plan_name, TRUE))) goto cleanup; SC_set_planname(stmt, plan_name); + SC_set_prepared(stmt, plan_name[0] ? PREPARING_PERMANENTLY : PREPARING_TEMPORARILY); + if (!sync) + { + retval = SQL_SUCCESS; + goto cleanup; + } if (!(res = SendSyncAndReceive(stmt, NULL, "prepare_and_describe"))) { SC_set_error(stmt, STMT_NO_RESPONSE, "commnication error while preapreand_describe", func); @@ -2447,7 +2523,7 @@ inolog("prep_params\n"); stmt->current_exec_param = num_pa; ret = SendParseRequest(stmt, plan_name, srvquery, endp2 < 0 ? SQL_NTS : endp2, num_p1); if (!ret) goto cleanup; - if (!once_descr && !SendDescribeRequest(stmt, plan_name)) + if (!once_descr && !SendDescribeRequest(stmt, plan_name, TRUE)) goto cleanup; if (!(res = SendSyncAndReceive(stmt, NULL, "prepare_and_describe"))) { @@ -2469,7 +2545,7 @@ cleanup: return retval; } -RETCODE prepareParameters(StatementClass *stmt) +RETCODE prepareParameters(StatementClass *stmt, BOOL sync) { switch (stmt->prepared) { @@ -2484,7 +2560,7 @@ inolog("prepareParameters\n"); qb = &query_crt; if (QB_initialize(qb, qp->stmt_len, stmt, NULL) < 0) return SQL_ERROR; - return prep_params(stmt, qp, qb); + return prep_params(stmt, qp, qb, sync); } return SQL_SUCCESS; } @@ -2504,7 +2580,7 @@ copy_statement_with_parameters(StatementClass *stmt, BOOL buildPrepareStatement) char *new_statement; - BOOL begin_first = FALSE, prepare_dummy_cursor = FALSE; + BOOL begin_first = FALSE, prepare_dummy_cursor = FALSE, bPrepConv; ConnectionClass *conn = SC_get_conn(stmt); ConnInfo *ci = &(conn->connInfo); SQLLEN current_row; @@ -2593,11 +2669,19 @@ inolog("type=%d concur=%d\n", stmt->options.cursor_type, stmt->options.scroll_co SC_set_pre_executable(stmt); qb = &query_crt; qb->query_statement = NULL; - if (PREPARED_PERMANENTLY == stmt->prepared || (buildPrepareStatement && stmt->options.scroll_concurrency == SQL_CONCUR_READ_ONLY)) + bPrepConv = FALSE; + if (PREPARED_PERMANENTLY == stmt->prepared) + bPrepConv = TRUE; + else if (buildPrepareStatement && + SQL_CONCUR_READ_ONLY == stmt->options.scroll_concurrency) + bPrepConv = TRUE; + if (bPrepConv) { retval = Prepare_and_convert(stmt, qp, qb); return retval; } + SC_forget_unnamed(stmt); + buildPrepareStatement = FALSE; if (ci->disallow_premature) prepare_dummy_cursor = stmt->pre_executing; @@ -2780,7 +2864,7 @@ Int4 findTag(const char *tag, char dollar_quote, int ccsc) { Int4 taglen = 0; encoded_str encstr; - char tchar; + unsigned char tchar; const char *sptr; encoded_str_constr(&encstr, ccsc, tag + 1); @@ -2800,6 +2884,83 @@ Int4 findTag(const char *tag, char dollar_quote, int ccsc) return taglen; } +static +Int4 findIdentifier(const char *str, int ccsc, const char **nextdel) +{ + Int4 strlen = 0; + encoded_str encstr; + unsigned char tchar; + const char *sptr; + BOOL dquote = FALSE; + + *nextdel = NULL; + encoded_str_constr(&encstr, ccsc, str); + for (sptr = str; *sptr; sptr++) + { + tchar = encoded_nextchar(&encstr); + if (ENCODE_STATUS(encstr) != 0) + continue; + if (sptr == str) /* the first character */ + { + if (dquote = (IDENTIFIER_QUOTE == tchar)) + continue; + if (!isalpha(tchar)) + { + strlen = 0; + if (!isspace(tchar)) + *nextdel = sptr; + break; + } + } + if (dquote) + { + if (IDENTIFIER_QUOTE == tchar) + { + if (IDENTIFIER_QUOTE == sptr[1]) + { + encoded_nextchar(&encstr); + sptr++; + continue; + } + strlen = sptr - str + 1; + sptr++; + break; + } + } + else + { + if (isalnum(tchar)) + continue; + if (isspace(tchar)) + { + strlen = sptr - str; + break; + } + switch (tchar) + { + case '_': + case '$': + continue; + } + strlen = sptr - str; + *nextdel = sptr; + break; + } + } + if (NULL == *nextdel) + { + for (; *sptr; sptr++) + { + if (!isspace(*sptr)) + { + *nextdel = sptr; + break; + } + } + } + return strlen; +} + static int inner_process_tokens(QueryParse *qp, QueryBuild *qb) { @@ -2905,8 +3066,12 @@ inner_process_tokens(QueryParse *qp, QueryBuild *qb) * Handle literals (date, time, timestamp) and ODBC scalar * functions */ - else if (oldchar == '{') + else if (oldchar == ODBC_ESCAPE_START) { + int npos = F_NewPos(qb); + BOOL addsp = TRUE; + unsigned char tchar; + if (SQL_ERROR == convert_escape(qp, qb)) { if (0 == qb->errornumber) @@ -2917,21 +3082,12 @@ inner_process_tokens(QueryParse *qp, QueryBuild *qb) mylog("%s convert_escape error\n", func); return SQL_ERROR; } - if (isalnum((UCHAR)F_OldPtr(qp)[1])) - CVT_APPEND_CHAR(qb, ' '); return SQL_SUCCESS; } /* End of an escape sequence */ - else if (oldchar == '}') + else if (oldchar == ODBC_ESCAPE_END) { - if (qp->statement_type == STMT_TYPE_PROCCALL) - { - if (qp->proc_no_param) - CVT_APPEND_STR(qb, "()"); - } - else if (!isspace(F_OldPtr(qp)[1])) - CVT_APPEND_CHAR(qb, ' '); - return SQL_SUCCESS; + return QB_end_brace(qb); } else if (oldchar == '@' && strnicmp(F_OldPtr(qp), "@@identity", 10) == 0) @@ -3196,11 +3352,11 @@ inolog("num_p=%d\n", num_p); memset(bindreq + leng, 0, sizeof(Int2) * num_p); /* initialize by text format */ for (i = stmt->proc_return, j = 0; i < num_params; i++) { -inolog("%dth paramater type oid is %u\n", i, parameters[i].PGType); +inolog("%dth parameter type oid is %u\n", i, PIC_dsp_pgtype(stmt, parameters[i])); if (discard_output && SQL_PARAM_OUTPUT == parameters[i].paramType) continue; - if (PG_TYPE_BYTEA == parameters[i].PGType) + if (PG_TYPE_BYTEA == PIC_dsp_pgtype(stmt, parameters[i])) { mylog("%dth parameter is of binary format\n", j); memcpy(bindreq + leng + sizeof(Int2) * j, @@ -3649,10 +3805,10 @@ inolog("ipara=%p paramType=%d %d proc_return=%d\n", ipara, ipara ? ipara->paramT param_ctype = apara->CType; param_sqltype = ipara->SQLType; - param_pgtype = ipara->PGType; + param_pgtype = PIC_dsp_pgtype(qb->stmt, *ipara); - mylog("%s: from(fcType)=%d, to(fSqlType)=%d\n", func, - param_ctype, param_sqltype); + mylog("%s: from(fcType)=%d, to(fSqlType)=%d(%u)\n", func, + param_ctype, param_sqltype, param_pgtype); /* replace DEFAULT with something we can use */ if (param_ctype == SQL_C_DEFAULT) @@ -4057,8 +4213,10 @@ mylog("buf=%p flag=%d\n", buf, qb->flags); lobj_oid = pdata->pdata[param_number].lobj_oid; else { + BOOL is_in_trans_at_entry = CC_is_in_trans(conn); + /* begin transaction if needed */ - if (!CC_is_in_trans(conn)) + if (!is_in_trans_at_entry) { if (!CC_begin(conn)) { @@ -4091,7 +4249,7 @@ mylog("buf=%p flag=%d\n", buf, qb->flags); odbc_lo_close(conn, lobj_fd); /* commit transaction if needed */ - if (!ci->drivers.use_declarefetch && CC_does_autocommit(conn)) + if (!is_in_trans_at_entry) { if (!CC_commit(conn)) { @@ -4260,7 +4418,7 @@ processParameters(QueryParse *qp, QueryBuild *qb, *output_count = F_NewPos(qb); break; - case '}': + case ODBC_ESCAPE_END: stop = (0 == innerParenthesis); break; @@ -4305,12 +4463,13 @@ static int convert_escape(QueryParse *qp, QueryBuild *qb) { CSTR func = "convert_escape"; + ConnectionClass *conn = qb->conn; RETCODE retval = SQL_SUCCESS; char buf[1024], buf_small[128], key[65]; UCHAR ucv; UInt4 prtlen; - if (F_OldChar(qp) == '{') /* skip the first { */ + if (F_OldChar(qp) == ODBC_ESCAPE_START) /* skip the first { */ F_OldNext(qp); /* Separate off the key, skipping leading and trailing whitespace */ while ((ucv = F_OldChar(qp)) != '\0' && isspace(ucv)) @@ -4318,6 +4477,59 @@ convert_escape(QueryParse *qp, QueryBuild *qb) /* * procedure calls */ + /* '?=' to accept return values exists ? */ + if (F_OldChar(qp) == '?') + { + qb->param_number++; + qb->proc_return = 1; + if (qb->stmt) + qb->stmt->proc_return = 1; + while (isspace((UCHAR) qp->statement[++qp->opos])); + if (F_OldChar(qp) != '=') + { + F_OldPrior(qp); + return SQL_SUCCESS; + } + while (isspace((UCHAR) qp->statement[++qp->opos])); + } + /** + if (qp->statement_type == STMT_TYPE_PROCCALL) + { + int lit_call_len = 4; + + // '?=' to accept return values exists ? + if (F_OldChar(qp) == '?') + { + qb->param_number++; + qb->proc_return = 1; + if (qb->stmt) + qb->stmt->proc_return = 1; + while (isspace((UCHAR) qp->statement[++qp->opos])); + if (F_OldChar(qp) != '=') + { + F_OldPrior(qp); + return SQL_SUCCESS; + } + while (isspace((UCHAR) qp->statement[++qp->opos])); + } + if (strnicmp(F_OldPtr(qp), "call", lit_call_len) || + !isspace((UCHAR) F_OldPtr(qp)[lit_call_len])) + { + F_OldPrior(qp); + return SQL_SUCCESS; + } + qp->opos += lit_call_len; + if (qb->num_io_params > 1 || + (0 == qb->proc_return && + PG_VERSION_GE(conn, 7.3))) + CVT_APPEND_STR(qb, "SELECT * FROM"); + else + CVT_APPEND_STR(qb, "SELECT"); + if (my_strchr(conn, F_OldPtr(qp), '(')) + qp->proc_no_param = FALSE; + return SQL_SUCCESS; + } + **/ sscanf(F_OldPtr(qp), "%32s", key); while ((ucv = F_OldChar(qp)) != '\0' && (!isspace(ucv))) @@ -4327,62 +4539,83 @@ convert_escape(QueryParse *qp, QueryBuild *qb) /* Avoid the concatenation of the function name with the previous word. Aceto */ - if (F_NewPos(qb) > 0 && isalnum((UCHAR)F_NewPtr(qb)[-1])) - CVT_APPEND_CHAR(qb, ' '); if (stricmp(key, "call") == 0) { Int4 funclen; const char *nextdel; + if (SQL_ERROR == QB_start_brace(qb)) + return SQL_ERROR; if (qb->num_io_params > 1 || (0 == qb->proc_return && PG_VERSION_GE(conn, 7.3))) CVT_APPEND_STR(qb, "SELECT * FROM "); else CVT_APPEND_STR(qb, "SELECT "); - /* Continue at inner_process_tokens loop */ - F_OldPrior(qp); - return SQL_SUCCESS; + funclen = findIdentifier(F_OldPtr(qp), qb->ccsc, &nextdel); + if (nextdel && ODBC_ESCAPE_END == *nextdel) + { + CVT_APPEND_DATA(qb, F_OldPtr(qp), funclen); + CVT_APPEND_STR(qb, "()"); + if (SQL_ERROR == QB_end_brace(qb)) + return SQL_ERROR; + /* positioned at } */ + qp->opos += (nextdel - F_OldPtr(qp)); + } + else + { + /* Continue at inner_process_tokens loop */ + F_OldPrior(qp); + return SQL_SUCCESS; + } } else if (stricmp(key, "d") == 0) { /* Literal; return the escape part adding type cast */ - F_ExtractOldTo(qp, buf_small, '}', sizeof(buf_small)); - if (PG_VERSION_LT(qb->conn, 7.3)) - prtlen = snprintf(buf, sizeof(buf), "%s ", buf_small); + F_ExtractOldTo(qp, buf_small, ODBC_ESCAPE_END, sizeof(buf_small)); + if (PG_VERSION_LT(conn, 7.3)) + prtlen = snprintf(buf, sizeof(buf), "%s", buf_small); else - prtlen = snprintf(buf, sizeof(buf), "%s::date ", buf_small); + prtlen = snprintf(buf, sizeof(buf), "%s::date", buf_small); CVT_APPEND_DATA(qb, buf, prtlen); + retval = QB_append_space_to_separate_identifiers(qb, qp); } else if (stricmp(key, "t") == 0) { /* Literal; return the escape part adding type cast */ - F_ExtractOldTo(qp, buf_small, '}', sizeof(buf_small)); + F_ExtractOldTo(qp, buf_small, ODBC_ESCAPE_END, sizeof(buf_small)); prtlen = snprintf(buf, sizeof(buf), "%s::time", buf_small); CVT_APPEND_DATA(qb, buf, prtlen); + retval = QB_append_space_to_separate_identifiers(qb, qp); } else if (stricmp(key, "ts") == 0) { /* Literal; return the escape part adding type cast */ - F_ExtractOldTo(qp, buf_small, '}', sizeof(buf_small)); - if (PG_VERSION_LT(qb->conn, 7.1)) + F_ExtractOldTo(qp, buf_small, ODBC_ESCAPE_END, sizeof(buf_small)); + if (PG_VERSION_LT(conn, 7.1)) prtlen = snprintf(buf, sizeof(buf), "%s::datetime", buf_small); else prtlen = snprintf(buf, sizeof(buf), "%s::timestamp", buf_small); CVT_APPEND_DATA(qb, buf, prtlen); + retval = QB_append_space_to_separate_identifiers(qb, qp); } else if (stricmp(key, "oj") == 0) /* {oj syntax support for 7.1 * servers */ { if (qb->stmt) SC_set_outer_join(qb->stmt); + retval = QB_start_brace(qb); + /* Continue at inner_process_tokens loop */ F_OldPrior(qp); - return SQL_SUCCESS; /* Continue at inner_process_tokens loop */ + return retval; } - else if (stricmp(key, "escape") == 0) /* like escape support for 7.1+ servers */ + else if (stricmp(key, "escape") == 0) /* like escape syntax support for 7.1+ servers */ { - CVT_APPEND_STR(qb, key); - return SQL_SUCCESS; + /* Literal; return the escape part adding type cast */ + F_ExtractOldTo(qp, buf_small, ODBC_ESCAPE_END, sizeof(buf_small)); + prtlen = snprintf(buf, sizeof(buf), "%s %s", key, buf_small); + CVT_APPEND_DATA(qb, buf, prtlen); + retval = QB_append_space_to_separate_identifiers(qb, qp); } else if (stricmp(key, "fn") == 0) { @@ -4627,7 +4860,7 @@ parse_datetime(const char *buf, SIMPLE_TIME *st) st->infinity = 0; /* escape sequence ? */ - if (buf[0] == '{') + if (buf[0] == ODBC_ESCAPE_START) { while (*(++buf) && *buf != LITERAL_QUOTE); if (!(*buf)) @@ -5125,6 +5358,13 @@ convert_lo(StatementClass *stmt, const void *value, SQLSMALLINT fCType, PTR rgbV GetDataInfo *gdata_info = SC_get_GDTI(stmt); int factor; + oid = ATOI32U(value); + if (0 == oid) + { + if (pcbValue) + *pcbValue = SQL_NULL_DATA; + return COPY_OK; + } switch (fCType) { case SQL_C_CHAR: @@ -5161,7 +5401,6 @@ convert_lo(StatementClass *stmt, const void *value, SQLSMALLINT fCType, PTR rgbV } } - oid = ATOI32U(value); stmt->lobj_fd = odbc_lo_open(conn, oid, INV_READ); if (stmt->lobj_fd < 0) { diff --git a/execute.c b/execute.c index 7f55eb1..9cb24d1 100644 --- a/execute.c +++ b/execute.c @@ -282,6 +282,130 @@ decideHowToPrepare(StatementClass *stmt, BOOL force) return method; } +/* dont/should/can send Parse request ? */ +enum { + doNothing = 0 + ,allowParse + ,preferParse + ,shouldParse + ,usingCommand +}; + +#define ONESHOT_CALL_PARSE allowParse +#define NOPARAM_ONESHOT_CALL_PARSE doNothing + +static +int HowToPrepareBeforeExec(StatementClass *stmt, BOOL checkOnly) +{ + SQLSMALLINT num_params = stmt->num_params; + ConnectionClass *conn = SC_get_conn(stmt); + ConnInfo *ci = &(conn->connInfo); + int nCallParse = doNothing, how_to_prepare = 0; + BOOL bNeedsTrans = FALSE; + + if (num_params < 0) + PGAPI_NumParams(stmt, &num_params); + how_to_prepare = decideHowToPrepare(stmt, checkOnly); + if (checkOnly) + { + if (num_params <= 0) + return doNothing; + } + else + { + switch (how_to_prepare) + { + case USING_PREPARE_COMMAND: + return usingCommand; + case NAMED_PARSE_REQUEST: + return shouldParse; + case PARSE_TO_EXEC_ONCE: + switch (stmt->prepared) + { + case PREPARED_TEMPORARILY: + nCallParse = preferParse; + break; + default: + if (num_params <= 0) + nCallParse = NOPARAM_ONESHOT_CALL_PARSE; + else + nCallParse = ONESHOT_CALL_PARSE; + } + break; + default: + return doNothing; + } + } + if (PG_VERSION_LE(conn, 7.3)) + return nCallParse; + + if (num_params > 0) + { + int param_number = -1; + ParameterInfoClass *apara; + ParameterImplClass *ipara; + OID pgtype; + + while (TRUE) + { + SC_param_next(stmt, ¶m_number, &apara, &ipara); + if (!ipara || !apara) + break; + pgtype = PIC_get_pgtype(*ipara); + if (checkOnly) + { + switch (ipara->SQLType) + { + case SQL_LONGVARBINARY: + if (0 == pgtype) + { + if (ci->bytea_as_longvarbinary && + 0 != conn->lobj_type) + nCallParse = shouldParse; + } + break; + case SQL_CHAR: + if (ci->cvt_null_date_string) + nCallParse = shouldParse; + break; + } + } + else + { + BOOL bBytea = FALSE; + + switch (ipara->SQLType) + { + case SQL_LONGVARBINARY: + if (conn->lobj_type == pgtype || + PG_TYPE_OID == pgtype) + bNeedsTrans = TRUE; + else if (PG_TYPE_BYTEA == pgtype) + bBytea = TRUE; + else if (0 == pgtype) + { + if (ci->bytea_as_longvarbinary) + bBytea = TRUE; + else + bNeedsTrans = TRUE; + } + if (bBytea) + if (nCallParse < preferParse) + nCallParse = preferParse; + break; + } + } + } + } + if (bNeedsTrans && + PARSE_TO_EXEC_ONCE == how_to_prepare) + { + if (!CC_is_in_trans(conn) && CC_does_autocommit(conn)) + nCallParse = doNothing; + } + return nCallParse; +} + /* * The execution after all parameters were resolved. */ @@ -308,15 +432,19 @@ RETCODE Exec_with_parameters_resolved(StatementClass *stmt, BOOL *exec_end) /* Prepare the statement if possible at backend side */ if (!stmt->inaccurate_result) { + /** switch (decideHowToPrepare(stmt, FALSE)) { case USING_PREPARE_COMMAND: case NAMED_PARSE_REQUEST: #ifndef BYPASS_ONESHOT_PLAN_EXECUTION case PARSE_TO_EXEC_ONCE: -#endif/* BYPASS_ONESHOT_PLAN_EXECUTION */ +#endif prepare_before_exec = TRUE; } + **/ + if (HowToPrepareBeforeExec(stmt, FALSE) >= allowParse) + prepare_before_exec = TRUE; } inolog("prepare_before_exec=%d srv=%d\n", prepare_before_exec, conn->connInfo.use_server_side_prepare); /* Create the statement with parameters substituted. */ @@ -903,63 +1031,19 @@ PGAPI_Execute(HSTMT hstmt, UWORD flag) We sometimes need to know about the PG type of binding parameters even in case of non-prepared statements. */ - BOOL bCallPrepare = FALSE; - - ConnInfo *ci = &(conn->connInfo); - if (NOT_YET_PREPARED == stmt->prepared && - PROTOCOL_74(ci) && - ci->use_server_side_prepare && - (ci->bytea_as_longvarbinary || /* both lo and bytea are LO */ - ci->cvt_null_date_string)) + int nCallParse = doNothing; + + if (NOT_YET_PREPARED == stmt->prepared) { - SQLSMALLINT num_params = stmt->num_params; - if (num_params < 0) - PGAPI_NumParams(stmt, &num_params); - if (num_params > 0) + switch (nCallParse = HowToPrepareBeforeExec(stmt, TRUE)) { - int param_number = -1; - ParameterInfoClass *apara; - ParameterImplClass *ipara; - BOOL reqOK = FALSE; - - switch (decideHowToPrepare(stmt, TRUE)) - { - case NAMED_PARSE_REQUEST: - case PARSE_TO_EXEC_ONCE: - case PARSE_REQ_FOR_INFO: - reqOK = TRUE; - } - while (reqOK) - { - SC_param_next(stmt, ¶m_number, &apara, &ipara); - if (!ipara || !apara) - break; - if (SQL_LONGVARBINARY == ipara->SQLType) - { - if (ci->bytea_as_longvarbinary) - { - bCallPrepare = TRUE; - break; - } - } - else if (SQL_CHAR == ipara->SQLType) - { - if (SQL_C_CHAR == apara->CType && - ci->cvt_null_date_string) - { - bCallPrepare = TRUE; - break; - } - } - } + case shouldParse: + if (retval = prepareParameters(stmt, TRUE), SQL_ERROR == retval) + goto cleanup; + break; } } - if (bCallPrepare) - { - if (retval = prepareParameters(stmt), SQL_ERROR == retval) - goto cleanup; - } -mylog("prepareParameters %d end\n", stmt->prepare); +mylog("prepareParameters was %s called, prepare state:%d\n", shouldParse == nCallParse ? "" : "not", stmt->prepare); if (ipdopts->param_processed_ptr) *ipdopts->param_processed_ptr = 0; @@ -1481,7 +1565,7 @@ PGAPI_PutData( char *buffer, *putbuf, *allocbuf = NULL; Int2 ctype; SQLLEN putlen; - BOOL lenset = FALSE; + BOOL lenset = FALSE, handling_lo = FALSE; mylog("%s: entering...\n", func); @@ -1554,7 +1638,8 @@ PGAPI_PutData( putlen = ctype_length(ctype); } putbuf = rgbValue; - if (current_iparam->PGType == conn->lobj_type && SQL_C_CHAR == ctype) + handling_lo = (PIC_dsp_pgtype(stmt, *current_iparam) == conn->lobj_type); + if (handling_lo && SQL_C_CHAR == ctype) { allocbuf = malloc(putlen / 2 + 1); if (allocbuf) @@ -1589,7 +1674,7 @@ PGAPI_PutData( /* Handle Long Var Binary with Large Objects */ /* if (current_iparam->SQLType == SQL_LONGVARBINARY) */ - if (current_iparam->PGType == conn->lobj_type) + if (handling_lo) { /* begin transaction if needed */ if (!CC_is_in_trans(conn)) @@ -1648,7 +1733,7 @@ PGAPI_PutData( mylog("PGAPI_PutData: (>1) cbValue = %d\n", cbValue); /* if (current_iparam->SQLType == SQL_LONGVARBINARY) */ - if (current_iparam->PGType == conn->lobj_type) + if (handling_lo) { /* the large object fd is in EXEC_buffer */ retval = odbc_lo_write(conn, estmt->lobj_fd, putbuf, (Int4) putlen); diff --git a/info.c b/info.c index 2a32dd9..2c71551 100644 --- a/info.c +++ b/info.c @@ -2693,7 +2693,7 @@ PGAPI_SpecialColumns( SQLSMALLINT internal_asis_type = SQL_C_CHAR, cbSchemaName; const char *szSchemaName, *eq_string; - mylog("%s: entering...stmt=%p scnm=%p len=%d colType=%d\n", func, stmt, szTableOwner, cbTableOwner, fColType); + mylog("%s: entering...stmt=%p scnm=%p len=%d colType=%d scope=%d\n", func, stmt, szTableOwner, cbTableOwner, fColType, fScope); if (result = SC_initialize_and_recycle(stmt), SQL_SUCCESS != result) return result; diff --git a/parse.c b/parse.c index a1bc3e5..0674cdf 100644 --- a/parse.c +++ b/parse.c @@ -1207,7 +1207,7 @@ static char *insert_as_to_the_statement(char *stmt, char **pptr, char **ptr) static char parse_the_statement(StatementClass *stmt, BOOL check_hasoids, BOOL sqlsvr_check) { - CSTR func = "parse_statement"; + CSTR func = "parse_the_statement"; char token[TOKEN_SIZE], stoken[TOKEN_SIZE], btoken[TOKEN_SIZE]; char delim, quote, diff --git a/psqlodbc.h b/psqlodbc.h index b7ebf70..5440ecd 100644 --- a/psqlodbc.h +++ b/psqlodbc.h @@ -5,7 +5,7 @@ * * Comments: See "notice.txt" for copyright and license information. * - * $Id: psqlodbc.h,v 1.131 2009/07/06 15:23:02 h-saito Exp $ + * $Id: psqlodbc.h,v 1.132 2009/10/25 13:36:31 hinoue Exp $ * */ @@ -268,6 +268,8 @@ BOOL isSqlServr(); #define SEARCH_PATTERN_ESCAPE '\\' #define LITERAL_QUOTE '\'' #define IDENTIFIER_QUOTE '\"' +#define ODBC_ESCAPE_START '{' +#define ODBC_ESCAPE_END '}' #define DOLLAR_QUOTE '$' #define LITERAL_EXT 'E' #define PG_CARRIAGE_RETURN '\r' diff --git a/statement.c b/statement.c index 28f0c16..0e1e342 100644 --- a/statement.c +++ b/statement.c @@ -1006,11 +1006,11 @@ SC_pre_execute(StatementClass *self) { case NAMED_PARSE_REQUEST: case PARSE_TO_EXEC_ONCE: - if (SQL_SUCCESS != prepareParameters(self)) + if (SQL_SUCCESS != prepareParameters(self, TRUE)) return num_fields; break; case PARSE_REQ_FOR_INFO: - if (SQL_SUCCESS != prepareParameters(self)) + if (SQL_SUCCESS != prepareParameters(self, TRUE)) return num_fields; self->status = STMT_PREMATURE; self->inaccurate_result = TRUE; @@ -1791,24 +1791,31 @@ SC_execute(StatementClass *self) */ /* in copy_statement... */ use_extended_protocol = FALSE; - if (PREPARED_PERMANENTLY == self->prepared && - PROTOCOL_74(ci)) - use_extended_protocol = TRUE; - else if (PREPARED_TEMPORARILY == self->prepared) + switch (self->prepared) { - switch (SC_get_prepare_method(self)) - { + case PREPARING_PERMANENTLY: + case PREPARED_PERMANENTLY: + if (PROTOCOL_74(ci)) + use_extended_protocol = TRUE; + break; + case PREPARING_TEMPORARILY: + case PREPARED_TEMPORARILY: + if (!issue_begin) + { + switch (SC_get_prepare_method(self)) + { #ifndef BYPASS_ONESHOT_PLAN_EXECUTION - case PARSE_TO_EXEC_ONCE: + case PARSE_TO_EXEC_ONCE: #endif /* BYPASS_ONESHOT_PLAN_EXECUTION */ - case NAMED_PARSE_REQUEST: - use_extended_protocol = TRUE; - } - if (!use_extended_protocol) - { - SC_forget_unnamed(self); - SC_set_Result(self, NULL); /* discard the parsed information */ - } + case NAMED_PARSE_REQUEST: + use_extended_protocol = TRUE; + } + } + if (!use_extended_protocol) + { + SC_forget_unnamed(self); + SC_set_Result(self, NULL); /* discard the parsed information */ + } } if (use_extended_protocol) { @@ -1816,8 +1823,6 @@ SC_execute(StatementClass *self) if (issue_begin) CC_begin(conn); - for (res = SC_get_Result(self); NULL != res->next; res = res->next) ; -inolog("get_Result=%p %p %d\n", res, SC_get_Result(self), self->curr_param_result); if (!plan_name) plan_name = ""; if (!SendBindRequest(self, plan_name)) @@ -1832,6 +1837,8 @@ inolog("get_Result=%p %p %d\n", res, SC_get_Result(self), self->curr_param_resul SC_set_error(self, STMT_EXEC_ERROR, "Execute request error", func); goto cleanup; } + for (res = SC_get_Result(self); NULL != res && NULL != res->next; res = res->next) ; +inolog("get_Result=%p %p %d\n", res, SC_get_Result(self), self->curr_param_result); if (!(res = SendSyncAndReceive(self, self->curr_param_result ? res : NULL, "bind_and_execute"))) { if (SC_get_errornumber(self) <= 0) @@ -2273,6 +2280,7 @@ SendBindRequest(StatementClass *stmt, const char *plan_name) return FALSE; if (!BuildBindRequest(stmt, plan_name)) return FALSE; + conn->stmt_in_extquery = stmt; return TRUE; } @@ -2347,7 +2355,6 @@ inolog(" response_length=%d\n", response_length); case 'E': /* ErrorMessage */ msg_truncated = handle_error_message(conn, msgbuffer, sizeof(msgbuffer), res->sqlstate, comment, res); - rcvend = TRUE; break; case 'N': /* Notice */ msg_truncated = handle_notice_message(conn, msgbuffer, sizeof(msgbuffer), res->sqlstate, comment, res); @@ -2395,7 +2402,7 @@ inolog("num_params=%d info=%d\n", stmt->num_params, num_p); continue; } oid = SOCK_get_int(sock, 4); - ipdopts->parameters[pidx].PGType = oid; + PIC_set_pgtype(ipdopts->parameters[pidx], oid); } } else @@ -2406,7 +2413,7 @@ inolog("num_params=%d info=%d\n", stmt->num_params, num_p); oid = SOCK_get_int(sock, 4); if (SQL_PARAM_OUTPUT != paramType || PG_TYPE_VOID != oid) - ipdopts->parameters[pidx].PGType = oid; + PIC_set_pgtype(ipdopts->parameters[pidx], oid); } } #endif /* NOT_USED */ @@ -2425,7 +2432,7 @@ inolog("num_params=%d info=%d\n", stmt->num_params, num_p); paramType = ipdopts->parameters[pidx].paramType; if (SQL_PARAM_OUTPUT != paramType || PG_TYPE_VOID != oid) - ipdopts->parameters[pidx].PGType = oid; + PIC_set_pgtype(ipdopts->parameters[pidx], oid); } break; case 'T': /* RowDesription */ @@ -2453,8 +2460,8 @@ inolog("num_params=%d info=%d\n", stmt->num_params, num_p); if (SQL_PARAM_OUTPUT == paramType || SQL_PARAM_INPUT_OUTPUT == paramType) { -inolog("!![%d].PGType %u->%u\n", i, ipdopts->parameters[i].PGType, CI_get_oid(res->fields, cidx)); - ipdopts->parameters[i].PGType = CI_get_oid(res->fields, cidx); +inolog("!![%d].PGType %u->%u\n", i, PIC_get_pgtype(ipdopts->parameters[i]), CI_get_oid(res->fields, cidx)); + PIC_set_pgtype(ipdopts->parameters[i], CI_get_oid(res->fields, cidx)); cidx++; } } @@ -2481,6 +2488,7 @@ inolog("!![%d].PGType %u->%u\n", i, ipdopts->parameters[i].PGType, CI_get_oid(re break; } } + conn->stmt_in_extquery = NULL; return res; } @@ -2543,7 +2551,6 @@ mylog("sta_pidx=%d end_pidx=%d num_p=%d\n", sta_pidx, end_pidx, num_params); qlen = (SQL_NTS == qlen) ? strlen(query) : qlen; leng = strlen(plan_name) + 1 + qlen + 1 + pileng; SOCK_put_int(sock, (Int4) (leng + 4), 4); /* length */ -/* inolog("parse leng=%d\n", leng); */ inolog("parse leng=" FORMAT_SIZE_T "\n", leng); SOCK_put_string(sock, plan_name); SOCK_put_n_char(sock, query, qlen); @@ -2563,12 +2570,51 @@ inolog("parse leng=" FORMAT_SIZE_T "\n", leng); SOCK_put_int(sock, 0, sizeof(UInt4)); } } + conn->stmt_in_extquery = stmt; return TRUE; } +BOOL SyncParseRequest(ConnectionClass *conn) +{ + StatementClass *stmt = conn->stmt_in_extquery; + QResultClass *res, *last; + BOOL ret = FALSE; + + if (!stmt) + return TRUE; + + res = SC_get_Result(stmt); + for (last = res; last && last->next; last = last->next) + ; + if (!(res = SendSyncAndReceive(stmt, stmt->curr_param_result ? last : NULL, __FUNCTION__))) + { + if (SC_get_errornumber(stmt) <= 0) + SC_set_error(stmt, STMT_NO_RESPONSE, "Could not receive the response, communication down ??", __FUNCTION__); + CC_on_abort(conn, CONN_DEAD); + goto cleanup; + } + + if (!last) + SC_set_Result(stmt, res); + else + { + if (res != last) + last->next = res; + stmt->curr_param_result = 1; + } + if (!QR_command_maybe_successful(res)) + { + SC_set_error(stmt, STMT_EXEC_ERROR, "Error while syncing parse reuest", __FUNCTION__); + goto cleanup; + } + ret = TRUE; +cleanup: + return ret; +} + BOOL -SendDescribeRequest(StatementClass *stmt, const char *plan_name) +SendDescribeRequest(StatementClass *stmt, const char *plan_name, BOOL paramAlso) { CSTR func = "SendDescribeRequest"; ConnectionClass *conn = SC_get_conn(stmt); @@ -2593,7 +2639,7 @@ SendDescribeRequest(StatementClass *stmt, const char *plan_name) if (!sockerr) { inolog("describe leng=%d\n", leng); - SOCK_put_char(sock, 'S'); /* describe a prepared statement */ + SOCK_put_char(sock, paramAlso ? 'S' : 'P'); /* describe a prepared statement */ if (SOCK_get_errcode(sock) != 0) sockerr = TRUE; } @@ -2609,6 +2655,7 @@ inolog("describe leng=%d\n", leng); CC_on_abort(conn, CONN_DEAD); return FALSE; } + conn->stmt_in_extquery = stmt; return TRUE; } @@ -2670,6 +2717,7 @@ inolog("Close leng=%d\n", leng); SOCK_put_char(sock, 'P'); /* Portal */ SOCK_put_string(sock, plan_name); } + conn->stmt_in_extquery = stmt; return TRUE; } @@ -2681,6 +2729,7 @@ BOOL SendSyncRequest(ConnectionClass *conn) SOCK_put_char(sock, 'S'); /* Sync command */ SOCK_put_int(sock, 4, 4); SOCK_flush_output(sock); + conn->stmt_in_extquery = NULL; return TRUE; } @@ -2725,6 +2774,8 @@ BOOL SC_SetExecuting(StatementClass *self, BOOL on) LEAVE_COMMON_CS; return exeSet; } + +#ifdef NOT_USED BOOL SC_SetCancelRequest(StatementClass *self) { BOOL enteredCS = FALSE; @@ -2747,6 +2798,8 @@ BOOL SC_SetCancelRequest(StatementClass *self) LEAVE_COMMON_CS; return enteredCS; } +#endif /* NOT_USED */ + BOOL SC_AcceptedCancelRequest(const StatementClass *self) { BOOL shouldCancel = FALSE; diff --git a/statement.h b/statement.h index 37ade26..78f7fec 100644 --- a/statement.h +++ b/statement.h @@ -388,6 +388,8 @@ enum { enum { NOT_YET_PREPARED = 0 + ,PREPARING_PERMANENTLY + ,PREPARING_TEMPORARILY ,PREPARED_PERMANENTLY ,PREPARED_TEMPORARILY ,ONCE_DESCRIBED @@ -428,6 +430,7 @@ enum #define SC_ref_CC_error(a) ((a->ref_CC_error) = TRUE) #define SC_forget_unnamed(a) (PREPARED_TEMPORARILY == (a)->prepared ? SC_set_prepared(a, ONCE_DESCRIBED) : (void) 0) #define SC_returns_rows(a) (STMT_TYPE_SELECT == (a)->statement_type || STMT_TYPE_WITH == (a)->statement_type) +#define SC_determine_statement_type(a) (STMT_TYPE_SELECT != (a)->statement_type ? (a)->statement_type : ((a)->statement_type = || statement_type((a)->statement))) /* For Multi-thread */ @@ -509,7 +512,8 @@ RETCODE DiscardStatementSvp(StatementClass *self, RETCODE, BOOL errorOnly); BOOL SendParseRequest(StatementClass *self, const char *name, const char *query, Int4 qlen, Int2 num_params); -BOOL SendDescribeRequest(StatementClass *self, const char *name); +BOOL SyncParseRequest(ConnectionClass *conn); +BOOL SendDescribeRequest(StatementClass *self, const char *name, BOOL paramAlso); BOOL SendBindRequest(StatementClass *self, const char *name); BOOL BuildBindRequest(StatementClass *stmt, const char *name); BOOL SendExecuteRequest(StatementClass *stmt, const char *portal, UInt4 count); diff --git a/version.h b/version.h index 54af6ff..286321a 100644 --- a/version.h +++ b/version.h @@ -12,6 +12,6 @@ #define POSTGRESDRIVERVERSION "08.04.0101" #define POSTGRES_RESOURCE_VERSION "08.04.0101\0" #define PG_DRVFILE_VERSION 8,4,01,01 -#define PG_BUILD_VERSION "200910250003" +#define PG_BUILD_VERSION "200910250004" #endif -- 2.39.5