diff --git a/CMakeLists.txt b/CMakeLists.txt index 88abd075..c0d20bee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -288,10 +288,8 @@ add_custom_target(curlclean # set(TINYCBOR_PATH_SRC ${CMAKE_SOURCE_DIR}/libs/tinycbor CACHE PATH "Lib tinycbor source path") -aux_source_directory(${TINYCBOR_PATH_SRC}/src DRV_SRC) -list(FILTER DRV_SRC EXCLUDE REGEX .*open_memstream.c$) # Win-unsupported -list(FILTER DRV_SRC EXCLUDE REGEX .*cborparser.c$) # to be patched -file(COPY ${TINYCBOR_PATH_SRC}/src/cborparser.c DESTINATION ${CMAKE_BINARY_DIR}) +file(COPY ${TINYCBOR_PATH_SRC}/src/cborparser.c DESTINATION + ${CMAKE_BINARY_DIR}) # tinycbor doesn't expose (yet? #125) the text/binary string pointer, since the # string can span multiple stream chunks. However, in our case the CBOR object # is available entirely, so access to it can safely be had; this saves a @@ -304,9 +302,13 @@ CborError cbor_value_get_string_chunk(CborValue *it, CborError err = get_string_chunk(it, bufferptr, len); return err != CborNoError ? err : preparse_next_value(it); }") -aux_source_directory(${CMAKE_BINARY_DIR} DRV_SRC) +list(APPEND DRV_SRC ${CMAKE_BINARY_DIR}/cborparser.c) +list(APPEND DRV_SRC ${TINYCBOR_PATH_SRC}/src/cborvalidation.c) +list(APPEND DRV_SRC ${TINYCBOR_PATH_SRC}/src/cborerrorstrings.c) +list(APPEND DRV_SRC ${TINYCBOR_PATH_SRC}/src/cborencoder.c) +list(APPEND DRV_SRC + ${TINYCBOR_PATH_SRC}/src/cborencoder_close_container_checked.c) set(TINYCBOR_INC ${TINYCBOR_PATH_SRC}/src) -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /DWITHOUT_OPEN_MEMSTREAM") # limit how deep the parser will recurse (current need: 3) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /DCBOR_PARSER_MAX_RECURSIONS=16") diff --git a/driver/connect.c b/driver/connect.c index 115772d3..629fe5bf 100644 --- a/driver/connect.c +++ b/driver/connect.c @@ -235,6 +235,20 @@ static int debug_callback(CURL *handle, curl_infotype type, char *data, /* * "ptr points to the delivered data, and the size of that data is size * multiplied with nmemb." + * + * Note: Elasticsearch supports (atm.) no streaming API and ES/SQL doesn't + * either. This function will keep realloc'ing (if needed) until the entire + * page sent by ES/SQL is received. The alternative is to stream-parse. + * However, with text & binary data, the stream parsing libraries will ask the + * client to provide a buffer to copy the data into, out of potentially + * multiple received data chunks in the stream. Which could require an extra + * allocation and will always involve an extra copy (or more, for UTF-8 + * decoding). With current design (= reply object in contiguous chunk) at + * least the copy is skipped, since the text/binary data is contiguous and + * ready to be read from the receive buffer directly. + * + * TODO: initial chunk size and incremental sizes for the reallocation should + * be better "calibrated" (/ follow some max/hysteretic curve). */ static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) @@ -740,10 +754,10 @@ SQLRETURN curl_post(esodbc_stmt_st *stmt, int url_type, BOOL is_json; if (dbc->pack_json) { - DBGH(stmt, "POSTing JSON type %d: [%zu] `" LCPDL "`.", url_type, + DBGH(stmt, "POSTing JSON to URL type %d: [%zu] `" LCPDL "`.", url_type, req_body->cnt, LCSTR(req_body)); } else { - DBGH(stmt, "POSTing CBOR type %d: [%zu] `%s`.", url_type, + DBGH(stmt, "POSTing CBOR to URL type %d: [%zu] `%s`.", url_type, req_body->cnt, cstr_hex_dump(req_body)); } @@ -1515,7 +1529,7 @@ static BOOL parse_es_version_cbor(esodbc_dbc_st *dbc, cstr_st *rsp_body, /* the _init() doesn't actually validate the object */ res = cbor_value_validate(&top_obj, ES_CBOR_PARSE_FLAGS); CHK_RES(stmt, "failed to validate CBOR object: [%zu] `%s`", - stmt->rset.body.cnt, cstr_hex_dump(&stmt->rset.body)); + rsp_body->cnt, cstr_hex_dump(rsp_body)); # endif /*0*/ # endif /* !NDEBUG */ diff --git a/driver/convert.c b/driver/convert.c index 1306041e..792dd5ce 100644 --- a/driver/convert.c +++ b/driver/convert.c @@ -3494,12 +3494,12 @@ static inline BOOL conv_implemented(SQLSMALLINT sqltype, SQLSMALLINT ctype) } -/* Check if data types in returned columns are compabile with buffer types - * bound for those columns OR if parameter data conversion is allowed. +/* Check (1) if data types in returned columns are compabile with buffer types + * bound for those columns OR (2) if parameter data conversion is allowed. * idx: * if > 0: parameter number for parameter binding; - * if < 0: indicator for bound columns check. - * */ + * if < 0: negated column number to check OR indicator to check all bound + * columns (CONV_CHECK_ALL_COLS). */ SQLRETURN convertability_check(esodbc_stmt_st *stmt, SQLINTEGER idx, int *conv_code) { @@ -3519,7 +3519,9 @@ SQLRETURN convertability_check(esodbc_stmt_st *stmt, SQLINTEGER idx, axd = stmt->ard; ixd = stmt->ird; - start = 0; + /* if this is a SQLGetData() call, only check the one bound column */ + assert(idx == CONV_CHECK_ALL_COLS || STMT_GD_CALLING(stmt)); + start = (idx == CONV_CHECK_ALL_COLS) ? 0 : -idx - 1; stop = axd->count < ixd->count ? axd->count : ixd->count; } else { /* diff --git a/driver/convert.h b/driver/convert.h index bbb14c7d..a6e6f7f1 100644 --- a/driver/convert.h +++ b/driver/convert.h @@ -21,6 +21,15 @@ SQLULEN get_param_size(esodbc_rec_st *irec); inline void *deferred_address(SQLSMALLINT field_id, size_t pos, esodbc_rec_st *rec); + +/* column and parameters are all SQLUSMALLINT (unsigned short) */ +#define CONV_CHECK_ALL_COLS (- ((SQLINTEGER)USHRT_MAX + 1)) +/* Check (1) if data types in returned columns are compabile with buffer types + * bound for those columns OR (2) if parameter data conversion is allowed. + * idx: + * if > 0: parameter number for parameter binding; + * if < 0: negated column number to check OR indicator to check all bound + * columns (CONV_CHECK_ALL_COLS). */ SQLRETURN convertability_check(esodbc_stmt_st *stmt, SQLINTEGER idx, int *conv_code); BOOL update_crr_date(struct tm *now); diff --git a/driver/handles.c b/driver/handles.c index 6b4fb60f..a154a1e7 100644 --- a/driver/handles.c +++ b/driver/handles.c @@ -953,7 +953,7 @@ SQLRETURN EsSQLGetStmtAttrW( /* "determine the number of the current row in the result set" */ case SQL_ATTR_ROW_NUMBER: - *(SQLULEN *)ValuePtr = (SQLULEN)STMT_CRR_ROW_NUMBER(stmt); + *(SQLULEN *)ValuePtr = (SQLULEN)stmt->tv_rows; DBGH(stmt, "getting row number: %llu", *(SQLULEN *)ValuePtr); break; @@ -1499,7 +1499,6 @@ esodbc_desc_st *getdata_set_ard(esodbc_stmt_st *stmt, esodbc_desc_st *gd_ard, SQLUSMALLINT colno, esodbc_rec_st *recs, SQLUSMALLINT count) { SQLRETURN ret; - SQLUSMALLINT i; esodbc_desc_st *ard = stmt->ard; init_desc(gd_ard, stmt, DESC_TYPE_ARD, SQL_DESC_ALLOC_USER); @@ -1511,13 +1510,10 @@ esodbc_desc_st *getdata_set_ard(esodbc_stmt_st *stmt, esodbc_desc_st *gd_ard, } if (colno < count) { /* can the static recs be used? */ - /* need to init all records, not only the single one that will be - * bound, since data covert. check will run against all bound recs. */ - for (i = 0; i < count; i ++) { - init_rec(&recs[i], gd_ard); - } + assert(0 < colno); + init_rec(&recs[colno - 1], gd_ard); - gd_ard->count = count; + gd_ard->count = colno; gd_ard->recs = recs; } /* else: recs will be alloc'd later when binding the column */ @@ -1996,7 +1992,8 @@ static void set_defaults_from_meta_type(esodbc_rec_st *rec) rec->concise_type == SQL_C_DEFAULT) || (DESC_TYPE_IS_IMPLEMENTATION(rec->desc->type) && rec->concise_type == ESODBC_SQL_NULL)); - WARNH(rec->desc, "max meta type: can't set defaults"); + DBGH(rec->desc, "max meta type (C default / SQL NULL): " + "can't set defaults"); break; } } diff --git a/driver/handles.h b/driver/handles.h index 6c2f6f68..44093b3e 100644 --- a/driver/handles.h +++ b/driver/handles.h @@ -206,6 +206,12 @@ typedef struct desc_rec { * need to be set for records in IxD descriptors */ esodbc_estype_st *es_type; + /* IRD reference copy of respective protocol value */ + union { + UJObject json; + CborValue cbor; + } i_val; + /* * record fields */ @@ -302,6 +308,7 @@ typedef struct struct_desc { struct resultset_cbor { cstr_st curs; /* ES'es cursor; refs req's body */ + CborValue rows_obj; /* top object rows container (EsSQLRowCount()) */ CborValue rows_iter; /* iterator over received rows; refs req's body */ wstr_st cols_buff /* columns descriptions; refs allocated chunk */; }; @@ -309,6 +316,7 @@ struct resultset_cbor { struct resultset_json { wstr_st curs; /* ES'es cursor; refs UJSON4C 'state' */ void *state; /* top UJSON decoder state */ + UJObject rows_obj; /* top object rows container (EsSQLRowCount()) */ void *rows_iter; /* UJSON iterator with the rows in result set */ UJObject row_array; /* UJSON object for current row */ }; @@ -317,17 +325,17 @@ typedef struct struct_resultset { long code; /* HTTP code of last response */ cstr_st body; /* HTTP body of last answer to a statement */ + BOOL pack_json; /* the server could send a JSON answer for a CBOR req. */ union { struct resultset_cbor cbor; struct resultset_json json; } pack; - size_t nrows; /* (count of) rows in current result set */ size_t vrows; /* (count of) visited rows in current result set */ } resultset_st; #define STMT_HAS_CURSOR(_stmt) \ - (HDRH(_stmt)->dbc->pack_json ? \ + ((_stmt)->rset.pack_json ? \ (_stmt)->rset.pack.json.curs.cnt : \ (_stmt)->rset.pack.cbor.curs.cnt) @@ -370,8 +378,8 @@ typedef struct struct_stmt { resultset_st rset; /* count of result sets fetched */ size_t nset; - /* total count of fetched rows for one statement (sum(resultset.nrows)) */ - size_t tf_rows; + /* total visited rows (SUM(resultset.vrows)) <=> SQL_ATTR_ROW_NUMBER */ + size_t tv_rows; /* SQL data types conversion to SQL C compatibility (IRD.SQL -> ARD.C) */ enum { CONVERSION_VIOLATION = -2, /* specs disallowed */ @@ -388,17 +396,13 @@ typedef struct struct_stmt { } esodbc_stmt_st; -/* reset total number of fetched rows for a statement */ -#define STMT_TFROWS_RESET(_stmt) \ +/* reset statment's result set count and number of visited rows */ +#define STMT_ROW_CNT_RESET(_stmt) \ do { \ - (_stmt)->tf_rows = 0; \ + (_stmt)->nset = 0; \ + (_stmt)->tv_rows = 0; \ } while (0) -/* 1-based current row number */ -#define STMT_CRR_ROW_NUMBER(_stmt) \ - ((_stmt)->tf_rows - (_stmt)->rset.nrows + \ - (_stmt)->rset.vrows + /*1-based*/1) - /* SQLGetData() state reset */ #define STMT_GD_RESET(_stmt) \ do { \ diff --git a/driver/queries.c b/driver/queries.c index cac528d6..0087282f 100644 --- a/driver/queries.c +++ b/driver/queries.c @@ -6,6 +6,8 @@ #include +#include /* for decode_half() */ + #include "queries.h" #include "log.h" #include "connect.h" @@ -26,9 +28,14 @@ #define PACK_PARAM_COL_DSIZE "display_size" #define PACK_PARAM_CURS_CLOSE "succeeded" - #define MSG_INV_SRV_ANS "Invalid server answer" +/* Macro valid for *_cbor() functions only. + * Assumes presence of a variable 'res' of type CborError in used scope and a + * label named 'err'. */ +#define CHK_RES(_hnd, _fmt, ...) \ + JUMP_ON_CBOR_ERR(res, err, _hnd, _fmt, __VA_ARGS__) + static thread_local cstr_st tz_param; static BOOL print_tz_param(long tz_dst_offt) @@ -141,12 +148,13 @@ BOOL queries_init() void clear_resultset(esodbc_stmt_st *stmt, BOOL on_close) { - INFOH(stmt, "clearing result set; vrows=%zu, nrows=%zu, nset=%zu.", - stmt->rset.vrows, stmt->rset.nrows, stmt->nset); + INFOH(stmt, "clearing result set #%zu, visited rows in set: %zu.", + stmt->nset, stmt->rset.vrows); if (stmt->rset.body.str) { + assert(stmt->rset.body.cnt); free(stmt->rset.body.str); } - if (HDRH(stmt)->dbc->pack_json) { + if (stmt->rset.pack_json) { if (stmt->rset.pack.json.state) { UJFree(stmt->rset.pack.json.state); } @@ -154,13 +162,15 @@ void clear_resultset(esodbc_stmt_st *stmt, BOOL on_close) if (stmt->rset.pack.cbor.cols_buff.cnt) { assert(stmt->rset.pack.cbor.cols_buff.str); free(stmt->rset.pack.cbor.cols_buff.str); + } else { + assert(! stmt->rset.pack.cbor.cols_buff.str); } } memset(&stmt->rset, 0, sizeof(stmt->rset)); if (on_close) { - DBGH(stmt, "on close, total fetched rows=%zu.", stmt->tf_rows); - STMT_TFROWS_RESET(stmt); + INFOH(stmt, "on close, total visited rows: %zu.", stmt->tv_rows); + STMT_ROW_CNT_RESET(stmt); } /* reset SQLGetData state to detect sequence "SQLExec*(); SQLGetData();" */ @@ -219,6 +229,10 @@ static BOOL attach_one_column(esodbc_rec_st *rec, wstr_st *col_name, esodbc_stmt_st *stmt; esodbc_dbc_st *dbc; + /* uncounted 0-term is present */ + assert(col_name->str[col_name->cnt] == '\0'); + assert(col_type->str[col_type->cnt] == '\0'); + stmt = HDRH(rec->desc)->stmt; dbc = HDRH(stmt)->dbc; @@ -386,18 +400,17 @@ static SQLRETURN attach_answer_json(esodbc_stmt_st *stmt) goto err; } stmt->rset.pack.json.rows_iter = UJBeginArray(rows); + /* UJSON4C will return NULL above, for empty array (meh!) */ if (! stmt->rset.pack.json.rows_iter) { - /* UJSON4C will return NULL above, for empty array (meh!) */ - DBGH(stmt, "received empty resultset array: forcing nodata."); STMT_FORCE_NODATA(stmt); - stmt->rset.nrows = 0; - } else { - stmt->nset ++; - /* the cast is made safe by the decoding format indicator for array */ - stmt->rset.nrows = (size_t)UJLengthArray(rows); - stmt->tf_rows += stmt->rset.nrows; } - DBGH(stmt, "rows received in result set: %zd.", stmt->rset.nrows); + /* save the object, as it might be required by EsSQLRowCount() */ + stmt->rset.pack.json.rows_obj = rows; + /* unlike with tinycbor, the count is readily available with ujson4c + * (since the lib parses the entire JSON object upfront) => keep it in + * Release builds. */ + INFOH(stmt, "rows received in current (#%zu) result set: %d.", + stmt->nset + 1, UJLengthArray(rows)); /* * copy ref to ES'es cursor (if there's one) @@ -428,9 +441,6 @@ static SQLRETURN attach_answer_json(esodbc_stmt_st *stmt) RET_HDIAG(stmt, SQL_STATE_HY000, MSG_INV_SRV_ANS, 0); } -/* macro valid for attach_*_cbor() functions only */ -#define CHK_RES(_hnd, _fmt, ...) \ - JUMP_ON_CBOR_ERR(res, err, _hnd, _fmt, __VA_ARGS__) /* Function iterates over the recived "columns" array (of map elements). * The recived column names (and their types) are UTF-8 multi-bytes, which @@ -486,8 +496,7 @@ static BOOL iterate_on_columns(esodbc_stmt_st *stmt, CborValue columns) "' array."); return FALSE; } - res = cbor_map_lookup_keys(&it, keys_cnt, keys, lens, objs, - /*drain*/TRUE); + res = cbor_map_lookup_keys(&it, keys_cnt, keys, lens, objs); CHK_RES(stmt, "failed to lookup keys in '" PACK_PARAM_COLUMNS "' element #%hd", recno); @@ -512,13 +521,15 @@ static BOOL iterate_on_columns(esodbc_stmt_st *stmt, CborValue columns) return FALSE; } if (! wrptr) { /* 1st iter */ - need += n; + need += n + /*\0*/1; } else { /* 2nd iter */ name_wstr.str = wrptr; - name_wstr.cnt = (size_t)n; + name_wstr.cnt = (size_t)n; /* no 0-counting */ wrptr += (size_t)n; left -= n; + *wrptr ++ = '\0'; + left --; } /* @@ -531,6 +542,8 @@ static BOOL iterate_on_columns(esodbc_stmt_st *stmt, CborValue columns) res = cbor_value_get_string_chunk(&type_obj, &type_cstr.str, &type_cstr.cnt); CHK_RES(stmt, "can't fetch value of '" PACK_PARAM_COL_TYPE "' elem"); + /* U8MB_TO_U16WC fails with 0-len source */ + assert(type_cstr.cnt); n = U8MB_TO_U16WC(type_cstr.str, type_cstr.cnt, wrptr, left); if (n <= 0) { @@ -539,13 +552,16 @@ static BOOL iterate_on_columns(esodbc_stmt_st *stmt, CborValue columns) return FALSE; } if (! wrptr) { /* 1st iter */ - need += n; + need += n + /*\0*/1; } else { /* 2nd iter */ type_wstr.str = wrptr; - type_wstr.cnt = (size_t)n; + type_wstr.cnt = (size_t)n; /* no 0-counting */ wrptr += (size_t)n; left -= n; + /* add \0 */ + *wrptr ++ = '\0'; + left --; } if (! wrptr) { /* 1st iter: collect lengths only */ @@ -558,6 +574,8 @@ static BOOL iterate_on_columns(esodbc_stmt_st *stmt, CborValue columns) return FALSE; } } + /* no overflow */ + assert((! wrptr) || left == 0); if ((! wrptr) /* 1st iter: alloc cols slab/buffer */ && (0 < need)) { if (! (wrptr = malloc(need * sizeof(wchar_t)))) { @@ -623,6 +641,9 @@ static SQLRETURN attach_answer_cbor(esodbc_stmt_st *stmt) }; CborValue *vals[] = {&cols_obj, &curs_obj, &rows_obj}; BOOL empty; +# ifndef NDEBUG + size_t nrows; +# endif /* !NDEBUG */ DBGH(stmt, "attaching CBOR answer: [%zu] `%s`.", stmt->rset.body.cnt, cstr_hex_dump(&stmt->rset.body)); @@ -644,8 +665,7 @@ static SQLRETURN attach_answer_cbor(esodbc_stmt_st *stmt) ERRH(stmt, "top object (of type 0x%x) is not a map.", obj_type); goto err; } - res = cbor_map_lookup_keys(&top_obj, keys_no, keys, lens, vals, - /*drain*/FALSE); + res = cbor_map_lookup_keys(&top_obj, keys_no, keys, lens, vals); CHK_RES(stmt, "failed to lookup answer keys in map"); /* @@ -662,17 +682,23 @@ static SQLRETURN attach_answer_cbor(esodbc_stmt_st *stmt) CHK_RES(stmt, "failed to check if '" PACK_PARAM_ROWS "' array is empty"); if (empty) { STMT_FORCE_NODATA(stmt); - DBGH(stmt, "received empty result set."); } else { - stmt->rset.pack.cbor.rows_iter = rows_obj; - stmt->nset ++; - // TODO: get rid of rows-counting / tf_rows - res = cbor_value_is_length_known(&rows_obj) ? - cbor_value_get_array_length(&rows_obj, &stmt->rset.nrows) : - cbor_container_count(rows_obj, &stmt->rset.nrows); + /* Note: "expensive", as it requires ad-hoc parsing; so only keep for + * debugging (switching to JSON should be easy if troubleshooting) */ +# ifndef NDEBUG + res = cbor_get_array_count(rows_obj, &nrows); CHK_RES(stmt, "failed to fetch '" PACK_PARAM_ROWS "' array length"); - stmt->tf_rows += stmt->rset.nrows; - DBGH(stmt, "rows received in result set: %zd.", stmt->rset.nrows); + INFOH(stmt, "rows received in current (#%zu) result set: %zu.", + stmt->nset + 1, nrows); +# endif /* NDEBUG */ + /* save the object, as it might be required by EsSQLRowCount() */ + stmt->rset.pack.cbor.rows_obj = rows_obj; + + /* prepare iterator for EsSQLFetch(); recursing object and iterator + * can be the same, since there's no need to "leave" the container. */ + res = cbor_value_enter_container(&rows_obj, &rows_obj); + CHK_RES(stmt, "failed to access '" PACK_PARAM_ROWS "' container"); + stmt->rset.pack.cbor.rows_iter = rows_obj; } /* @@ -680,7 +706,7 @@ static SQLRETURN attach_answer_cbor(esodbc_stmt_st *stmt) */ if (cbor_value_is_valid(&curs_obj)) { obj_type = cbor_value_get_type(&curs_obj); - if (obj_type != CborByteStringType) { + if (obj_type != CborTextStringType) { ERRH(stmt, "invalid '" PACK_PARAM_CURSOR "' parameter type " "(0x%x)", obj_type); goto err; @@ -759,7 +785,7 @@ static BOOL attach_error_cbor(SQLHANDLE hnd, cstr_st *body) /* the _init() doesn't actually validate the object */ res = cbor_value_validate(&top_obj, ES_CBOR_PARSE_FLAGS); CHK_RES(stmt, "failed to validate CBOR object: [%zu] `%s`", - stmt->rset.body.cnt, cstr_hex_dump(&stmt->rset.body)); + body->cnt, cstr_hex_dump(body)); # endif /*0*/ # endif /* !NDEBUG */ @@ -767,8 +793,7 @@ static BOOL attach_error_cbor(SQLHANDLE hnd, cstr_st *body) ERRH(hnd, "top object (of type 0x%x) is not a map.", obj_type); goto err; } - res = cbor_map_lookup_keys(&top_obj, keys_cnt, keys, lens, vals, - /*drain*/FALSE); + res = cbor_map_lookup_keys(&top_obj, keys_cnt, keys, lens, vals); CHK_RES(hnd, "failed to lookup answer keys in map"); if ((obj_type = cbor_value_get_type(&status_obj)) == CborIntegerType) { @@ -788,7 +813,7 @@ static BOOL attach_error_cbor(SQLHANDLE hnd, cstr_st *body) CborIntegerType) { /* error with root cause */ /* unpack "error" object */ res = cbor_map_lookup_keys(&err_obj, err_keys_cnt, err_keys, err_lens, - err_vals, /*drain*/FALSE); + err_vals); CHK_RES(hnd, "failed to lookup error object keys in map"); /* "type" and "reason" objects must be text strings */ if ((! cbor_value_is_text_string(&type_obj)) || @@ -829,8 +854,6 @@ static BOOL attach_error_cbor(SQLHANDLE hnd, cstr_st *body) return FALSE; } -#undef CHK_RES - /* * Processes a received answer: * - takes a dynamic buffer, answ->str, of length answ->cnt. Will handle the @@ -850,6 +873,7 @@ SQLRETURN TEST_API attach_answer(esodbc_stmt_st *stmt, cstr_st *answer, /* the statement takes ownership of mem obj */ stmt->rset.body = *answer; + stmt->rset.pack_json = is_json; old_ird_cnt = stmt->ird->count; ret = is_json ? attach_answer_json(stmt) : attach_answer_cbor(stmt); @@ -863,7 +887,13 @@ SQLRETURN TEST_API attach_answer(esodbc_stmt_st *stmt, cstr_st *answer, /* new columns have just been attached => force compat. check */ stmt->sql2c_conversion = CONVERSION_UNCHECKED; } + if (STMT_NODATA_FORCED(stmt)) { + DBGH(stmt, "empty result set received."); + } else { + stmt->nset ++; + } } + return ret; } @@ -1058,8 +1088,8 @@ SQLRETURN TEST_API attach_sql(esodbc_stmt_st *stmt, } /* if the app correctly SQL_CLOSE'es the statement, this would not be - * needed. but just in case: re-init counter of total # of rows */ - STMT_TFROWS_RESET(stmt); + * needed. but just in case: re-init counter of total # of rows and sets */ + STMT_ROW_CNT_RESET(stmt); return SQL_SUCCESS; } @@ -1070,6 +1100,7 @@ SQLRETURN TEST_API attach_sql(esodbc_stmt_st *stmt, void detach_sql(esodbc_stmt_st *stmt) { if (! stmt->u8sql.str) { + assert(! stmt->u8sql.cnt); return; } free(stmt->u8sql.str); @@ -1217,18 +1248,38 @@ SQLRETURN EsSQLBindCol( RET_STATE(stmt->hdr.diag.state); } +static SQLRETURN set_row_diag(esodbc_desc_st *ird, + esodbc_state_et state, const char *msg, + SQLULEN pos, SQLINTEGER colno) +{ + esodbc_stmt_st *stmt = HDRH(ird)->stmt; + SQLWCHAR wbuff[SQL_MAX_MESSAGE_LENGTH], *wmsg = NULL; + int res; + + if (ird->array_status_ptr) { + ird->array_status_ptr[pos] = SQL_ROW_ERROR; + } + if (msg) { + res = ascii_c2w((SQLCHAR *)msg, wbuff, SQL_MAX_MESSAGE_LENGTH - 1); + if (0 < res) { + wmsg = wbuff; + } + } + return post_row_diagnostic(stmt, state, wmsg, /*code*/0, + stmt->tv_rows + /*current*/1, colno); + +} + /* * Copy one row from IRD to ARD. * pos: row number in the rowset - * Returns: ... */ -SQLRETURN copy_one_row(esodbc_stmt_st *stmt, SQLULEN pos) +SQLRETURN copy_one_row_json(esodbc_stmt_st *stmt, SQLULEN pos) { - SQLSMALLINT i; - SQLLEN rowno; - SQLRETURN ret; + SQLINTEGER i; + size_t rowno; UJObject obj; - void *iter_row; + SQLRETURN ret; SQLLEN *ind_len; long long ll; double dbl; @@ -1239,88 +1290,60 @@ SQLRETURN copy_one_row(esodbc_stmt_st *stmt, SQLULEN pos) esodbc_desc_st *ard, *ird; esodbc_rec_st *arec, *irec; - rowno = (SQLLEN)STMT_CRR_ROW_NUMBER(stmt); ard = stmt->ard; ird = stmt->ird; - -#define RET_ROW_DIAG(_state, _message, _colno) \ - do { \ - if (ird->array_status_ptr) { \ - ird->array_status_ptr[pos] = SQL_ROW_ERROR; \ - } \ - return post_row_diagnostic(stmt, _state, MK_WPTR(_message), \ - 0, rowno, _colno); \ - } while (0) -#define SET_ROW_DIAG(_rowno, _colno) \ - do { \ - stmt->hdr.diag.row_number = _rowno; \ - stmt->hdr.diag.column_number = _colno; \ - } while (0) - - /* is current object an array? */ - if (! UJIsArray(stmt->rset.pack.json.row_array)) { - ERRH(stmt, "one '%s' element (#%zd) in result set not an array; type:" - " %d.", PACK_PARAM_ROWS, stmt->rset.vrows, - UJGetType(stmt->rset.pack.json.row_array)); - RET_ROW_DIAG(SQL_STATE_HY000, MSG_INV_SRV_ANS, SQL_NO_COLUMN_NUMBER); - } - /* are there elements in this row array to at least match the number of - * columns? */ - if (UJLengthArray(stmt->rset.pack.json.row_array) < ird->count) { - ERRH(stmt, "current row counts less elements (%d) than columns (%hd)", - UJLengthArray(stmt->rset.pack.json.row_array), ird->count); - RET_ROW_DIAG(SQL_STATE_HY000, MSG_INV_SRV_ANS, SQL_NO_COLUMN_NUMBER); - } else if (ird->count < UJLengthArray(stmt->rset.pack.json.row_array)) { - WARNH(stmt, "current row counts more elements (%d) than columns (%hd)", - UJLengthArray(stmt->rset.pack.json.row_array), ird->count); - } - /* get an iterator over the row array */ - if (! (iter_row = UJBeginArray(stmt->rset.pack.json.row_array))) { - ERRH(stmt, "Failed to obtain iterator on row (#%zd): %s.", rowno, - UJGetError(stmt->rset.pack.json.state)); - RET_ROW_DIAG(SQL_STATE_HY000, MSG_INV_SRV_ANS, SQL_NO_COLUMN_NUMBER); - } + rowno = stmt->tv_rows + /* current not yet counted */1; with_info = FALSE; - /* iterate over the bound cols and contents of one (table) row */ - for (i = 0; i < ard->count && UJIterArray(&iter_row, &obj); i ++) { + /* iterate over the bound cols of one (table) row */ + assert((! STMT_GD_CALLING(stmt)) || 0 < stmt->gd_col); + for (i = STMT_GD_CALLING(stmt) ? stmt->gd_col -1 : 0; i < ard->count; + i ++) { arec = &ard->recs[i]; /* access safe if 'i < ard->count' */ /* if record not bound skip it */ if (! REC_IS_BOUND(arec)) { DBGH(stmt, "column #%d not bound, skipping it.", i + 1); continue; } + DBGH(stmt, "column #%d is bound, copying data.", i + 1); + if (ird->count <= i) { + ERRH(stmt, "only %hd columns in result set, no data to return in " + "column #%hd.", ird->count, i + 1); + return set_row_diag(ird, SQL_STATE_HY000, MSG_INV_SRV_ANS, pos, + i + 1); + } else { + irec = &ird->recs[i]; + } - /* access made safe by ird->count match against array len above */ - irec = &ird->recs[i]; - + obj = irec->i_val.json; switch (UJGetType(obj)) { default: ERRH(stmt, "unexpected object of type %d in row L#%zu/T#%zd.", UJGetType(obj), stmt->rset.vrows, rowno); - RET_ROW_DIAG(SQL_STATE_HY000, MSG_INV_SRV_ANS, i + 1); - /* RET_.. returns */ + return set_row_diag(ird, SQL_STATE_HY000, MSG_INV_SRV_ANS, pos, + i + 1); case UJT_Null: DBGH(stmt, "value [%zd, %d] is NULL.", rowno, i + 1); - /* Note: if ever causing an issue, check - * arec->es_type->nullable before returning NULL to app */ ind_len = deferred_address(SQL_DESC_INDICATOR_PTR, pos, arec); if (! ind_len) { ERRH(stmt, "no buffer to signal NULL value."); - RET_ROW_DIAG(SQL_STATE_22002, "Indicator variable required" - " but not supplied", i + 1); + return set_row_diag(ird, SQL_STATE_22002, NULL, pos, + i + 1); + } + if (arec->es_type && (! arec->es_type->nullable)) { + WARNH(stmt, "returning NULL for non-nullable type."); } *ind_len = SQL_NULL_DATA; continue; /* instead of break! no 'ret' processing to do. */ case UJT_String: wstr = UJReadString(obj, &len); - DBGH(stmt, "value [%zd, %d] is string [%d]:`" LWPDL "`.", + DBGH(stmt, "value [%zd, %d] is string: [%d] `" LWPDL "`.", rowno, i + 1, len, len, wstr); /* UJSON4C returns chars count, but 0-terminates w/o counting * the terminator */ - assert(wstr[len] == 0); + assert(wstr[len] == '\0'); /* "When character data is returned from the driver to the * application, the driver must always null-terminate it." */ ret = sql2c_string(arec, irec, pos, wstr, len + /*\0*/1); @@ -1352,15 +1375,22 @@ SQLRETURN copy_one_row(esodbc_stmt_st *stmt, SQLULEN pos) break; } + /* set the (row, column) details in the diagnostic, in case the + * value copying isn't a clean success (i.e. success with info or an + * error) */ switch (ret) { case SQL_SUCCESS_WITH_INFO: with_info = TRUE; - SET_ROW_DIAG(rowno, i + 1); + stmt->hdr.diag.row_number = rowno; + stmt->hdr.diag.column_number = i + 1; + /* no break */ case SQL_SUCCESS: - break; + break; /* continue iteration over row's values */ + default: /* error */ - SET_ROW_DIAG(rowno, i + 1); - return ret; + stmt->hdr.diag.row_number = rowno; + stmt->hdr.diag.column_number = i + 1; + return ret; /* row fetching failed */ } } @@ -1372,11 +1402,305 @@ SQLRETURN copy_one_row(esodbc_stmt_st *stmt, SQLULEN pos) } return with_info ? SQL_SUCCESS_WITH_INFO : SQL_SUCCESS; +} -#undef RET_ROW_DIAG -#undef SET_ROW_DIAG +static SQLRETURN unpack_one_row_json(esodbc_stmt_st *stmt, SQLULEN pos) +{ + SQLSMALLINT i; + size_t rowno; + UJObject obj; + void *iter_row; + esodbc_desc_st *ird; + esodbc_rec_st *irec; + + ird = stmt->ird; + rowno = stmt->tv_rows + /* current not yet counted */1; + + /* is current object an array? */ + if (! UJIsArray(stmt->rset.pack.json.row_array)) { + ERRH(stmt, "one '%s' element (#%zu) in result set not an array; type:" + " %d.", PACK_PARAM_ROWS, stmt->rset.vrows, + UJGetType(stmt->rset.pack.json.row_array)); + return set_row_diag(ird, SQL_STATE_HY000, MSG_INV_SRV_ANS, pos, + SQL_NO_COLUMN_NUMBER); + } + /* get an iterator over the row array */ + if (! (iter_row = UJBeginArray(stmt->rset.pack.json.row_array))) { + ERRH(stmt, "Failed to obtain iterator on row (#%zd): %s.", rowno, + UJGetError(stmt->rset.pack.json.state)); + return set_row_diag(ird, SQL_STATE_HY000, MSG_INV_SRV_ANS, pos, + SQL_NO_COLUMN_NUMBER); + } + + /* iterate over the elements in current (table) row and reference-copy + * their value */ + for (i = 0; i < ird->count; i ++) { + /* access made safe by ird->count match against array len above */ + irec = &ird->recs[i]; + if (! UJIterArray(&iter_row, &irec->i_val.json)) { + ERRH(stmt, "current row %zd counts fewer elements: %hd than " + "columns: %hd.", rowno, i + 1, ird->count); + return set_row_diag(ird, SQL_STATE_HY000, MSG_INV_SRV_ANS, pos, + i + 1); + } + } + + /* Are there further elements in row? This could indicate that the data + * returned is not the data asked for => safer to fail. */ + if (UJIterArray(&iter_row, &obj)) { + ERRH(stmt, "current row %zd counts more elems than columns.", rowno); + return set_row_diag(ird, SQL_STATE_HY000, MSG_INV_SRV_ANS, pos, + SQL_NO_COLUMN_NUMBER); + } + + /* copy values, if there's already any bound column */ + return 0 < stmt->ard->count ? copy_one_row_json(stmt, pos) : SQL_SUCCESS; } + +/* + * Copy one row from IRD to ARD. + * pos: row number in the rowset + */ +static SQLRETURN copy_one_row_cbor(esodbc_stmt_st *stmt, SQLULEN pos) +{ + SQLRETURN ret; + CborError res; + CborValue obj; + CborType elem_type; + SQLINTEGER i; + esodbc_desc_st *ard, *ird; + esodbc_rec_st *arec, *irec; + size_t rowno; + BOOL with_info; + SQLLEN *ind_len; + int64_t i64; + wstr_st wstr; + bool boolval; + double dbl; + uint16_t ui16; + float flt; + + ard = stmt->ard; + ird = stmt->ird; + rowno = stmt->tv_rows + /* current row not yet counted */1; + + with_info = FALSE; + /* iterate over the bound cols and contents of one (table) row */ + assert((! STMT_GD_CALLING(stmt)) || 0 < stmt->gd_col); + for (i = STMT_GD_CALLING(stmt) ? stmt->gd_col -1 : 0; i < ard->count; + i ++) { + arec = &ard->recs[i]; /* access safe if 'i < ard->count' */ + /* if record not bound skip it */ + if (! REC_IS_BOUND(arec)) { + DBGH(stmt, "column #%d not bound, skipping it.", i + 1); + continue; + } + DBGH(stmt, "column #%d is bound, copying data.", i + 1); + if (ird->count <= i) { + ERRH(stmt, "only %hd columns in result set, no data to return in " + "column #%hd.", ird->count, i + 1); + return set_row_diag(ird, SQL_STATE_HY000, MSG_INV_SRV_ANS, pos, + i + 1); + } else { + irec = &ird->recs[i]; + } + + obj = irec->i_val.cbor; + elem_type = cbor_value_get_type(&obj); + DBGH(stmt, "current element of type: 0x%x", elem_type); + switch (elem_type) { + case CborByteStringType: + case CborArrayType: + case CborMapType: + case CborSimpleType: + case CborUndefinedType: + case CborTagType: + default: /* + CborInvalidType */ + ERRH(stmt, "unexpected elem. of type 0x%x in row", elem_type); + goto err; + + case CborNullType: + DBGH(stmt, "value [%zd, %d] is NULL.", rowno, i + 1); + ind_len = deferred_address(SQL_DESC_INDICATOR_PTR, pos, arec); + if (! ind_len) { + ERRH(stmt, "no buffer to signal NULL value."); + return set_row_diag(ird, SQL_STATE_22002, NULL, pos, + i + 1); + } + if (arec->es_type && (! arec->es_type->nullable)) { + WARNH(stmt, "returning NULL for non-nullable type."); + } + *ind_len = SQL_NULL_DATA; + ret = SQL_SUCCESS; + break; + + case CborTextStringType: + res = cbor_value_get_utf16_wstr(&obj, &wstr); + CHK_RES(stmt, "failed to extract text string"); + DBGH(stmt, "value [%zd, %d] is string: [%zu] `" LWPDL "`.", + rowno, i + 1, wstr.cnt, LWSTR(&wstr)); + /* UTF8/16 conversion terminates the string */ + assert(wstr.str[wstr.cnt] == '\0'); + /* "When character data is returned from the driver to the + * application, the driver must always null-terminate it." */ + ret = sql2c_string(arec, irec, pos, wstr.str, wstr.cnt + 1); + break; + + case CborIntegerType: + res = cbor_value_get_int64_checked(&obj, &i64); + CHK_RES(stmt, "failed to extract int64 value"); + DBGH(stmt, "value [%zd, %d] is integer: %I64d.", rowno, + i + 1, i64); + assert(sizeof(int64_t) == sizeof(long long)); + ret = sql2c_longlong(arec, irec, pos, (long long)i64); + break; + + /*INDENT-OFF*/ + do { + case CborHalfFloatType: + res = cbor_value_get_half_float(&obj, &ui16); + /* res not yet checked, but likely(OK) */ + dbl = decode_half(ui16); + break; + case CborFloatType: + res = cbor_value_get_float(&obj, &flt); + dbl = (double)flt; + break; + case CborDoubleType: + res = cbor_value_get_double(&obj, &dbl); + break; + } while (0); + CHK_RES(stmt, "failed to extract flt. point type 0x%x", + elem_type); + DBGH(stmt, "value [%zd, %d] is double: %f.", rowno, + i + 1, dbl); + ret = sql2c_double(arec, irec, pos, dbl); + break; + /*INDENT-ON*/ + + case CborBooleanType: + res = cbor_value_get_boolean(&obj, &boolval); + CHK_RES(stmt, "failed to extract boolean value"); + DBGH(stmt, "value [%zd, %d] is boolean: %d.", rowno, + i + 1, boolval); + /* 'When bit SQL data is converted to character C data, the + * possible values are "0" and "1".' */ + ret = sql2c_longlong(arec, irec, pos, (long long)!!boolval); + break; + } + + /* set the (row, column) details in the diagnostic, in case the + * value copying isn't a clean success (i.e. success with info or an + * error) */ + switch (ret) { + case SQL_SUCCESS_WITH_INFO: + with_info = TRUE; + stmt->hdr.diag.row_number = rowno; + stmt->hdr.diag.column_number = i + 1; + /* no break */ + case SQL_SUCCESS: + break; /* continue iteration over row's values */ + + default: /* error */ + stmt->hdr.diag.row_number = rowno; + stmt->hdr.diag.column_number = i + 1; + return ret; /* row fetching failed */ + } + } + + if (ird->array_status_ptr) { + ird->array_status_ptr[pos] = with_info ? SQL_ROW_SUCCESS_WITH_INFO : + SQL_ROW_SUCCESS; + DBGH(stmt, "status array @0x%p#%d set to %hu.", ird->array_status_ptr, + pos, ird->array_status_ptr[pos]); + } + return with_info ? SQL_SUCCESS_WITH_INFO : SQL_SUCCESS; + +err: + return set_row_diag(ird, SQL_STATE_HY000, MSG_INV_SRV_ANS, pos, i + 1); +} + +static SQLRETURN unpack_one_row_cbor(esodbc_stmt_st *stmt, SQLULEN pos) +{ + CborError res; + CborValue *rows_iter, it; + CborType elem_type; + SQLSMALLINT i; + esodbc_desc_st *ird; + esodbc_rec_st *irec; + size_t rowno; + + ird = stmt->ird; + rowno = stmt->tv_rows + /* current row not yet counted */1; + + i = -1; + rows_iter = &stmt->rset.pack.cbor.rows_iter; + /* is current object an array? */ + if ((elem_type = cbor_value_get_type(rows_iter)) != CborArrayType) { + ERRH(stmt, "current (#%zu) element of type 0x%x is not an array -- " + "skipping it.", stmt->rset.vrows, elem_type); + goto err; + } + /* enter the row array */ + if ((res = cbor_value_enter_container(rows_iter, &it)) != CborNoError) { + ERRH(stmt, "failed to enter row array: %s.", cbor_error_string(res)); + goto err; + } + + /* iterate over the elements in current (table) row and reference-copy + * their value */ + for (i = 0; i < ird->count; i ++) { + if (cbor_value_at_end(&it)) { + ERRH(stmt, "current row %zd counts fewer elements: %hd than " + "columns: %hd.", rowno, i + 1, ird->count); + } + irec = &ird->recs[i]; + irec->i_val.cbor = it; + assert(! cbor_value_is_tag(&it)); + + res = cbor_value_advance(&it); + if (res != CborNoError) { + ERRH(stmt, "failed to advance past current value in row array: " + "%s.", cbor_error_string(res)); + goto err; + } + } + + /* Are there further elements in row? This could indicate that the data + * returned is not the data asked for => safer to fail. */ + if (! cbor_value_at_end(&it)) { + ERRH(stmt, "current row %zd counts more elems than columns.", rowno); +# ifdef NDEBUG + return set_row_diag(ird, SQL_STATE_HY000, MSG_INV_SRV_ANS, pos, + SQL_NO_COLUMN_NUMBER); +# else /* NDEBUG */ + assert(0); + res = cbor_value_exit_container(rows_iter, &it); +# endif /* NDEBUG */ + } else { + res = cbor_value_leave_container(rows_iter, &it); + } + if (res != CborNoError) { + ERRH(stmt, "failed to exit row array: %s.", cbor_error_string(res)); + return set_row_diag(ird, SQL_STATE_HY000, MSG_INV_SRV_ANS, pos, + SQL_NO_COLUMN_NUMBER); + } + + return 0 < stmt->ard->count ? copy_one_row_cbor(stmt, pos) : SQL_SUCCESS; + +err: + res = (i < 0) ? cbor_value_advance(rows_iter) : + cbor_value_exit_container(rows_iter, &it); + if (res != CborNoError) { + ERRH(stmt, "failed to %s current row: %s.", (i < 0) ? "skip" : "exit", + cbor_error_string(res)); + } + return set_row_diag(ird, SQL_STATE_HY000, MSG_INV_SRV_ANS, pos, + SQL_NO_COLUMN_NUMBER); +} + + /* * "SQLFetch and SQLFetchScroll use the rowset size at the time of the call to * determine how many rows to fetch." @@ -1439,10 +1763,9 @@ SQLRETURN EsSQLFetch(SQLHSTMT StatementHandle) esodbc_desc_st *ard, *ird; SQLULEN i, j, errors; SQLRETURN ret; + BOOL empty, pack_json; stmt = STMH(StatementHandle); - ard = stmt->ard; - ird = stmt->ird; if (! STMT_HAS_RESULTSET(stmt)) { if (STMT_NODATA_FORCED(stmt)) { @@ -1453,12 +1776,6 @@ SQLRETURN EsSQLFetch(SQLHSTMT StatementHandle) RET_HDIAGS(stmt, SQL_STATE_HY010); } - /* TODO: remove guard on CBOR complete implementation. */ - if (! HDRH(stmt)->dbc->pack_json) { - FIXME; - return SQL_NO_DATA; - } - /* Check if the data [type] stored in DB is compatiblie with the buffer * [type] the application provides. This test can only be done at * fetch-time, since the application can unbind/rebind columns at any time @@ -1483,7 +1800,7 @@ SQLRETURN EsSQLFetch(SQLHSTMT StatementHandle) break; case CONVERSION_UNCHECKED: - ret = convertability_check(stmt, /*col bind check*/-1, + ret = convertability_check(stmt, CONV_CHECK_ALL_COLS, (int *)&stmt->sql2c_conversion); if (! SQL_SUCCEEDED(ret)) { return ret; @@ -1494,38 +1811,45 @@ SQLRETURN EsSQLFetch(SQLHSTMT StatementHandle) DBGH(stmt, "ES/app data/buffer types found compatible."); } - DBGH(stmt, "cursor @ row %zu / %zu in page #%zu. (SQL: `" LCPDL "`).", - stmt->rset.vrows, stmt->rset.nrows, stmt->nset, LCSTR(&stmt->u8sql)); + DBGH(stmt, "cursor found @ row # %zu in set # %zu.", stmt->rset.vrows, + stmt->nset); /* reset SQLGetData state, to reset fetch position */ STMT_GD_RESET(stmt); - DBGH(stmt, "rowset max size: %zu.", ard->array_size); + ard = stmt->ard; + ird = stmt->ird; + pack_json = stmt->rset.pack_json; + + DBGH(stmt, "rowset size: %zu.", ard->array_size); errors = 0; i = 0; - /* for all rows in rowset/array, iterate over rows in current resultset */ + /* for all rows in rowset/array (of application), iterate over rows in + * current resultset (of data source) */ while (i < ard->array_size) { - if (! UJIterArray(&stmt->rset.pack.json.rows_iter, - &stmt->rset.pack.json.row_array)) { - DBGH(stmt, "ran out of rows in current result set: nrows=%zd, " - "vrows=%zd.", stmt->rset.nrows, stmt->rset.vrows); - if (stmt->rset.pack.json.curs.cnt) { /* is there an ES cursor? */ + /* is there any array left in resultset? */ + empty = pack_json ? (! UJIterArray(&stmt->rset.pack.json.rows_iter, + &stmt->rset.pack.json.row_array)) : + cbor_value_at_end(&stmt->rset.pack.cbor.rows_iter); + + if (empty) { + DBGH(stmt, "ran out of rows in current result set."); + if (STMT_HAS_CURSOR(stmt)) { /* is there an ES cursor? */ ret = EsSQLExecute(stmt); if (! SQL_SUCCEEDED(ret)) { - ERRH(stmt, "failed to fetch next results."); + ERRH(stmt, "failed to fetch next resultset."); return ret; - } else { - assert(STMT_HAS_RESULTSET(stmt)); } + assert(STMT_HAS_RESULTSET(stmt)); if (! STMT_NODATA_FORCED(stmt)) { /* resume copying from the new resultset, staying on the * same position in rowset. */ continue; } } - - DBGH(stmt, "reached end of entire result set. fetched=%zd.", - stmt->tf_rows); + /* no cursor and no row left in resultset array: the End. */ + DBGH(stmt, "reached end of entire result set; fetched=%zd.", + stmt->tv_rows); /* indicate the non-processed rows in rowset */ if (ird->array_status_ptr) { DBGH(stmt, "setting rest of %zu rows in status array to " @@ -1534,18 +1858,28 @@ SQLRETURN EsSQLFetch(SQLHSTMT StatementHandle) ird->array_status_ptr[j] = SQL_ROW_NOROW; } } - /* stop the copying loop */ break; } - ret = copy_one_row(stmt, i); + + /* Unpack one row, then, if any columns are bound, transfer it to the + * application. + * Unpacking involves (a: JSON) iterating or (b: CBOR) parsing and + * iterating over the row-array and copying the respective format's + * object reference into the IRD records. + * These two are now separate steps since SQLGetData() binds/unbinds + * one column at a time (after SQLFetch()), which for CBOR would + * involve re-parsing the row for each column otherwise. */ + ret = pack_json ? unpack_one_row_json(stmt, i) : + unpack_one_row_cbor(stmt, i); if (! SQL_SUCCEEDED(ret)) { - ERRH(stmt, "copying row %zu failed.", stmt->rset.vrows + 1); + ERRH(stmt, "fetching row %zu failed.", stmt->rset.vrows + 1); errors ++; } i ++; /* account for processed rows */ stmt->rset.vrows ++; + stmt->tv_rows ++; } /* return number of processed rows (even if 0) */ @@ -1566,8 +1900,8 @@ SQLRETURN EsSQLFetch(SQLHSTMT StatementHandle) return SQL_ERROR; } - DBGH(stmt, "cursor left @ row %zu / %zu in page #%zu.", stmt->rset.vrows, - stmt->rset.nrows, stmt->nset); + DBGH(stmt, "cursor left @ row # %zu in set # %zu.", stmt->rset.vrows, + stmt->nset); /* only failures need stmt.diag defer'ing */ return SQL_SUCCESS; @@ -1605,12 +1939,15 @@ static SQLRETURN gd_checks(esodbc_stmt_st *stmt, SQLUSMALLINT colno) "(array_size=%llu).", stmt->ard->array_size); RET_HDIAGS(stmt, SQL_STATE_HYC00); } - /* has SQLFetch() been called? rset is reset with every new result */ - if (! stmt->rset.pack.json.row_array) { - /* DM should have detected this case */ - ERRH(stmt, "SQLFetch() hasn't yet been called on result set."); - RET_HDIAGS(stmt, SQL_STATE_24000); +# ifndef NDEBUG + /* has SQLFetch() been called? DM should have detected this case */ + assert(stmt->ird->count); + if (stmt->rset.pack_json) { + assert(stmt->ird->recs[0].i_val.json); + } else { + assert(cbor_value_is_valid(&stmt->ird->recs[0].i_val.cbor)); } +# endif /* !NDEBUG */ /* colno will be used as 1-based index, if not using bookmarks */ if (colno < 1) { // TODO: add bookmark check (when implementing it) ERRH(stmt, "column number (%hu) can be less than 1.", colno); @@ -1728,13 +2065,14 @@ SQLRETURN EsSQLGetData( } /* check if data types are compatible/convertible */ - ret = convertability_check(stmt, /*col bind check*/-1, NULL); + ret = convertability_check(stmt, -ColumnNumber, NULL); if (! SQL_SUCCEEDED(ret)) { goto end; } /* copy the data */ - ret = copy_one_row(stmt, 0); + ret = stmt->rset.pack_json ? copy_one_row_json(stmt, 0) : + copy_one_row_cbor(stmt, 0); if (! SQL_SUCCEEDED(ret)) { goto end; } @@ -1836,8 +2174,8 @@ SQLRETURN EsSQLMoreResults(SQLHSTMT hstmt) return SQL_NO_DATA; } -SQLRETURN close_es_answ_handler(esodbc_stmt_st *stmt, cstr_st *body, - BOOL is_json) +static SQLRETURN close_es_handler_json(esodbc_stmt_st *stmt, cstr_st *body, + BOOL *closed) { UJObject obj, succeeded; void *state = NULL; @@ -1847,11 +2185,6 @@ SQLRETURN close_es_answ_handler(esodbc_stmt_st *stmt, cstr_st *body, MK_WPTR(PACK_PARAM_CURS_CLOSE) }; - /* TODO: remove guard on CBOR complete implementation. */ - if (! is_json) { - FIXME; - goto err; - } obj = UJDecode(body->str, body->cnt, NULL, &state); if (! obj) { ERRH(stmt, "failed to decode JSON answer: %s ([%zu] `" LTPDL "`).", @@ -1866,23 +2199,90 @@ SQLRETURN close_es_answ_handler(esodbc_stmt_st *stmt, cstr_st *body, } switch (UJGetType(succeeded)) { case UJT_False: - ERRH(stmt, "failed to close cursor on server side."); - assert(0); - /* no break: not a driver/client error -- server would answer with - * an error answer */ + *closed = FALSE; + break; case UJT_True: - free(body->str); - return SQL_SUCCESS; - + *closed = TRUE; + break; default: ERRH(stmt, "invalid obj type in answer: %d ([%zu] `" LTPDL "`).", UJGetType(succeeded), body->cnt, LCSTR(body)); + goto err; } + + return SQL_SUCCESS; err: - free(body->str); RET_HDIAG(stmt, SQL_STATE_HY000, MSG_INV_SRV_ANS, 0); } +static SQLRETURN close_es_handler_cbor(esodbc_stmt_st *stmt, cstr_st *body, + BOOL *closed) +{ + CborError res; + CborParser parser; + CborType obj_type; + CborValue top_obj, succ_obj; + bool boolval; + + res = cbor_parser_init(body->str, body->cnt, ES_CBOR_PARSE_FLAGS, + &parser, &top_obj); + CHK_RES(stmt, "failed to init CBOR parser for object: [%zu] `%s`", + body->cnt, cstr_hex_dump(body)); +# ifndef NDEBUG +# if 0 // ES uses indefinite-length containers (TODO) which trips this check + /* the _init() doesn't actually validate the object */ + res = cbor_value_validate(&top_obj, ES_CBOR_PARSE_FLAGS); + CHK_RES(stmt, "failed to validate CBOR object: [%zu] `%s`", + body->cnt, cstr_hex_dump(body)); +# endif /*0*/ +# endif /* !NDEBUG */ + + if ((obj_type = cbor_value_get_type(&top_obj)) != CborMapType) { + ERRH(stmt, "top object (of type 0x%x) is not a map.", obj_type); + goto err; + } + res = cbor_value_map_find_value(&top_obj, PACK_PARAM_CURS_CLOSE, + &succ_obj); + CHK_RES(stmt, "failed to get '" PACK_PARAM_CURS_CLOSE "' object in answ."); + if ((obj_type = cbor_value_get_type(&succ_obj)) != CborBooleanType) { + ERRH(stmt, "object '" PACK_PARAM_CURS_CLOSE "' (of type 0x%x) is not a" + " boolean.", obj_type); + goto err; + } + res = cbor_value_get_boolean(&succ_obj, &boolval); + CHK_RES(stmt, "failed to extract boolean value"); + *closed = boolval; + return SQL_SUCCESS; +err: + RET_HDIAG(stmt, SQL_STATE_HY000, MSG_INV_SRV_ANS, 0); +} + +SQLRETURN close_es_answ_handler(esodbc_stmt_st *stmt, cstr_st *body, + BOOL is_json) +{ + SQLRETURN ret; + BOOL closed; + + ret = is_json ? close_es_handler_json(stmt, body, &closed) : + close_es_handler_cbor(stmt, body, &closed); + + free(body->str); + + if (! SQL_SUCCEEDED(ret)) { + return ret; + } + + if (! closed) { + ERRH(stmt, "failed to close cursor on server side."); + assert(0); + /* indicate success: not a driver/client error -- server would answer + * with an error answer if that'd be the case */ + } + DBGH(stmt, "cursor closed on server side."); + + return SQL_SUCCESS; +} + SQLRETURN close_es_cursor(esodbc_stmt_st *stmt) { SQLRETURN ret; @@ -1902,17 +2302,22 @@ SQLRETURN close_es_cursor(esodbc_stmt_st *stmt) free(body.str); } - /* the actual cursor freeing occurs in clear_resultset() */ - if (HDRH(stmt)->dbc->pack_json) { +# ifndef NDEBUG + /* the actual cursor freeing occurs in clear_resultset() (part of + * UJSON4C's state or the actual response body, in case of CBOR) */ + if (stmt->rset.pack_json) { DBGH(stmt, "clearing JSON cursor: [%zd] `" LWPDL "`.", stmt->rset.pack.json.curs.cnt, LWSTR(&stmt->rset.pack.json.curs)); - stmt->rset.pack.json.curs.cnt = 0; } else { DBGH(stmt, "clearing CBOR cursor: [%zd] `%s`.", stmt->rset.pack.cbor.curs.cnt, cstr_hex_dump(&stmt->rset.pack.cbor.curs)); - stmt->rset.pack.cbor.curs.cnt = 0; } + /* clear both possible cursors in union: next cursor could be received + * over different encapsulation than the one in current result set */ + stmt->rset.pack.json.curs.cnt = 0; + stmt->rset.pack.cbor.curs.cnt = 0; +# endif /* NDEBUG */ return ret; } @@ -2442,8 +2847,8 @@ static SQLRETURN convert_param_val(esodbc_rec_st *arec, esodbc_rec_st *irec, /* Forms the JSON array with params: * [{"type": "", "value": }(,etc)*] */ -static SQLRETURN serialize_params(esodbc_stmt_st *stmt, char *dest, - size_t *len, BOOL as_json) +static SQLRETURN serialize_params_json(esodbc_stmt_st *stmt, char *dest, + size_t *len) { /* JSON keys for building one parameter object */ # define JSON_KEY_TYPE "{\"type\": \"" @@ -2454,10 +2859,6 @@ static SQLRETURN serialize_params(esodbc_stmt_st *stmt, char *dest, SQLSMALLINT i; size_t l, pos; - if (! as_json) { - FIXME; // FIXME; add CBOR support - } - pos = 0; if (dest) { dest[pos] = '['; @@ -2518,20 +2919,31 @@ static SQLRETURN serialize_params(esodbc_stmt_st *stmt, char *dest, # undef JSON_KEY_VALUE } -static SQLRETURN statement_cbor_len(esodbc_stmt_st *stmt, size_t *outlen, + +static SQLRETURN statement_len_cbor(esodbc_stmt_st *stmt, size_t *outlen, size_t *keys) { SQLRETURN ret; - size_t bodylen, len; + size_t bodylen, len, curslen; esodbc_dbc_st *dbc = HDRH(stmt)->dbc; /* Initial all-encompassing map preamble. */ bodylen = cbor_nn_hdr_len(REST_REQ_KEY_COUNT); /* max count */ *keys = 1; /* cursor or query */ - if (stmt->rset.pack.cbor.curs.cnt) { /* eval CURSOR object length */ + curslen = STMT_HAS_CURSOR(stmt); + if (curslen) { /* eval CURSOR object length */ bodylen += cbor_str_obj_len(sizeof(REQ_KEY_CURSOR) - 1); - bodylen += cbor_str_obj_len(stmt->rset.pack.cbor.curs.cnt); + bodylen += cbor_str_obj_len(curslen); + if (stmt->rset.pack_json) { + /* if current result set was delivered as JSON, we'll need to + * convert the UTF16-stored cursor to ASCII (see note in + * statement_len_json()); for that, we'll use the serialization + * buffer, writing in it ahead of where the cursor will be + * CBOR-packed. The offset must be at least equal to the CBOR + * string object header's length. */ + bodylen += cbor_nn_hdr_len(curslen); + } } else { /* eval QUERY object length */ bodylen += cbor_str_obj_len(sizeof(REQ_KEY_QUERY) - 1); bodylen += cbor_str_obj_len(stmt->u8sql.cnt); @@ -2539,8 +2951,12 @@ static SQLRETURN statement_cbor_len(esodbc_stmt_st *stmt, size_t *outlen, /* does the statement have any parameters? */ if (stmt->apd->count) { bodylen += cbor_str_obj_len(sizeof(REQ_KEY_PARAMS) - 1); - ret = serialize_params(stmt, /* no copy, just eval */NULL, &len, - /*as JSON*/FALSE); + // TODO: + // ret = serialize_params_cbor(stmt, /* no copy, just eval */NULL, + // &len); + ret = SQL_ERROR; + len = 0; + FIXME; // TODO if (! SQL_SUCCEEDED(ret)) { ERRH(stmt, "failed to eval parameters length"); return ret; @@ -2577,20 +2993,21 @@ static SQLRETURN statement_cbor_len(esodbc_stmt_st *stmt, size_t *outlen, return SQL_SUCCESS; } -static SQLRETURN statement_json_len(esodbc_stmt_st *stmt, size_t *outlen) +static SQLRETURN statement_len_json(esodbc_stmt_st *stmt, size_t *outlen) { SQLRETURN ret; - size_t bodylen, len; + size_t bodylen, len, curslen; esodbc_dbc_st *dbc = HDRH(stmt)->dbc; bodylen = 1; /* { */ + curslen = STMT_HAS_CURSOR(stmt); /* evaluate how long the stringified REST object will be */ - if (stmt->rset.pack.json.curs.cnt) { /* eval CURSOR object length */ + if (curslen) { /* eval CURSOR object length */ /* assumptions: (1) the cursor is a Base64 encoded string and thus * (2) no JSON escaping needed. - * (both assumptions checked on copy, below). */ + * (both assumptions checked on copy, in serialize_to_json()). */ bodylen += sizeof(JSON_KEY_CURSOR) - 1; /* "cursor": */ - bodylen += stmt->rset.pack.json.curs.cnt; + bodylen += curslen; bodylen += 2; /* 2x `"` for cursor value */ } else { /* eval QUERY object length */ bodylen += sizeof(JSON_KEY_QUERY) - 1; @@ -2600,9 +3017,9 @@ static SQLRETURN statement_json_len(esodbc_stmt_st *stmt, size_t *outlen) /* does the statement have any parameters? */ if (stmt->apd->count) { bodylen += sizeof(JSON_KEY_PARAMS) - 1; - /* serialize_params will count/copy array delimiters (`[`, `]`) */ - ret = serialize_params(stmt, /* no copy, just eval */NULL, &len, - /*as JSON*/TRUE); + /* serialize_params_json will count/copy array delims (`[`, `]`) */ + ret = serialize_params_json(stmt, /* no copy, just eval */NULL, + &len); if (! SQL_SUCCEEDED(ret)) { ERRH(stmt, "failed to eval parameters length"); return ret; @@ -2647,19 +3064,33 @@ static SQLRETURN serialize_to_cbor(esodbc_stmt_st *stmt, cstr_st *dest, { CborEncoder encoder, map; CborError err; - cstr_st tz; + cstr_st tz, curs; esodbc_dbc_st *dbc = HDRH(stmt)->dbc; + size_t dest_cnt; cbor_encoder_init(&encoder, dest->str, dest->cnt, /*flags*/0); err = cbor_encoder_create_map(&encoder, &map, keys); FAIL_ON_CBOR_ERR(stmt, err); - if (stmt->rset.pack.cbor.curs.cnt) { /* copy CURSOR object */ + if (STMT_HAS_CURSOR(stmt)) { /* copy CURSOR object */ err = cbor_encode_text_string(&map, REQ_KEY_CURSOR, sizeof(REQ_KEY_CURSOR) - 1); FAIL_ON_CBOR_ERR(stmt, err); - err = cbor_encode_text_string(&map, stmt->rset.pack.cbor.curs.str, - stmt->rset.pack.cbor.curs.cnt); + if (stmt->rset.pack_json) { + /* calculate the location where to convert the UTF16 cursor to its + * UTF8 (even ASCII in this case) representation */ + curs.cnt = stmt->rset.pack.json.curs.cnt; + curs.str = dest->str + (dest->cnt - curs.cnt); + if (ascii_w2c(stmt->rset.pack.json.curs.str, curs.str, + curs.cnt) <= 0) { + ERRH(stmt, "failed to convert cursor `" LWPDL "` to ASCII.", + LWSTR(&stmt->rset.pack.json.curs)); + RET_HDIAGS(stmt, SQL_STATE_24000); + } + } else { + curs = stmt->rset.pack.cbor.curs; + } + err = cbor_encode_text_string(&map, curs.str, curs.cnt); FAIL_ON_CBOR_ERR(stmt, err); } else { /* copy QUERY object */ err = cbor_encode_text_string(&map, REQ_KEY_QUERY, @@ -2670,14 +3101,15 @@ static SQLRETURN serialize_to_cbor(esodbc_stmt_st *stmt, cstr_st *dest, /* does the statement have any parameters? */ if (stmt->apd->count) { - FIXME; + FIXME; // TODO + return SQL_ERROR; } /* does the statement have any fetch_size? */ if (dbc->fetch.slen) { err = cbor_encode_text_string(&map, REQ_KEY_FETCH, sizeof(REQ_KEY_FETCH) - 1); FAIL_ON_CBOR_ERR(stmt, err); - err = cbor_encode_uint(&map, dbc->fetch.slen); + err = cbor_encode_uint(&map, dbc->fetch.max); FAIL_ON_CBOR_ERR(stmt, err); } /* "field_multi_value_leniency": true/false */ @@ -2722,7 +3154,9 @@ static SQLRETURN serialize_to_cbor(esodbc_stmt_st *stmt, cstr_st *dest, err = cbor_encoder_close_container(&encoder, &map); FAIL_ON_CBOR_ERR(stmt, err); - dest->cnt = cbor_encoder_get_buffer_size(&encoder, dest->str); + dest_cnt = cbor_encoder_get_buffer_size(&encoder, dest->str); + assert(dest_cnt <= dest->cnt); /* tinycbor should check this, but still */ + dest->cnt = dest_cnt; DBGH(stmt, "request serialized to CBOR: [%zd] `0x%s`.", dest->cnt, cstr_hex_dump(dest)); @@ -2750,20 +3184,25 @@ static SQLRETURN serialize_to_json(esodbc_stmt_st *stmt, cstr_st *dest) pos = 0; body[pos ++] = '{'; /* build the actual stringified JSON object */ - if (stmt->rset.pack.json.curs.cnt) { /* copy CURSOR object */ + if (STMT_HAS_CURSOR(stmt)) { /* copy CURSOR object */ memcpy(body + pos, JSON_KEY_CURSOR, sizeof(JSON_KEY_CURSOR) - 1); pos += sizeof(JSON_KEY_CURSOR) - 1; body[pos ++] = '"'; - if (ascii_w2c(stmt->rset.pack.json.curs.str, body + pos, - stmt->rset.pack.json.curs.cnt) <= 0) { - ERRH(stmt, "failed to convert cursor `" LWPDL "` to ASCII.", - LWSTR(&stmt->rset.pack.json.curs)); - RET_HDIAGS(stmt, SQL_STATE_24000); - } else { + if (stmt->rset.pack_json) { + if (ascii_w2c(stmt->rset.pack.json.curs.str, body + pos, + stmt->rset.pack.json.curs.cnt) <= 0) { + ERRH(stmt, "failed to convert cursor `" LWPDL "` to ASCII.", + LWSTR(&stmt->rset.pack.json.curs)); + RET_HDIAGS(stmt, SQL_STATE_24000); + } /* no character needs JSON escaping */ assert(stmt->rset.pack.json.curs.cnt == json_escape(body + pos, stmt->rset.pack.json.curs.cnt, NULL, 0)); pos += stmt->rset.pack.json.curs.cnt; + } else { + memcpy(body + pos, stmt->rset.pack.cbor.curs.str, + stmt->rset.pack.cbor.curs.cnt); + pos += stmt->rset.pack.cbor.curs.cnt; } body[pos ++] = '"'; } else { /* copy QUERY object */ @@ -2778,8 +3217,8 @@ static SQLRETURN serialize_to_json(esodbc_stmt_st *stmt, cstr_st *dest) if (stmt->apd->count) { memcpy(body + pos, JSON_KEY_PARAMS, sizeof(JSON_KEY_PARAMS) - 1); pos += sizeof(JSON_KEY_PARAMS) - 1; - /* serialize_params will count/copy array delimiters (`[`, `]`) */ - ret = serialize_params(stmt, body + pos, &len, /*as JSON*/TRUE); + /* serialize_params_json will count/copy array delims (`[`, `]`) */ + ret = serialize_params_json(stmt, body + pos, &len); if (! SQL_SUCCEEDED(ret)) { ERRH(stmt, "failed to serialize parameters"); return ret; @@ -2813,9 +3252,6 @@ static SQLRETURN serialize_to_json(esodbc_stmt_st *stmt, cstr_st *dest) sizeof(JSON_VAL_TIMEZONE_Z) - 1); pos += sizeof(JSON_VAL_TIMEZONE_Z) - 1; } - - /* reset the page counter when the params change */ - stmt->nset = 0; } /* "mode": */ memcpy(body + pos, JSON_KEY_VAL_MODE, sizeof(JSON_KEY_VAL_MODE) - 1); @@ -2824,9 +3260,12 @@ static SQLRETURN serialize_to_json(esodbc_stmt_st *stmt, cstr_st *dest) pos += sizeof(JSON_KEY_CLT_ID) - 1; body[pos ++] = '}'; + /* check that the buffer hasn't been overrun. it can be used less than + * initially calculated, since the calculation is an upper-bound one. */ + assert(pos <= dest->cnt); dest->cnt = pos; - INFOH(stmt, "request serialized to JSON: [%zd] `" LCPDL "`.", pos, + DBGH(stmt, "request serialized to JSON: [%zd] `" LCPDL "`.", pos, LCSTR(dest)); return SQL_SUCCESS; } @@ -2850,8 +3289,8 @@ SQLRETURN TEST_API serialize_statement(esodbc_stmt_st *stmt, cstr_st *dest) "Failed to update the timezone parameter", 0); } - ret = dbc->pack_json ? statement_json_len(stmt, &len) : - statement_cbor_len(stmt, &len, &keys); + ret = dbc->pack_json ? statement_len_json(stmt, &len) : + statement_len_cbor(stmt, &len, &keys); if (! SQL_SUCCEEDED(ret)) { return ret; } @@ -3330,23 +3769,38 @@ SQLRETURN EsSQLNumParams( SQLRETURN EsSQLRowCount(_In_ SQLHSTMT StatementHandle, _Out_ SQLLEN *RowCount) { esodbc_stmt_st *stmt = STMH(StatementHandle); + size_t nrows; + CborError res; if (! STMT_HAS_RESULTSET(stmt)) { ERRH(stmt, "no resultset available on statement."); RET_HDIAGS(stmt, SQL_STATE_HY010); } - DBGH(stmt, "current resultset rows count: %zd.", stmt->rset.nrows); - *RowCount = (SQLLEN)stmt->rset.nrows; - if (STMT_HAS_CURSOR(stmt)) { /* fetch_size or scroller size chunks the result */ - WARNH(stmt, "this function will only return the row count of the " - "partial result set available."); - /* returning a _WITH_INFO here will fail the query for MSQRY32.. */ - //RET_HDIAG(stmt, SQL_STATE_01000, "row count is for partial result " - // "only", 0); + ERRH(stmt, "can't evaluate the total size of paged result set."); + /* returning a _WITH_INFO here will fail the query for MSQRY32. */ +# if 0 + RET_HDIAG(stmt, SQL_STATE_01000, "row count is for partial result " + "only", 0); +# endif /* 0 */ + *RowCount = 0; + } else { + if (stmt->rset.pack_json) { + *RowCount = UJLengthArray(stmt->rset.pack.json.rows_obj); + } else { + res = cbor_get_array_count(stmt->rset.pack.cbor.rows_obj, &nrows); + if (res != CborNoError) { + ERRH(stmt, "failed to read row array count: %s.", + cbor_error_string(res)); + RET_HDIAGS(stmt, SQL_STATE_HY000); + } + *RowCount = (SQLLEN)nrows; + } + DBGH(stmt, "result set rows count: %zd.", *RowCount); } + return SQL_SUCCESS; } diff --git a/driver/tinycbor.c b/driver/tinycbor.c index bd211b0d..43b8e082 100644 --- a/driver/tinycbor.c +++ b/driver/tinycbor.c @@ -63,8 +63,25 @@ CborError cbor_map_advance_to_key(CborValue *it, const char *key, return CborNoError; } +/* similar to cbor_value_leave_container(), but the iterator may find itself + * anywhere within the container (and not necessarily at the end of it). */ +CborError cbor_value_exit_container(CborValue *cont, CborValue *it) +{ + CborError res; + assert(cbor_value_is_container(cont)); + while (! cbor_value_at_end(it)) { + if ((res = cbor_value_advance(it)) != CborNoError) { + return res; + } + } + return cbor_value_leave_container(cont, it); +} + +/* Looks up a number of 'cnt' objects mapped to the 'keys' of given + * 'len[gth]s'. If a key is not found, the corresponding objects are marked + * with an invalid type. */ CborError cbor_map_lookup_keys(CborValue *map, size_t cnt, - const char **keys, const size_t *lens, CborValue **objs, BOOL drain) + const char **keys, const size_t *lens, CborValue **objs) { CborError res; CborValue it; @@ -118,17 +135,7 @@ CborError cbor_map_lookup_keys(CborValue *map, size_t cnt, } } - if (drain) { - while (! cbor_value_at_end(&it)) { - if ((res = cbor_value_advance(&it)) != CborNoError) { - return res; - } - } - - return cbor_value_leave_container(map, &it); - } else { - return CborNoError; - } + return cbor_value_exit_container(map, &it); } CborError cbor_container_count(CborValue cont, size_t *count) @@ -224,7 +231,8 @@ void tinycbor_cleanup() } /* Fetches and converts a(n always UTF8) text string to UTF16 wide char. - * Uses a dynamically allocated thread-local buffer. */ + * Uses a dynamically allocated thread-local buffer. + * 0-terminates the string */ CborError cbor_value_get_utf16_wstr(CborValue *it, wstr_st *utf16) { static thread_local wstr_st wbuff = {.str = NULL, .cnt = (size_t)-1}; @@ -244,18 +252,26 @@ CborError cbor_value_get_utf16_wstr(CborValue *it, wstr_st *utf16) wbuff.cnt)) <= 0) { /* U8MB_TO_U16WC will return error (though not set it with * SetLastError()) for empty source strings */ - if (! mb_str.cnt) { - utf16->cnt = 0; - utf16->str = NULL; - return CborNoError; - } - /* is this a non-buffer related error? (like decoding) */ - if ((! WAPI_ERR_EBUFF()) && wbuff.str) { - return CborErrorInvalidUtf8TextString; - } /* else: buffer hasn't yet been allocated or is too small */ - /* what's the minimum space needed? */ - if ((n = U8MB_TO_U16WC(mb_str.str, mb_str.cnt, NULL, 0)) < 0) { - return CborErrorInvalidUtf8TextString; + if (mb_str.cnt) { + /* is this a non-buffer related error? (like decoding) */ + if ((! WAPI_ERR_EBUFF()) && wbuff.str) { + ERR("mb_str.str @ 0x%p, mb_str.cnt = %zu, wbuff.str @ 0x%p, " + "wbuff.cnt = %zu", mb_str.str, mb_str.cnt, wbuff.str, + wbuff.cnt); + ERR("WAPI_ERRNO=0x%x.", WAPI_ERRNO()); + ERR("MB: [%zu] `" LCPDL "`.", mb_str.cnt, LCSTR(&mb_str)); + return CborErrorInvalidUtf8TextString; + } /* else: buffer hasn't yet been allocated or is too small */ + /* what's the minimum space needed? */ + if ((n = U8MB_TO_U16WC(mb_str.str, mb_str.cnt, NULL, 0)) < 0) { + TRACE; + return CborErrorInvalidUtf8TextString; + } + } else { + n = 0; /* \0 */ + if (wbuff.str) { /* has it been already allocated? */ + break; + } } /* double scratchpad size until exceeding min needed space. * condition on equality, to allow for a 0-term */ diff --git a/driver/tinycbor.h b/driver/tinycbor.h index 9d61b5a9..d7210564 100644 --- a/driver/tinycbor.h +++ b/driver/tinycbor.h @@ -75,8 +75,14 @@ static inline size_t cbor_str_obj_len(size_t item_len) * given key, if that exists */ CborError cbor_map_advance_to_key(CborValue *it, const char *key, size_t key_len, CborValue *val); +/* similar to cbor_value_leave_container(), but the iterator may find itself + * anywhere within the container (and not necessarily at the end of it). */ +CborError cbor_value_exit_container(CborValue *cont, CborValue *it); +/* Looks up a number of 'cnt' objects mapped to the 'keys' of given + * 'len[gth]s'. If a key is not found, the corresponding objects are marked + * with an invalid type. */ CborError cbor_map_lookup_keys(CborValue *map, size_t cnt, - const char **keys, const size_t *lens, CborValue **objs, BOOL drain); + const char **keys, const size_t *lens, CborValue **objs); CborError cbor_container_count(CborValue cont, size_t *count); CborError cbor_get_array_count(CborValue arr, size_t *count); CborError cbor_container_is_empty(CborValue cont, BOOL *empty); diff --git a/driver/util.c b/driver/util.c index 5137594d..f0b3c408 100644 --- a/driver/util.c +++ b/driver/util.c @@ -529,13 +529,13 @@ SQLRETURN write_wstr(SQLHANDLE hnd, SQLWCHAR *dest, wstr_st *src, { size_t wide_avail; + DBGH(hnd, "copying %zd wchars (`" LWPDL "`) into buffer @0x%p, of %dB " + "len; out-len @0x%p.", src->cnt, LWSTR(src), dest, avail, usedp); + /* cnt must not count the 0-term (XXX: ever need to copy 0s?) */ assert(src->cnt <= 0 || src->str[src->cnt - 1]); assert(src->cnt <= 0 || src->str[src->cnt] == 0); - DBGH(hnd, "copying %zd wchars (`" LWPDL "`) into buffer @0x%p, of %dB " - "len; out-len @0x%p.", src->cnt, LWSTR(src), dest, avail, usedp); - if (usedp) { /* how many bytes are available to return (not how many would be * written into the buffer (which could be less));