driver/convert.c (4,024 lines of code) (raw):

/* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ #include <inttypes.h> #include <stdlib.h> #include <float.h> #include <math.h> #include <timestamp.h> #include "convert.h" #define JSON_VAL_NULL "null" #define JSON_VAL_TRUE "true" #define JSON_VAL_FALSE "false" #ifdef _WIN32 # ifndef _USE_32BIT_TIME_T # define MKTIME_YEAR_RANGE "1970-3000" # else /* !_USE_32BIT_TIME_T */ # define MKTIME_YEAR_RANGE "1970-2038" # endif /* !_USE_32BIT_TIME_T */ #else /* _WIN32 */ # error "platform not supported" #endif /* _WIN32 */ #define MKTIME_FAIL_MSG "Outside of the " MKTIME_YEAR_RANGE "year range?" static thread_local SQLCHAR current_date[] = "1970-01-01"; static thread_local struct tm today; /* For fixed size (destination) types, the target buffer can't be NULL. */ #define REJECT_IF_NULL_DEST_BUFF(_s/*tatement*/, _p/*ointer*/) \ do { \ if (! _p) { \ ERRH(_s, "destination buffer can't be NULL."); \ RET_HDIAGS(stmt, SQL_STATE_HY009); \ } \ } while (0) #define REJECT_AS_OOR(_stmt, _val, _fix_val, _target) /* Out Of Range */ \ do { \ if (_fix_val) { \ ERRH(_stmt, "can't convert value 0x%llx to %s: out of range.", \ (uint64_t)_val, STR(_target)); \ } else { \ ERRH(_stmt, "can't convert value %f to %s: out of range.", \ _val, STR(_target)); \ } \ RET_HDIAGS(_stmt, SQL_STATE_22003); \ } while (0) #define DBL_BASE10_MAX_LEN /*-0.*/3 + DBL_DIG - DBL_MIN_10_EXP /* maximum length of an interval literal (with terminator; both ISO and SQL), * with no field sanity checks: five longs with separators and sign */ #define INTERVAL_VAL_MAX_LEN (5 * sizeof("4294967295")) #if (0x0300 <= ODBCVER) # define ESSQL_TYPE_MIN SQL_GUID # define ESSQL_TYPE_MAX SQL_INTERVAL_MINUTE_TO_SECOND # define ESSQL_C_TYPE_MIN SQL_C_UTINYINT # define ESSQL_C_TYPE_MAX SQL_C_INTERVAL_MINUTE_TO_SECOND #else /* ODBCVER < 0x0300 */ /* would need to adjust the limits */ # error "ODBC version not supported; must be 3.0 (0x0300) or higher" #endif /* 0x0300 <= ODBCVER */ #define ESSQL_NORM_RANGE (ESSQL_TYPE_MAX - ESSQL_TYPE_MIN + 1) #define ESSQL_C_NORM_RANGE (ESSQL_C_TYPE_MAX - ESSQL_C_TYPE_MIN + 1) /* conversion matrix SQL indexer */ #define ESSQL_TYPE_IDX(_t) (_t - ESSQL_TYPE_MIN) /* conversion matrix C SQL indexer */ #define ESSQL_C_TYPE_IDX(_t) (_t - ESSQL_C_TYPE_MIN) /* sparse SQL-C_SQL types conversion matrix, used for quick compatiblity check * on columns and parameters binding */ static BOOL compat_matrix[ESSQL_NORM_RANGE][ESSQL_C_NORM_RANGE] = {FALSE}; /* Note: check is array-access unsafe: types IDs must be validated prior to * checking compatibility (ex. meta type setting) */ #define ESODBC_TYPES_COMPATIBLE(_sql, _csql) \ /* if not within the ODBC range, it can only by a binary conversion;.. */ \ ((ESSQL_TYPE_MAX < _sql && _csql == SQL_C_BINARY) || \ /* ..otheriwse use the conversion matrix */ \ compat_matrix[ESSQL_TYPE_IDX(_sql)][ESSQL_C_TYPE_IDX(_csql)]) /* populates the compat_matrix as required in: * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/converting-data-from-c-to-sql-data-types * "from" and "to" nameing attributes below correspond to items in the * vertical and horizontal lists, respectively. */ void convert_init() { SQLSMALLINT i, j, sql, csql, lim_i, lim_j; /*INDENT-OFF*/ SQLSMALLINT block_idx_sql[] = {SQL_CHAR, SQL_VARCHAR, SQL_LONGVARCHAR, SQL_WCHAR, SQL_WVARCHAR, SQL_WLONGVARCHAR, SQL_DECIMAL, SQL_NUMERIC, SQL_BIT, ESODBC_SQL_BOOLEAN, SQL_TINYINT, SQL_SMALLINT, SQL_INTEGER, SQL_BIGINT, SQL_REAL, SQL_FLOAT, SQL_DOUBLE }; /*INDENT-ON*/ SQLSMALLINT block_idx_csql[] = {SQL_C_CHAR, SQL_C_WCHAR, SQL_C_BIT, SQL_C_NUMERIC, SQL_C_STINYINT, SQL_C_UTINYINT, SQL_C_TINYINT, SQL_C_SBIGINT, SQL_C_UBIGINT, SQL_C_SSHORT, SQL_C_USHORT, SQL_C_SHORT, SQL_C_SLONG, SQL_C_ULONG, SQL_C_LONG, SQL_C_FLOAT, SQL_C_DOUBLE, SQL_C_BINARY }; /* SQL types convertible to all interval types (v-chars) */ SQLSMALLINT to_csql_interval_all[] = {SQL_CHAR, SQL_VARCHAR, SQL_LONGVARCHAR, SQL_WCHAR, SQL_WVARCHAR, SQL_WLONGVARCHAR }; /* SQL types convertible to single-component intervals only */ SQLSMALLINT to_csql_interval_single[] = {SQL_DECIMAL, SQL_NUMERIC, SQL_TINYINT, SQL_SMALLINT, SQL_INTEGER, SQL_BIGINT }; SQLSMALLINT from_sql_interval_all[] = {SQL_C_CHAR, SQL_C_WCHAR}; SQLSMALLINT from_sql_interval_single[] = {SQL_C_BIT, SQL_C_NUMERIC, SQL_C_STINYINT, SQL_C_UTINYINT, SQL_C_TINYINT, SQL_C_SBIGINT, SQL_C_UBIGINT, SQL_C_SSHORT, SQL_C_USHORT, SQL_C_SHORT, SQL_C_SLONG, SQL_C_ULONG, SQL_C_LONG }; SQLSMALLINT sql_interval[] = {SQL_INTERVAL_YEAR, SQL_INTERVAL_MONTH, SQL_INTERVAL_DAY, SQL_INTERVAL_HOUR, SQL_INTERVAL_MINUTE, SQL_INTERVAL_SECOND, SQL_INTERVAL_YEAR_TO_MONTH, SQL_INTERVAL_DAY_TO_HOUR, SQL_INTERVAL_DAY_TO_MINUTE, SQL_INTERVAL_DAY_TO_SECOND, SQL_INTERVAL_HOUR_TO_MINUTE, SQL_INTERVAL_HOUR_TO_SECOND, SQL_INTERVAL_MINUTE_TO_SECOND }; SQLSMALLINT sql_interval_single[] = {SQL_INTERVAL_YEAR, SQL_INTERVAL_MONTH, SQL_INTERVAL_DAY, SQL_INTERVAL_HOUR, SQL_INTERVAL_MINUTE, SQL_INTERVAL_SECOND }; SQLSMALLINT csql_interval[] = {SQL_C_INTERVAL_YEAR, SQL_C_INTERVAL_MONTH, SQL_C_INTERVAL_DAY, SQL_C_INTERVAL_HOUR, SQL_C_INTERVAL_MINUTE, SQL_C_INTERVAL_SECOND, SQL_C_INTERVAL_YEAR_TO_MONTH, SQL_C_INTERVAL_DAY_TO_HOUR, SQL_C_INTERVAL_DAY_TO_MINUTE, SQL_C_INTERVAL_DAY_TO_SECOND, SQL_C_INTERVAL_HOUR_TO_MINUTE, SQL_C_INTERVAL_HOUR_TO_SECOND, SQL_C_INTERVAL_MINUTE_TO_SECOND }; SQLSMALLINT csql_interval_single[] = {SQL_C_INTERVAL_YEAR, SQL_C_INTERVAL_MONTH, SQL_C_INTERVAL_DAY, SQL_C_INTERVAL_HOUR, SQL_C_INTERVAL_MINUTE, SQL_C_INTERVAL_SECOND }; SQLSMALLINT to_csql_datetime[] = {SQL_CHAR, SQL_VARCHAR, SQL_LONGVARCHAR, SQL_WCHAR, SQL_WVARCHAR, SQL_WLONGVARCHAR, SQL_TYPE_DATE, SQL_TYPE_TIME, SQL_TYPE_TIMESTAMP }; SQLSMALLINT csql_datetime[] = {SQL_C_TYPE_DATE, SQL_C_TYPE_TIME, SQL_C_TYPE_TIMESTAMP }; /* fill the compact block of TRUEs (growing from the upper left corner) */ for (i = 0; i < sizeof(block_idx_sql)/sizeof(*block_idx_sql); i ++) { for (j = 0; j < sizeof(block_idx_csql)/sizeof(*block_idx_csql); j ++) { sql = block_idx_sql[i]; csql = block_idx_csql[j]; compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; } } /* SQL_C_ BINARY, CHAR, WCHAR and DEFAULT are comatible with all SQL types; * this will set also non-ODBC intersections (but it's convenient) */ for (sql = 0; sql < ESSQL_NORM_RANGE; sql ++) { compat_matrix[sql][ESSQL_C_TYPE_IDX(SQL_C_CHAR)] = TRUE; compat_matrix[sql][ESSQL_C_TYPE_IDX(SQL_C_WCHAR)] = TRUE; compat_matrix[sql][ESSQL_C_TYPE_IDX(SQL_C_BINARY)] = TRUE; compat_matrix[sql][ESSQL_C_TYPE_IDX(SQL_C_DEFAULT)] = TRUE; } /* ESODBC_SQL_NULL (NULL) is compabitle with all SQL_C types */ for (csql = 0; csql < ESSQL_C_NORM_RANGE; csql ++) { compat_matrix[ESSQL_TYPE_IDX(ESODBC_SQL_NULL)][csql] = TRUE; } /* set conversions to all INTERVAL_C */ lim_i = sizeof(to_csql_interval_all)/sizeof(*to_csql_interval_all); lim_j = sizeof(csql_interval)/sizeof(*csql_interval); for (i = 0; i < lim_i; i ++) { for (j = 0; j < lim_j; j ++ ) { sql = to_csql_interval_all[i]; csql = csql_interval[j]; compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; } } /* set conversions to single-component INTERVAL_C */ lim_i = sizeof(to_csql_interval_single)/sizeof(*to_csql_interval_single); lim_j = sizeof(csql_interval_single)/sizeof(*csql_interval_single); for (i = 0; i < lim_i; i ++) { for (j = 0; j < lim_j; j ++ ) { sql = to_csql_interval_single[i]; csql = csql_interval_single[j]; compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; } } /* set conversions from all INTERVAL_SQL */ lim_i = sizeof(sql_interval)/sizeof(*sql_interval); lim_j = sizeof(from_sql_interval_all)/sizeof(*from_sql_interval_all); for (i = 0; i < lim_i; i ++) { for (j = 0; j < lim_j; j ++ ) { sql = sql_interval[i]; csql = from_sql_interval_all[j]; compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; } } /* set conversions from single_component INTERVAL_SQL */ lim_i = sizeof(sql_interval_single)/sizeof(*sql_interval_single); lim_j = sizeof(from_sql_interval_single)/sizeof(*from_sql_interval_single); for (i = 0; i < lim_i; i ++) { for (j = 0; j < lim_j; j ++ ) { sql = sql_interval_single[i]; csql = from_sql_interval_single[j]; compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; } } /* set default conversions for all ITERVALs */ lim_i = sizeof(sql_interval)/sizeof(*sql_interval); lim_j = sizeof(csql_interval)/sizeof(*csql_interval); assert(lim_i == lim_j); for (i = 0; i < lim_i; i ++) { sql = sql_interval[i]; csql = csql_interval[i]; compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; } /* set conversions between date-time types */ for (i = 0; i < sizeof(to_csql_datetime)/sizeof(*to_csql_datetime); i ++) { for (j = 0; j < sizeof(csql_datetime)/sizeof(*csql_datetime); j ++ ) { sql = to_csql_datetime[i]; csql = csql_datetime[j]; if (sql == SQL_TYPE_DATE && csql == SQL_C_TYPE_TIME) { continue; } if (sql == SQL_TYPE_TIME && csql == SQL_C_TYPE_DATE) { continue; } compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; } } /* GUID conversion */ sql = SQL_GUID; csql = SQL_C_GUID; compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; } SQLRETURN set_param_decdigits(esodbc_rec_st *irec, SQLUSMALLINT param_no, SQLSMALLINT decdigits) { assert(irec->desc->type == DESC_TYPE_IPD); switch (irec->meta_type) { /* for "SQL_TYPE_TIME, SQL_TYPE_TIMESTAMP, SQL_INTERVAL_SECOND, * SQL_INTERVAL_DAY_TO_SECOND, SQL_INTERVAL_HOUR_TO_SECOND, or * SQL_INTERVAL_MINUTE_TO_SECOND, the SQL_DESC_PRECISION field of the * IPD is set to DecimalDigits." */ case METATYPE_DATE_TIME: if (irec->concise_type == SQL_TYPE_DATE) { break; } case METATYPE_INTERVAL_WSEC: if (decdigits < 0) { ERRH(irec->desc, "can't set negative (%hd) as fractional " "second precision.", decdigits); RET_HDIAGS(irec->desc, SQL_STATE_HY104); } return EsSQLSetDescFieldW(irec->desc, param_no, SQL_DESC_PRECISION, (SQLPOINTER)(intptr_t)decdigits, SQL_IS_SMALLINT); /* for " SQL_NUMERIC or SQL_DECIMAL, the SQL_DESC_SCALE field of the * IPD is set to DecimalDigits." */ case METATYPE_EXACT_NUMERIC: if (irec->concise_type == SQL_DECIMAL || irec->concise_type == SQL_NUMERIC) { return EsSQLSetDescFieldW(irec->desc, param_no, SQL_DESC_SCALE, (SQLPOINTER)(intptr_t)decdigits, SQL_IS_SMALLINT); } break; // formal default: /* "For all other data types, the DecimalDigits argument is * ignored." */ ; } return SQL_SUCCESS; } SQLSMALLINT get_param_decdigits(esodbc_rec_st *irec) { assert(irec->desc->type == DESC_TYPE_IPD); switch(irec->meta_type) { case METATYPE_DATE_TIME: if (irec->concise_type == SQL_TYPE_DATE) { break; } case METATYPE_INTERVAL_WSEC: return irec->precision; case METATYPE_EXACT_NUMERIC: if (irec->concise_type == SQL_DECIMAL || irec->concise_type == SQL_NUMERIC) { return irec->scale; } break; default: WARNH(irec->desc, "retriving decdigits for IPD metatype: %d.", irec->meta_type); } return 0; } SQLRETURN set_param_size(esodbc_rec_st *irec, SQLUSMALLINT param_no, SQLULEN size) { assert(irec->desc->type == DESC_TYPE_IPD); switch (irec->meta_type) { /* for "SQL_CHAR, SQL_VARCHAR, SQL_LONGVARCHAR, SQL_BINARY, * SQL_VARBINARY, SQL_LONGVARBINARY, or one of the concise SQL * datetime or interval data types, the SQL_DESC_LENGTH field of the * IPD is set to the value of [s]ize." */ case METATYPE_STRING: case METATYPE_BIN: case METATYPE_DATE_TIME: case METATYPE_INTERVAL_WSEC: case METATYPE_INTERVAL_WOSEC: return EsSQLSetDescFieldW(irec->desc, param_no, SQL_DESC_LENGTH, (SQLPOINTER)(uintptr_t)size, SQL_IS_UINTEGER); /* for "SQL_DECIMAL, SQL_NUMERIC, SQL_FLOAT, SQL_REAL, or SQL_DOUBLE, * the SQL_DESC_PRECISION field of the IPD is set to the value of * [s]ize." */ case METATYPE_EXACT_NUMERIC: if (irec->concise_type != SQL_DECIMAL && irec->concise_type != SQL_NUMERIC) { break; } /* no break */ case METATYPE_FLOAT_NUMERIC: /* https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/column-size : * "The ColumnSize argument of SQLBindParameter is ignored for * this data type." return EsSQLSetDescFieldW(irec->desc, param_no, SQL_DESC_PRECISION, (SQLPOINTER)(uintptr_t)size, SQL_IS_SMALLINT); */ default: ;/* "For other data types, the [s]ize argument is ignored." */ } return SQL_SUCCESS; } SQLULEN get_param_size(esodbc_rec_st *irec) { assert(irec->desc->type == DESC_TYPE_IPD); switch (irec->meta_type) { case METATYPE_STRING: case METATYPE_BIN: case METATYPE_DATE_TIME: case METATYPE_INTERVAL_WSEC: case METATYPE_INTERVAL_WOSEC: return irec->length; case METATYPE_BIT: case METATYPE_EXACT_NUMERIC: // TODO: make DEC, NUM a floating meta? if (irec->concise_type != SQL_DECIMAL && irec->concise_type != SQL_NUMERIC) { assert(irec->es_type); return irec->es_type->column_size; } case METATYPE_FLOAT_NUMERIC: return irec->precision; default: WARNH(irec->desc, "retriving colsize for IPD metatype: %d.", irec->meta_type); } return 0; } /* * field: SQL_DESC_: DATA_PTR / INDICATOR_PTR / OCTET_LENGTH_PTR * pos: position in array/row_set (not result_set) */ inline void *deferred_address(SQLSMALLINT field_id, size_t pos, esodbc_rec_st *rec) { size_t elem_size; SQLLEN offt; void *base; esodbc_desc_st *desc = rec->desc; #define ROW_OFFSETS \ do { \ elem_size = desc->bind_type; \ offt = desc->bind_offset_ptr ? *(desc->bind_offset_ptr) : 0; \ } while (0) switch (field_id) { case SQL_DESC_DATA_PTR: base = rec->data_ptr; if (desc->bind_type == SQL_BIND_BY_COLUMN) { elem_size = (size_t)rec->octet_length; offt = 0; } else { /* by row */ ROW_OFFSETS; } break; case SQL_DESC_INDICATOR_PTR: base = rec->indicator_ptr; if (desc->bind_type == SQL_BIND_BY_COLUMN) { elem_size = sizeof(*rec->indicator_ptr); offt = 0; } else { /* by row */ ROW_OFFSETS; } break; case SQL_DESC_OCTET_LENGTH_PTR: base = rec->octet_length_ptr; if (desc->bind_type == SQL_BIND_BY_COLUMN) { elem_size = sizeof(*rec->octet_length_ptr); offt = 0; } else { /* by row */ ROW_OFFSETS; } break; default: BUG("can't calculate the deferred address of field type %d.", field_id); return NULL; } #undef ROW_OFFSETS DBGH(desc->hdr.stmt, "rec@0x%p, field_id:%hd, pos: %zu : base@0x%p, " "offset=%lld, elem_size=%zu", rec, field_id, pos, base, (int64_t)offt, elem_size); return base ? (char *)base + offt + pos * elem_size : NULL; } /* * Handles the lengths of the data to copy out to the application: * (1) returns the max amount of bytes to copy (in the data_ptr), taking into * account size of data and of buffer, relevant statement attribute and * buffer type; * (2) indicates if truncation occured into 'state'. * WARN: only to be used with ARD.meta_type == STR || BIN (as it can indicate * a size to copy smaller than the original -- truncating). */ static size_t buff_octet_size( size_t avail, /* how many bytes are there to copy out */ size_t unit_size, /* the unit size of the buffer (i.e. sizeof(wchar_t)) */ esodbc_rec_st *arec, esodbc_rec_st *irec, esodbc_state_et *state /* out param: only written when truncating */) { esodbc_stmt_st *stmt = arec->desc->hdr.stmt; /* how large (bytes) is the buffer to copy into*/ size_t room = (size_t)arec->octet_length; /* statement attribute SQL_ATTR_MAX_LENGTH value */ size_t attr_max = stmt->max_length; /* meta type of IRD */ esodbc_metatype_et ird_mt = irec->meta_type; size_t max_copy, max; /* type is signed, driver should not allow a negative to this point: * making sure the cast above is sane. */ assert(0 <= arec->octet_length); /* truncate to statment max bytes, only if "the column contains character * or binary data" */ max = (ird_mt == METATYPE_STRING || ird_mt == METATYPE_BIN) ? attr_max : 0; /* apply "network" truncation first, if need to */ if (0 < max && max < avail) { INFOH(stmt, "applying 'network' truncation %zd -> %zd.", avail, max); max_copy = max; /* no truncation indicated for this case */ } else { max_copy = avail; } /* is target buffer to small? adjust size if so and indicate truncation */ /* Note: this should only be tested/applied if ARD.meta_type == STR||BIN */ // FIXME: check note above if (room < max_copy) { INFOH(stmt, "applying buffer truncation %zd -> %zd.", max_copy, room); max_copy = room; *state = SQL_STATE_01004; } /* adjust to align to target buffer unit */ if (max_copy % unit_size) { max_copy -= max_copy % unit_size; } DBGH(stmt, "avail=%zd, room=%zd, attr_max=%zd, metatype:%d => " "max_copy=%zd, state=%d.", avail, room, attr_max, ird_mt, max_copy, *state); return max_copy; } /* * Indicate the amount of data available to the application, taking into * account: the type of data, should truncation - due to max length attr * setting - need to be indicated, since original length is indicated, w/o * possible buffer truncation, but with possible 'network' truncation. */ static inline void write_out_octets( SQLLEN *octet_len_ptr, /* buffer to write the avail octets into */ size_t avail, /* amount of bytes avail */ esodbc_rec_st *irec) { esodbc_stmt_st *stmt = irec->desc->hdr.stmt; /* statement attribute SQL_ATTR_MAX_LENGTH value */ size_t attr_max = stmt->max_length; /* meta type of IRD */ esodbc_metatype_et ird_mt = irec->meta_type; size_t max; if (! octet_len_ptr) { DBGH(stmt, "NULL octet len pointer, length (%zu) not indicated.", avail); return; } /* truncate to statment max bytes, only if "the column contains character * or binary data" */ max = (ird_mt == METATYPE_STRING || ird_mt == METATYPE_BIN) ? attr_max : 0; if (0 < max) { /* put the value of SQL_ATTR_MAX_LENGTH attribute.. even * if this would be larger than what the data actually * occupies after conversion: "the driver has no way of * figuring out what the actual length is" */ *octet_len_ptr = max; DBGH(stmt, "max length (%zd) attribute enforced.", max); } else { /* if no "network" truncation done, indicate data's length, no * matter if truncated to buffer's size or not */ *octet_len_ptr = avail; } DBGH(stmt, "length of data available for transfer: %ld", *octet_len_ptr); } /* * If an application doesn't specify the conversion, use column's type. * * Note: in case the ARD binds a SQL_C_DEFAULT, the driver won't check the * size of the bound buffer for fixed types: "SQLFetch never truncates data * converted to fixed-length data types; it always assumes that the length of * the data buffer is the size of the data type." * TODO: accommodate apps that do? */ static inline SQLSMALLINT get_rec_c_type(esodbc_rec_st *arec, esodbc_rec_st *irec) { SQLSMALLINT ctype; /* "To use the default mapping, an application specifies the SQL_C_DEFAULT * type identifier." */ if (arec->concise_type != SQL_C_DEFAULT) { ctype = arec->concise_type; } else { ctype = irec->es_type->c_concise_type; } DBGH(arec->desc, "AxD data C type: %hd.", ctype); return ctype; } static inline void gd_offset_apply(esodbc_stmt_st *stmt, xstr_st *xstr) { if (! STMT_GD_CALLING(stmt)) { return; } assert(0 <= stmt->gd_offt); /* negative means "data exhausted" */ assert(stmt->gd_offt < (SQLLEN)xstr->w.cnt + /*\0*/1); if (xstr->wide) { xstr->w.str += stmt->gd_offt; xstr->w.cnt -= stmt->gd_offt; } else { xstr->c.str += stmt->gd_offt; xstr->c.cnt -= stmt->gd_offt; } DBGH(stmt, "applied an offset of %lld.", (int64_t)stmt->gd_offt); } /* * cnt: character count of string/bin to transfer, excluding \0 * xfed: char count of copied data. */ static inline void gd_offset_update(esodbc_stmt_st *stmt, size_t cnt, size_t xfed) { if (! STMT_GD_CALLING(stmt)) { return; } if (cnt <= xfed) { /* if all has been transfered, indicate so in the gd_offt */ stmt->gd_offt = -1; } else { stmt->gd_offt += xfed; } DBGH(stmt, "offset updated with %zu to new value of %lld.", xfed, (int64_t)stmt->gd_offt); } /* transfer to the application a 0-terminated (but unaccounted for) xstr_st */ static SQLRETURN transfer_xstr0(esodbc_rec_st *arec, esodbc_rec_st *irec, xstr_st *xsrc, void *data_ptr, SQLLEN *octet_len_ptr) { size_t in_bytes, in_chars; SQLCHAR *dst_c; SQLWCHAR *dst_w; size_t cnt, char_sz; esodbc_state_et state = SQL_STATE_00000; esodbc_stmt_st *stmt = arec->desc->hdr.stmt; /* apply source offset, if this is a SQLGetData() call */ gd_offset_apply(stmt, xsrc); cnt = xsrc->w.cnt; /*==->c.cnt*/ if (xsrc->wide) { /* the source string must be 0-term'd (buff_octet_size param below * counts it) */ assert(xsrc->w.str[cnt] == 0); char_sz = sizeof(*xsrc->w.str); } else { assert(xsrc->c.str[cnt] == 0); char_sz = sizeof(*xsrc->c.str); } /* always return the app the untruncated number of bytes */ write_out_octets(octet_len_ptr, cnt * char_sz, irec); if (data_ptr) { in_bytes = buff_octet_size((cnt + /*\0*/1) * char_sz, char_sz, arec, irec, &state); if (in_bytes) { in_chars = in_bytes / char_sz; /* deduct the \0 added above; which is needed, since we need to * copy it too out to the app (or truncate the data, but still not * count the \0) */ in_chars --; if (xsrc->wide) { dst_w = (SQLWCHAR *)data_ptr; memcpy(dst_w, xsrc->w.str, in_bytes); /* TODO: should the left be filled with spaces? : * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/rules-for-conversions */ if (state != SQL_STATE_00000) { /* 0-term the buffer */ dst_w[in_chars] = L'\0'; DBGH(stmt, "aREC@0x%p: `" LWPDL "` transfered truncated " "as `" LWPDL "` at data_ptr@0x%p.", arec, LWSTR(&xsrc->w), in_chars, dst_w, dst_w); } else { assert(dst_w[in_chars] == L'\0'); DBGH(stmt, "aREC@0x%p: `" LWPDL "` transfered at " "data_ptr@0x%p.", arec, LWSTR(&xsrc->w), dst_w); } } else { dst_c = (SQLCHAR *)data_ptr; memcpy(dst_c, xsrc->c.str, in_bytes); if (state != SQL_STATE_00000) { /* 0-term the buffer */ dst_c[in_chars] = '\0'; DBGH(stmt, "aREC@0x%p: `" LCPDL "` transfered truncated " "as `" LCPDL "` at data_ptr@0x%p.", arec, LCSTR(&xsrc->w), in_chars, dst_c, dst_c); } else { assert(dst_c[in_chars] == '\0'); DBGH(stmt, "aREC@0x%p: `" LCPDL "` transfered at " "data_ptr@0x%p.", arec, LCSTR(&xsrc->c), dst_c); } } /* only update offset if data is copied out */ gd_offset_update(stmt, xsrc->w.cnt, in_chars); /*==->c.cnt*/ } else { DBGH(stmt, "aREC@0x%p, data_ptr@0x%p, no room to copy bytes out.", arec, data_ptr); } } else { DBGH(stmt, "aREC@0x%p: NULL transfer buffer.", arec); } if (state != SQL_STATE_00000) { RET_HDIAGS(stmt, state); } return SQL_SUCCESS; } /* 10^n */ static inline unsigned long long pow10(unsigned n) { unsigned long long pow = 1; pow <<= n; while (n--) { pow += pow << 2; } return pow; } static SQLRETURN double_to_numeric(esodbc_rec_st *arec, double src, void *dst) { SQL_NUMERIC_STRUCT *numeric; esodbc_stmt_st *stmt; SQLSMALLINT prec/*..ision*/; unsigned long long ullng, pow; long long llng; stmt = arec->desc->hdr.stmt; numeric = (SQL_NUMERIC_STRUCT *)dst; assert(numeric); numeric->scale = (SQLCHAR)arec->scale; numeric->sign = 0 <= src; ullng = numeric->sign ? (unsigned long long)src : (unsigned long long)-src; /* =~ log10(abs(src)) */ for (prec = 0; ullng; prec ++) { ullng /= 10; } if (arec->scale < 0) { pow = pow10(-arec->scale); llng = (long long)(src / pow); prec += arec->scale; /* prec lowers */ } else if (0 < arec->scale) { pow = pow10(arec->scale); if (DBL_MAX / pow < src) { // TODO: numeric.val is 16 octets long -> expand ERRH(stmt, "max numeric conversion scale reached."); RET_HDIAGS(stmt, SQL_STATE_22003); } llng = (long long)(src * pow); prec += arec->scale; /* prec grows */ } else { llng = (long long)src; } ullng = numeric->sign ? (unsigned long long)llng : (unsigned long long)-llng; DBGH(stmt, "arec@0x%p: precision=%hd, scale=%hd; src.precision=%hd", arec, arec->precision, arec->scale, prec); if ((UCHAR_MAX < prec) || (0 < arec->precision && arec->precision < prec)) { /* precision of source is higher than requested => overflow */ ERRH(stmt, "conversion overflow. source: %.6e; requested: " "precisions: %d, scale: %d.", src, arec->precision, arec->scale); RET_HDIAGS(stmt, SQL_STATE_22003); } else if (prec < 0) { prec = 0; assert(ullng == 0); } numeric->precision = (SQLCHAR)prec; #if REG_DWORD != REG_DWORD_LITTLE_ENDIAN ullng = _byteswap_ulong(ullng); #endif /* LE */ assert(sizeof(ullng) <= sizeof(numeric->val)); memcpy(numeric->val, (char *)&ullng, sizeof(ullng)); memset(numeric->val+sizeof(ullng), 0, sizeof(numeric->val)-sizeof(ullng)); DBGH(stmt, "double %.6e converted to numeric: .sign=%d, precision=%d " "(req: %d), .scale=%d (req: %d), .val:`" LCPDL "` (0x%lx).", src, numeric->sign, numeric->precision, arec->precision, numeric->scale, arec->scale, (int)sizeof(numeric->val), numeric->val, ullng); return SQL_SUCCESS; } static SQLRETURN numeric_to_double(esodbc_rec_st *irec, void *src, double *dst) { unsigned long long ullng, pow; double dbl; SQLSMALLINT prec/*..ision*/; SQL_NUMERIC_STRUCT *numeric; esodbc_stmt_st *stmt = irec->desc->hdr.stmt; assert(src); numeric = (SQL_NUMERIC_STRUCT *)src; assert(2 * sizeof(ullng) == sizeof(numeric->val)); ullng = *(unsigned long long *)&numeric->val[sizeof(ullng)]; if (ullng) { // TODO: shift down with scale ERRH(stmt, "max numeric precision scale reached."); goto erange; } ullng = *(unsigned long long *)&numeric->val[0]; #if REG_DWORD != REG_DWORD_LITTLE_ENDIAN ullng = _byteswap_ulong(ullng); #endif /* LE */ /* =~ log10(abs(ullng)) */ for (prec = 0, pow = ullng; pow; prec ++) { pow /= 10; } if (DBL_MAX < ullng) { goto erange; } else { dbl = (double)ullng; } if (numeric->scale < 0) { pow = pow10(-numeric->scale); if (DBL_MAX / pow < dbl) { goto erange; } dbl *= pow; prec -= numeric->scale; /* prec grows */ } else if (0 < numeric->scale) { pow = pow10(numeric->scale); dbl /= pow; prec -= numeric->scale; /* prec lowers */ } DBGH(stmt, "irec@0x%p: precision=%hd, scale=%hd; src.precision=%hd", irec, irec->precision, irec->scale, prec); if ((UCHAR_MAX < prec) || (0 < irec->precision && irec->precision < prec)) { ERRH(stmt, "source precision (%hd) larger than requested (%hd)", prec, irec->precision); goto erange; } else { if (! numeric->sign) { dbl = -dbl; } } DBGH(stmt, "VAL: %f", dbl); DBGH(stmt, "numeric val: %llu, scale: %hhd, precision: %hhu converted to " "double %.6e.", ullng, numeric->scale, numeric->precision, dbl); *dst = dbl; return SQL_SUCCESS; erange: ERRH(stmt, "can't convert numeric val: %llu, scale: %hhd, precision: %hhu" " to double.", ullng, numeric->scale, numeric->precision); RET_HDIAGS(stmt, SQL_STATE_22003); } /* * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/transferring-data-in-its-binary-form */ static SQLRETURN llong_to_binary(esodbc_rec_st *arec, esodbc_rec_st *irec, long long src, void *dst, SQLLEN *src_len) { size_t cnt; char *s = (char *)&src; esodbc_state_et state = SQL_STATE_00000; esodbc_stmt_st *stmt = arec->desc->hdr.stmt; unsigned long long ull = src < 0 ? -src : src; /* UJ4C uses long long for any integer type -> find out the * smallest type that would accomodate the value (since fixed negatives * would take more space then minimally required). */ if (ull < CHAR_MAX) { cnt = sizeof(char); } else if (ull < SHRT_MAX) { cnt = sizeof(short); } else if (ull < INT_MAX) { cnt = sizeof(int); } else if (ull < LONG_MAX) { cnt = sizeof(long); } else { /* definetely ull < LLONG_MAX */ cnt = sizeof(long long); } cnt = buff_octet_size(cnt, sizeof(*s), arec, irec, &state); if (state) { /* has it been shrunk? */ REJECT_AS_OOR(stmt, src, /*fixed?*/TRUE, "[BINARY]<[value]"); } if (dst) { /* copy bytes as-are: the reverse conversion need to take place on * "same DBMS and hardare platform". */ memcpy(dst, s, cnt); //TODO: should the driver clear all the received buffer?? Cfg option? //memset((char *)dst + cnt, 0, arec->octet_length - cnt); } write_out_octets(src_len, cnt, irec); DBGH(stmt, "long long value %lld, converted on %zd octets.", src, cnt); return SQL_SUCCESS; } static SQLRETURN quadword_to_str(esodbc_rec_st *arec, esodbc_rec_st *irec, uint64_t qword, bool unsignd, void *data_ptr, SQLLEN *octet_len_ptr, BOOL wide) { SQLRETURN ret; /* buffer is overprovisioned for !wide, but avoids double declaration */ SQLCHAR buff[(ESODBC_PRECISION_UINT64 + /*0-term*/1 + /*+/-*/1) * sizeof(SQLWCHAR)]; esodbc_stmt_st *stmt = arec->desc->hdr.stmt; xstr_st xsrc = (xstr_st) { .wide = wide, .c = (cstr_st) { buff, 0 } }; xsrc.w.cnt = unsignd ? ui64tot(qword, buff, wide) : i64tot((int64_t)qword, buff, wide); /* ==.c.cnt */ if (wide) { DBGH(stmt, "quad word 0x%llx convertible to w-string `" LWPD "` on " "%zd octets.", qword, xsrc.w.str, xsrc.w.cnt); } else { DBGH(stmt, "quad word 0x%llx convertible to string `" LCPD "` on " "%zd octets.", qword, xsrc.c.str, xsrc.c.cnt); } ret = transfer_xstr0(arec, irec, &xsrc, data_ptr, octet_len_ptr); /* need to change the error code from truncation to "out of * range", since "whole digits" are truncated */ if (ret == SQL_SUCCESS_WITH_INFO && HDRH(stmt)->diag.state == SQL_STATE_01004) { if (STMT_GD_CALLING(stmt)) { return ret; } else { REJECT_AS_OOR(stmt, qword, /*fixed?*/TRUE, "[STRING]<[value]"); } } return SQL_SUCCESS; } SQLRETURN sql2c_quadword(esodbc_rec_st *arec, esodbc_rec_st *irec, SQLULEN pos, uint64_t qword, bool unsignd) { esodbc_stmt_st *stmt; void *data_ptr; SQLLEN *octet_len_ptr; esodbc_desc_st *ard, *ird; long long ll; unsigned long long ull; SQLSMALLINT ctype; SQLRETURN ret; SQL_INTERVAL_STRUCT ivl; SQLINTERVAL ivl_type; SQLUINTEGER *ivl_comp; long long ivl_val; stmt = arec->desc->hdr.stmt; ird = stmt->ird; ard = stmt->ard; ull = (unsigned long long)qword; ll = (long long)qword; /* pointer where to write how many characters we will/would use */ octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); /* pointer to app's buffer */ data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); /* Assume a C type behind an SQL C type, but check size representation. * Uses local vars: stmt. */ # define REJECT_IF_OOR(_qword, _min, _max, _sqlctype, _ctype) \ do { \ assert(sizeof(_sqlctype) == sizeof(_ctype)); \ if (_qword < _min || _max < _qword) { \ REJECT_AS_OOR(stmt, _qword, /*fixed int*/TRUE, _ctype); \ } \ } while (0) /* Transfer a quadword to an SQL integer type. * Uses local vars: ll, ull, stmt, data_ptr, irec, octet_len_ptr. */ # define TRANSFER_QW(_min, _max, _sqlctype, _ctype) \ do { \ REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); \ if (unsignd) { \ REJECT_IF_OOR(ull, 0, _max, _sqlctype, _ctype); \ *(_sqlctype *)data_ptr = (_sqlctype)ull; \ DBGH(stmt, "converted unsigned quad word %llu to " STR(_sqlctype) \ " 0x%llx.", ull, (uint64_t)*(_sqlctype *)data_ptr); \ } else { \ REJECT_IF_OOR(ll, _min, _max, _sqlctype, _ctype); \ *(_sqlctype *)data_ptr = (_sqlctype)ll; \ DBGH(stmt, "converted signed quad word %lld to " STR(_sqlctype) \ " 0x%llx.", ll, (uint64_t)*(_sqlctype *)data_ptr); \ } \ write_out_octets(octet_len_ptr, sizeof(_sqlctype), irec); \ } while (0) switch ((ctype = get_rec_c_type(arec, irec))) { case SQL_C_CHAR: case SQL_C_WCHAR: return quadword_to_str(arec, irec, qword, unsignd, data_ptr, octet_len_ptr, ctype == SQL_C_WCHAR); case SQL_C_TINYINT: case SQL_C_STINYINT: TRANSFER_QW(CHAR_MIN, CHAR_MAX, SQLSCHAR, char); break; case SQL_C_UTINYINT: TRANSFER_QW(0, UCHAR_MAX, SQLCHAR, unsigned char); break; case SQL_C_SHORT: case SQL_C_SSHORT: TRANSFER_QW(SHRT_MIN, SHRT_MAX, SQLSMALLINT, short); break; case SQL_C_USHORT: TRANSFER_QW(0, USHRT_MAX, SQLUSMALLINT, unsigned short); break; case SQL_C_LONG: case SQL_C_SLONG: TRANSFER_QW(LONG_MIN, LONG_MAX, SQLINTEGER, long); break; case SQL_C_ULONG: TRANSFER_QW(0, ULONG_MAX, SQLUINTEGER, unsigned long); break; case SQL_C_SBIGINT: TRANSFER_QW(LLONG_MIN, LLONG_MAX, SQLBIGINT, long long); break; case SQL_C_UBIGINT: TRANSFER_QW(0, ULLONG_MAX, SQLUBIGINT, unsigned long long); break; case SQL_C_BIT: REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); if (ll != 0 && ll != 1) { REJECT_AS_OOR(stmt, ll, /*fixed int*/TRUE, SQL_C_BIT); } else { *(SQLCHAR *)data_ptr = (SQLCHAR)ll; } write_out_octets(octet_len_ptr, sizeof(SQLSCHAR), irec); break; case SQL_C_NUMERIC: REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); ret = double_to_numeric(arec, unsignd ? (double)ull : (double)ll, data_ptr); if (! SQL_SUCCEEDED(ret)) { return ret; } write_out_octets(octet_len_ptr, sizeof(SQL_NUMERIC_STRUCT), irec); break; case SQL_C_FLOAT: REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); *(SQLREAL *)data_ptr = unsignd ? (SQLREAL)ull : (SQLREAL)ll; write_out_octets(octet_len_ptr, sizeof(SQLREAL), irec); break; case SQL_C_DOUBLE: REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); *(SQLDOUBLE *)data_ptr = unsignd ? (SQLDOUBLE)ull : (SQLDOUBLE)ll; write_out_octets(octet_len_ptr, sizeof(SQLDOUBLE), irec); break; case SQL_C_BINARY: return llong_to_binary(arec, irec, ll, data_ptr, octet_len_ptr); /*INDENT-OFF*/ do { case SQL_C_INTERVAL_YEAR: ivl_comp = &ivl.intval.year_month.year; ivl_type = SQL_IS_YEAR; break; case SQL_C_INTERVAL_MONTH: ivl_comp = &ivl.intval.year_month.month; ivl_type = SQL_IS_MONTH; break; case SQL_C_INTERVAL_DAY: ivl_comp = &ivl.intval.day_second.day; ivl_type = SQL_IS_DAY; break; case SQL_C_INTERVAL_HOUR: ivl_comp = &ivl.intval.day_second.hour; ivl_type = SQL_IS_HOUR; break; case SQL_C_INTERVAL_MINUTE: ivl_comp = &ivl.intval.day_second.minute; ivl_type = SQL_IS_MINUTE; break; case SQL_C_INTERVAL_SECOND: ivl_comp = &ivl.intval.day_second.second; ivl_type = SQL_IS_SECOND; break; } while (0); REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); memset(&ivl, 0, sizeof(ivl)); if (unsignd) { REJECT_IF_OOR(ull, 0, ULONG_MAX, SQLUINTEGER, unsigned long); ivl_val = (long long)ull; } else { if (ll < 0) { ivl_val = -ll; ivl.interval_sign = SQL_TRUE; } else { ivl_val = ll; } REJECT_IF_OOR(ivl_val, 0, ULONG_MAX, SQLUINTEGER, unsigned long); } ivl.interval_type = ivl_type; /* all interval components are defined as SQLUINTEGER */ *ivl_comp = (SQLUINTEGER)ivl_val; *(SQL_INTERVAL_STRUCT *)data_ptr = ivl; write_out_octets(octet_len_ptr, sizeof(ivl), irec); DBGH(stmt, "converted quad word 0x%llx to interval %lld type %d.", qword, ivl_val, ivl_type); break; /*INDENT-ON*/ default: BUGH(stmt, "unexpected unhanlded data type: %d.", get_rec_c_type(arec, irec)); RET_HDIAG(stmt, SQL_STATE_HY000, "Unexpected application data " "type for source of type long", 0); } DBGH(stmt, "REC@0x%p, data_ptr@0x%p, copied long long: %lld.", arec, data_ptr, ll); return SQL_SUCCESS; # undef REJECT_IF_OOR # undef TRANSFER_QW } static SQLRETURN double_to_bit(esodbc_rec_st *arec, esodbc_rec_st *irec, double src, void *data_ptr, SQLLEN *octet_len_ptr) { esodbc_state_et state = SQL_STATE_00000; esodbc_stmt_st *stmt = arec->desc->hdr.stmt; REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); write_out_octets(octet_len_ptr, sizeof(SQLCHAR), irec); if (src < 0. || 2. <= src) { REJECT_AS_OOR(stmt, src, /*fixed?*/FALSE, SQL_C_BIT); } else if (0. < src && src < 1.) { *(SQLCHAR *)data_ptr = 0; state = SQL_STATE_01S07; } else if (1. < src && src < 2.) { *(SQLCHAR *)data_ptr = 1; state = SQL_STATE_01S07; } else { /* 0 or 1 */ *(SQLCHAR *)data_ptr = (SQLCHAR)src; } if (state != SQL_STATE_00000) { INFOH(stmt, "truncating when converting %f as %d.", src, *(SQLCHAR *)data_ptr); RET_HDIAGS(stmt, state); } DBGH(stmt, "double %f converted to bit %d.", src, *(SQLCHAR *)data_ptr); return SQL_SUCCESS; } static SQLRETURN double_to_binary(esodbc_rec_st *arec, esodbc_rec_st *irec, double dbl, void *data_ptr, SQLLEN *octet_len_ptr) { size_t cnt; double udbl = dbl < 0. ? -dbl : dbl; float flt; char *ptr; esodbc_state_et state = SQL_STATE_00000; esodbc_stmt_st *stmt = arec->desc->hdr.stmt; if (udbl < FLT_MIN || FLT_MAX < udbl) { /* value's precision/scale requires a double */ cnt = sizeof(dbl); ptr = (char *)&dbl; } else { flt = (float)dbl; cnt = sizeof(flt); ptr = (char *)&flt; } cnt = buff_octet_size(cnt, sizeof(*ptr), arec, irec, &state); if (state) { REJECT_AS_OOR(stmt, dbl, /*fixed?*/FALSE, "[BINARY]<[floating]"); } write_out_octets(octet_len_ptr, cnt, irec); if (data_ptr) { memcpy(data_ptr, ptr, cnt); //TODO: should the driver clear all the received buffer?? Cfg option? //memset((char *)data_ptr + cnt, 0, arec->octet_length - cnt); } DBGH(stmt, "converted double %f to binary on %zd octets.", dbl, cnt); return SQL_SUCCESS; } static inline const void *floats_rep(esodbc_stmt_st *stmt, double dbl, BOOL wide, int *rep) { static const char *flt_def = "%.*f"; static const wchar_t *flt_wdef = L"%.*f"; static const char *flt_exp = "%.*E"; static const wchar_t *flt_wexp = L"%.*E"; esodbc_dbc_st *dbc = HDRH(stmt)->dbc; double abs; if (wide) { switch (dbc->sci_floats) { case ESODBC_FLTS_DEFAULT: *rep = ESODBC_FLTS_DEFAULT; return flt_wdef; case ESODBC_FLTS_SCIENTIFIC: *rep = ESODBC_FLTS_SCIENTIFIC; return flt_wexp; case ESODBC_FLTS_AUTO: abs = (0 <= dbl) ? dbl : -dbl; if (abs < 1E-3 || 1E7 <= abs) { *rep = ESODBC_FLTS_SCIENTIFIC; return flt_wexp; } else { *rep = ESODBC_FLTS_DEFAULT; return flt_wdef; } default: BUGH(stmt, "unexpected floats representation value: %d.", dbc->sci_floats); *rep = ESODBC_FLTS_DEFAULT; return flt_wdef; } } else { switch (dbc->sci_floats) { case ESODBC_FLTS_DEFAULT: *rep = ESODBC_FLTS_DEFAULT; return flt_def; case ESODBC_FLTS_SCIENTIFIC: *rep = ESODBC_FLTS_SCIENTIFIC; return flt_exp; case ESODBC_FLTS_AUTO: abs = (0 <= dbl) ? dbl : -dbl; if (abs < 1E-3 || 1E7 <= abs) { *rep = ESODBC_FLTS_SCIENTIFIC; return flt_exp; } else { *rep = ESODBC_FLTS_DEFAULT; return flt_def; } default: BUGH(stmt, "unexpected floats representation value: %d.", dbc->sci_floats); *rep = ESODBC_FLTS_DEFAULT; return flt_def; } } } static SQLRETURN double_to_str(esodbc_rec_st *arec, esodbc_rec_st *irec, double dbl, void *data_ptr, SQLLEN *octet_len_ptr, BOOL wide) { esodbc_stmt_st *stmt = HDRH(arec->desc)->stmt; SQLCHAR buff[DBL_BASE10_MAX_LEN + /*\0*/1]; SQLWCHAR wbuff[DBL_BASE10_MAX_LEN + /*\0*/1]; size_t usize; esodbc_state_et state; xstr_st xstr; SQLSMALLINT prec; /* ~ision */ const char *flt_fmt; const wchar_t *flt_wfmt; int rep; size_t octets, cnt; int n; /* Note: there's no way for the app to ask for a number of decimal digits * - =scale - from the driver in the conversion (setting the scale of the * ARD would apply to the C type, _[W]CHAR) except for limiting the buffer * size, which has an uneven effect, due to the variance in the number of * the whole part of the floats. */ /* https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/rules-for-conversions */ prec = irec->es_type->maximum_scale; if (wide) { flt_wfmt = (const wchar_t *)floats_rep(stmt, dbl, /*wide?*/TRUE, &rep); /* Note: this call would fail on Win CRT on insufficient buffer */ n = swprintf(wbuff, DBL_BASE10_MAX_LEN + 1,flt_wfmt, prec, dbl); xstr.w.str = wbuff; } else { flt_fmt = (const char *)floats_rep(stmt, dbl, /*wide?*/FALSE, &rep); n = snprintf(buff, DBL_BASE10_MAX_LEN + 1, flt_fmt, prec, dbl); xstr.c.str = buff; } if (n <= 0) { /* ..if this will work this time. */ ERRNH(stmt, "failed to %c-print double %lf and precision %d.", dbl, prec, wide ? 'W' : 'C'); RET_HDIAG(stmt, SQL_STATE_HY000, "failed to print double", 0); } xstr.c.cnt = (size_t)n; assert(xstr.c.cnt == xstr.w.cnt); xstr.wide = wide; usize = wide ? sizeof(SQLWCHAR) : sizeof(SQLCHAR); state = SQL_STATE_00000; octets = buff_octet_size((n + 1) * usize, usize, arec, irec, &state); if (state) { cnt = octets / usize; /* fail the call if exponential printing or default with truncation of * whole part */ if (rep == ESODBC_FLTS_SCIENTIFIC || cnt < xstr.c.cnt - (prec + /* radix char ('.')*/!!prec)) { REJECT_AS_OOR(stmt, dbl, FALSE, "[STRING]<[floating.whole]"); } } else { assert(octets == (n + 1) * usize); } return transfer_xstr0(arec, irec, &xstr, data_ptr, octet_len_ptr); } SQLRETURN sql2c_double(esodbc_rec_st *arec, esodbc_rec_st *irec, SQLULEN pos, double dbl) { esodbc_stmt_st *stmt; void *data_ptr; SQLLEN *octet_len_ptr; esodbc_desc_st *ard, *ird; SQLSMALLINT ctype; SQLRETURN ret; double udbl; stmt = arec->desc->hdr.stmt; ird = stmt->ird; ard = stmt->ard; /* pointer where to write how many characters we will/would use */ octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); /* pointer to app's buffer */ data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); /* Transfer a double to an SQL integer type. * Uses local vars: stmt, data_ptr, irec, octet_len_ptr. * Returns - RET_ - 01S07 on success (due to truncation of fractionals). */ # define RET_TRANSFER_DBL(_dbl, _min, _max, _sqlctype, _ctype) \ do { \ /* using C type limits, so check C and SQL C type precision */ \ assert(sizeof(_sqlctype) == sizeof(_ctype)); \ if (_dbl) { \ if ((_sqlctype)_dbl < _min || _max < (_sqlctype)_dbl) { \ REJECT_AS_OOR(stmt, _dbl, /*fixed?*/FALSE, _sqlctype); \ } \ } else { \ double __udbl = dbl < 0 ? -dbl : dbl; \ if (_max < (_sqlctype)__udbl) { \ REJECT_AS_OOR(stmt, _dbl, /*fixed?*/FALSE, _sqlctype); \ } \ } \ *(_sqlctype *)data_ptr = (_sqlctype)_dbl; \ write_out_octets(octet_len_ptr, sizeof(_sqlctype), irec); \ DBGH(stmt, "converted double %f to " STR(_sqlctype) " 0x%llx.", _dbl, \ (intptr_t)*(_sqlctype *)data_ptr); \ RET_HDIAGS(stmt, SQL_STATE_01S07); \ } while (0) switch ((ctype = get_rec_c_type(arec, irec))) { case SQL_C_CHAR: case SQL_C_WCHAR: return double_to_str(arec, irec, dbl, data_ptr, octet_len_ptr, ctype == SQL_C_WCHAR); case SQL_C_TINYINT: case SQL_C_STINYINT: RET_TRANSFER_DBL(dbl, CHAR_MIN, CHAR_MAX, SQLSCHAR, char); case SQL_C_UTINYINT: RET_TRANSFER_DBL(dbl, 0, UCHAR_MAX, SQLCHAR, unsigned char); case SQL_C_SBIGINT: RET_TRANSFER_DBL(dbl, LLONG_MIN, LLONG_MAX, SQLBIGINT, long long); case SQL_C_UBIGINT: RET_TRANSFER_DBL(dbl, 0, LLONG_MAX, SQLUBIGINT, long long); case SQL_C_SHORT: case SQL_C_SSHORT: RET_TRANSFER_DBL(dbl, SHRT_MIN, SHRT_MAX, SQLSMALLINT, short); case SQL_C_USHORT: RET_TRANSFER_DBL(dbl, 0, USHRT_MAX, SQLUSMALLINT, unsigned short); case SQL_C_LONG: case SQL_C_SLONG: RET_TRANSFER_DBL(dbl, LONG_MIN, LONG_MAX, SQLINTEGER, long); case SQL_C_ULONG: RET_TRANSFER_DBL(dbl, 0, ULONG_MAX, SQLINTEGER, unsigned long); case SQL_C_NUMERIC: REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); ret = double_to_numeric(arec, dbl, data_ptr); if (! SQL_SUCCEEDED(ret)) { return ret; } write_out_octets(octet_len_ptr, sizeof(SQL_NUMERIC_STRUCT), irec); break; case SQL_C_FLOAT: REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); if (dbl) { udbl = dbl < 0 ? -dbl : dbl; if (udbl < FLT_MIN || FLT_MAX < udbl) { REJECT_AS_OOR(stmt, dbl, /* is fixed */FALSE, SQLREAL); } } *(SQLREAL *)data_ptr = (SQLREAL)dbl; write_out_octets(octet_len_ptr, sizeof(SQLREAL), irec); break; case SQL_C_DOUBLE: REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); *(SQLDOUBLE *)data_ptr = dbl; write_out_octets(octet_len_ptr, sizeof(SQLDOUBLE), irec); break; case SQL_C_BIT: return double_to_bit(arec, irec, dbl, data_ptr, octet_len_ptr); case SQL_C_BINARY: return double_to_binary(arec, irec, dbl, data_ptr, octet_len_ptr); default: BUGH(stmt, "unexpected unhanlded data type: %d.", get_rec_c_type(arec, irec)); RET_HDIAG(stmt, SQL_STATE_HY000, "Unexpected application data " "type for source of type double", 0); } DBGH(stmt, "REC@0x%p, data_ptr@0x%p, copied double: %.6e.", arec, data_ptr, dbl); return SQL_SUCCESS; # undef RET_TRANSFER_DBL } static SQLRETURN wstr_to_cstr(esodbc_rec_st *arec, esodbc_rec_st *irec, void *data_ptr, SQLLEN *octet_len_ptr, const wchar_t *wstr, size_t chars_0) { esodbc_state_et state = SQL_STATE_00000; esodbc_stmt_st *stmt = arec->desc->hdr.stmt; char *charp; int in_bytes, out_bytes, c; xstr_st xstr = (xstr_st) { .wide = TRUE, .w = (wstr_st) { (SQLWCHAR *)wstr, chars_0 - 1 } }; gd_offset_apply(stmt, &xstr); assert(xstr.w.str[xstr.w.cnt] == L'\0'); /* how much space would the converted string take? */ in_bytes = U16WC_TO_MBU8(xstr.w.str, xstr.w.cnt + /*\0*/1, NULL, 0); if (in_bytes <= 0) { ERRNH(stmt, "failed to convert wchar* to char* for string `" LWPDL "`.", LWSTR(&xstr.w)); RET_HDIAGS(stmt, SQL_STATE_22018); } /* out length needs to be provided with no (potential) truncation. */ if (octet_len_ptr) { /* chars_0 accounts for 0-terminator, so U16WC_TO_MBU8 will count that * in the output as well => trim it, since we must not count it when * indicating the length to the application */ out_bytes = in_bytes - 1; write_out_octets(octet_len_ptr, out_bytes, irec); } else { DBGH(stmt, "REC@0x%p, NULL octet_len_ptr.", arec); } if (data_ptr) { charp = (char *)data_ptr; /* calculate how much of original data could possibly be copied in * provided buffer; this will be given as a limitation to W-to-C * conversion function. */ in_bytes = (int)buff_octet_size(in_bytes, sizeof(SQLCHAR), arec, irec, &state); if (in_bytes) { /* trim the original string until it fits in output buffer, with * given length limitation */ for (c = (int)xstr.w.cnt + 1; 0 < c; c --) { out_bytes = U16WC_TO_MBU8(xstr.w.str, c, charp, in_bytes); if (0 < out_bytes) { break; /* conversion succeeded */ } // else: out_bytes <= 0 if (! WAPI_ERR_EBUFF()) { ERRNH(stmt, "failed to convert wchar_t* to char* for " "string `" LWPDL "`.", c, xstr.w.str); RET_HDIAGS(stmt, SQL_STATE_22018); } // else: buffer too small for full string: trim further } assert(0 < out_bytes); if (charp[out_bytes - 1] != '\0') { /* ran out of buffer => not 0-term'd and truncated already */ charp[out_bytes - 1] = '\0'; state = SQL_STATE_01004; /* indicate truncation */ c --; /* last char was overwritten with 0 -> dec xfed count */ } /* only update offset if data is copied out */ gd_offset_update(stmt, xstr.w.cnt, c); DBGH(stmt, "REC@0x%p, data_ptr@0x%p, copied %d bytes: `" LCPD "`.", arec, data_ptr, out_bytes, charp); } else { DBGH(stmt, "REC@0x%p, data_ptr@0x%p, no room to copy bytes out.", arec, data_ptr); } } else { DBGH(stmt, "REC@0x%p, NULL data_ptr.", arec); } if (state != SQL_STATE_00000) { RET_HDIAGS(stmt, state); } return SQL_SUCCESS; } /* * -> SQL_C_WCHAR * Note: chars_0 accounts for 0-term, but length indicated back to the * application must not. */ static inline SQLRETURN wstr_to_wstr(esodbc_rec_st *arec, esodbc_rec_st *irec, void *data_ptr, SQLLEN *octet_len_ptr, const wchar_t *wstr, size_t chars_0) { xstr_st xsrc = (xstr_st) { .wide = TRUE, .w = (wstr_st) { (SQLWCHAR *)wstr, chars_0 - 1 } }; return transfer_xstr0(arec, irec, &xsrc, data_ptr, octet_len_ptr); } static SQLRETURN tss_local_to_utc(esodbc_stmt_st *stmt, TIMESTAMP_STRUCT *tss) { struct tm *tmp, tm; time_t utc; TIMESTAMP_STRUCT_TO_TM(tss, &tm); tm.tm_isdst = -1; #ifndef _WIN32 /* ISO/C90's gmtime() is not thead safe */ #error "gmtime_r required for thread safety" #endif /* ! _WIN32 */ if (((utc = mktime(&tm)) == (time_t)-1) || (! (tmp = gmtime(&utc)))) { ERRNH(stmt, "failed to convert local timestamp " "%04hd-%02hu-%02hu %02hu:%02hu:%02hu..%lu to UTC. " MKTIME_FAIL_MSG, tss->year, tss->month, tss->day, tss->hour, tss->minute, tss->second, tss->fraction); RET_HDIAG(stmt, SQL_STATE_22008, "Timestamp timezone adjustment " "failed. " MKTIME_FAIL_MSG, errno); } TM_TO_TIMESTAMP_STRUCT(tmp, tss, tss->fraction); DBGH(stmt, "UTC: `%04hd-%02hu-%02hu %02hu:%02hu:%02hu..%lu`.", tss->year, tss->month, tss->day, tss->hour, tss->minute, tss->second, tss->fraction); return SQL_SUCCESS; } static inline SQLRETURN tm_utc_to_local(esodbc_stmt_st *stmt, struct tm *tm) { time_t utc; struct tm *tmp; #ifndef _WIN32 /* ISO/C90's localtime() is not thead safe */ #error "localtime_r required for thread safety" #endif /* ! _WIN32 */ if ((utc = timegm(tm)) == (time_t)-1 || ! (tmp = localtime(&utc))) { ERRNH(stmt, "failed to convert local timestamp `" "%04d-%02d-%02d %02d:%02d:%02d` to UTC. " MKTIME_FAIL_MSG, tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); RET_HDIAG(stmt, SQL_STATE_22008, "Timestamp conversion failed. " MKTIME_FAIL_MSG, 0); } else { *tm = *tmp; } return SQL_SUCCESS; } static SQLRETURN tss_utc_to_local(esodbc_stmt_st *stmt, TIMESTAMP_STRUCT *tss) { struct tm tm; SQLRETURN res; TIMESTAMP_STRUCT_TO_TM(tss, &tm); tm.tm_isdst = -1; res = tm_utc_to_local(stmt, &tm); if (! SQL_SUCCEEDED(res)) { return res; } TM_TO_TIMESTAMP_STRUCT(&tm, tss, tss->fraction); DBGH(stmt, "local: `%04hd-%02hu-%02hu %02hu:%02hu:%02hu..%lu`.", tss->year, tss->month, tss->day, tss->hour, tss->minute, tss->second, tss->fraction); return SQL_SUCCESS; } /* Parses an ISO8601 timestamp and returns the result into a TIMESTAMP_STRUCT. * The time is adjusted to UTC timezone or kept "local", depending on 'to_utc' * value (TIMESTAMP_STRUCT lacks any timezone indicator). * Returns: * - SQL_STATE_0000 for success, * - _22007 if the string is not a timestamp and * - _22008 if TZ adjustment fails. */ static SQLRETURN parse_iso8601_timestamp(esodbc_stmt_st *stmt, xstr_st *xstr, BOOL to_utc, TIMESTAMP_STRUCT *tss) { /* need the 0-term in the buff, since ascii_w2c will write it */ char buff[ISO8601_TIMESTAMP_MAX_LEN + /*\0*/1]; cstr_st ts_str; timestamp_t tsp; struct tm tm; SQLRETURN res; if (xstr->wide) { DBGH(stmt, "parsing `" LWPDL "` as ISO timestamp.", LWSTR(&xstr->w)); if (xstr->w.cnt < ISO8601_TIMESTAMP_MIN_LEN || ISO8601_TIMESTAMP_MAX_LEN < xstr->w.cnt) { ERRH(stmt, "`" LWPDL "` not an ISO TIMESTAMP.", LWSTR(&xstr->w)); RET_HDIAGS(stmt, SQL_STATE_22007); } else { /* ascii_w2c will count the \0 */ ts_str.cnt = ascii_w2c(xstr->w.str, buff, xstr->w.cnt) - 1; ts_str.str = buff; } } else { DBGH(stmt, "parsing `" LCPDL "` as ISO timestamp.", LCSTR(&xstr->c)); if (xstr->c.cnt < ISO8601_TIMESTAMP_MIN_LEN || ISO8601_TIMESTAMP_MAX_LEN < xstr->c.cnt) { ERRH(stmt, "`" LCPDL "` not an ISO TIMESTAMP.", LCSTR(&xstr->c)); RET_HDIAGS(stmt, SQL_STATE_22007); } else { ts_str = xstr->c; } } if (timestamp_parse(ts_str.str, ts_str.cnt, &tsp) || (! timestamp_to_tm_utc(&tsp, &tm))) { ERRH(stmt, "`" LCPDL "` not in ISO 8601 format.", LCSTR(&ts_str)); RET_HDIAGS(stmt, SQL_STATE_22007); } if (! to_utc) { /* convert UTC to localtime */ res = tm_utc_to_local(stmt, &tm); if (! SQL_SUCCEEDED(res)) { return res; } } /* "the fraction field is the number of billionths of a second" */ TM_TO_TIMESTAMP_STRUCT(&tm, tss, tsp.nsec); DBGH(stmt, "parsed %s timestamp: %04d-%02d-%02d %02d:%02d:%02d.%u.", to_utc ? "UTC" : "local", tss->year, tss->month, tss->day, tss->hour, tss->minute, tss->second, tss->fraction); return SQL_SUCCESS; } BOOL update_crr_date(struct tm *now) { int n; assert(sizeof(SQLCHAR) == sizeof(char)); assert(sizeof(current_date) - /*\0*/1 == DATE_TEMPLATE_LEN); n = snprintf(current_date, DATE_TEMPLATE_LEN + 1, "%04d-%02d-%02d", now->tm_year + 1900, now->tm_mon + 1, now->tm_mday); if (n <= 0 || DATE_TEMPLATE_LEN < n) { ERRN("failed to update the DST date."); return FALSE; } today = *now; return TRUE; } /* Analyze the received string as time/date/timestamp(=timedate) and parse it * into received 'tss' struct, indicating detected format in 'format'. */ static SQLRETURN parse_date_time_ts(esodbc_stmt_st *stmt, xstr_st *xstr, BOOL sql2c, TIMESTAMP_STRUCT *tss, SQLSMALLINT *format) { SQLRETURN ret; /* Template buffer: date or time values will be copied in place and * evaluated as a timestamp. * It needs to be a "recent" date, since UTC-local_time conversions will * need to make use of the correct DST setting. * "recent": date within the same DST interval with the wall-clock time. */ SQLCHAR templ[] = "1970-01-01T00:00:00.000000000+00:00"; SQLCHAR sign; /* conversion Wide to C-string buffer */ SQLCHAR w2c[ISO8601_TIMESTAMP_MAX_LEN]; cstr_st td;/* timedate string */ xstr_st xtd; esodbc_dbc_st *dbc = HDRH(stmt)->dbc; BOOL src_iso; SQLSMALLINT fmt; /* W-strings will eventually require convertion to C-string for TS * conversion => do it now to simplify string analysis */ if (xstr->wide) { td.cnt = ascii_w2c(xstr->w.str, w2c, xstr->w.cnt) - /*\0*/1; td.str = w2c; } else { td = xstr->c; } xtd.wide = FALSE; do { /* could this be a TIMESTAMP (either SQL or ISO8601) value? */ if (TIMESTAMP_TEMPLATE_LEN(0) <= td.cnt) { assert(TIMESTAMP_TEMPLATE_LEN(0) < ISO8601_TIMESTAMP_MIN_LEN); /* is this a SQL-format timestamp? (vs ISO8601) */ if (td.str[DATE_TEMPLATE_LEN] == ' ') { /* vs. 'T' */ /* If the received string is a local timestamp, it needs to be * first parsed (possibly as if it were a UTC value already) * and once that's done and the TSS is available, shifted to * the actual UTC value. */ src_iso = FALSE; td.str[DATE_TEMPLATE_LEN] = 'T'; td.str[td.cnt ++] = 'Z'; DBGH(stmt, "SQL format translated to ISO: [%zu] `" LCPDL "`.", td.cnt, LCSTR(&td)); } else { /* else: already in ISO8601 format */ src_iso = TRUE; } xtd.c = td; fmt = SQL_TYPE_TIMESTAMP; break; } /* could this be a TIME value? hh:mm:ss / hh:mm:ssZ / hh:mm:ss+HH:MM */ if (TIME_TEMPLATE_LEN(0) <= td.cnt && td.str[2] == ':' && td.str[5] == ':') { /* SQL2C, C2SQL: Time -> Timestamp: "The date portion of the * timestamp is set to the current date, and the fractional * seconds portion of the timestamp is set to zero." */ memcpy(templ, current_date, sizeof(current_date) - /*\0*/1); /* copy active value in template and parse it as TS */ /* copy is safe: cnt <= [time template] < [templ] */ memcpy(templ + DATE_TEMPLATE_LEN + /*'T'*/1, td.str, td.cnt); assert(sizeof("+HH:MM") - 1 < TIME_TEMPLATE_LEN(0)); sign = td.str[td.cnt - (sizeof("+HH:MM") - /*\0*/1)]; if (td.str[td.cnt - 1] != 'Z' && /* not a Zulu time */ (sign != '+' && sign != '-')) { src_iso = FALSE; /* not in hh:mm:ss±HH:MM format */ /* there could be a varying number of fractional digits */ templ[DATE_TEMPLATE_LEN + /*'T'*/1 + td.cnt] = 'Z'; xtd.c.cnt = DATE_TEMPLATE_LEN + /*'T'*/1 + td.cnt + /*Z*/1; } else { src_iso = TRUE; xtd.c.cnt = DATE_TEMPLATE_LEN + /*'T'*/1 + td.cnt; } xtd.c.str = templ; fmt = SQL_TYPE_TIME; break; } /* could this be a DATE value? YYYY-MM-DD */ if (DATE_TEMPLATE_LEN <= td.cnt && td.str[4] == '-' && td.str[7] == '-') { /* copy active value in template and parse it as TS */ /* copy is safe: cnt <= [time template] < [templ] */ memcpy(templ, td.str, td.cnt); xtd.c.str = templ; xtd.c.cnt = sizeof(templ)/sizeof(templ[0]) - /*0*/1; fmt = SQL_TYPE_DATE; break; } ERRH(stmt, "`" LCPDL "` is not a valid TIME/DATE/TIMESTAMP value.", LCSTR(&td)); RET_HDIAGS(stmt, SQL_STATE_22007); } while (0); ret = parse_iso8601_timestamp(stmt, &xtd, /*to UTC*/TRUE, tss); if (! SQL_SUCCEEDED(ret)) { ERRH(stmt, "`" LCPDL "` is not a TIMESTAMP value.", LCSTR(&xtd.c)); return ret; } assert(fmt); if (format) { *format = fmt; } if (fmt == SQL_TYPE_DATE) { tss->hour = tss->minute = tss->second = 0; tss->fraction = 0; } else { if (dbc->apply_tz) { if (sql2c) { if (src_iso) { ret = tss_utc_to_local(stmt, tss); } /* else: TEXT non-ISO timestamp: leave as-is (no TZ) */ } else { /* c2sql */ if (! src_iso) { ret = tss_local_to_utc(stmt, tss); } /* else: ISO already */ } } } return ret; } /* * -> SQL_C_TYPE_TIMESTAMP * * Conversts an ES/SQL 'DATETIME' or a text representation of a * timestamp/date/time value into a TIMESTAMP_STRUCT (indicates the detected * input format into the "format" parameter). */ static SQLRETURN wstr_to_timestamp_struct(esodbc_rec_st *arec, esodbc_rec_st *irec, void *data_ptr, SQLLEN *octet_len_ptr, const wchar_t *w_str, size_t chars_0, SQLSMALLINT *format) { esodbc_stmt_st *stmt = arec->desc->hdr.stmt; TIMESTAMP_STRUCT *tss = (TIMESTAMP_STRUCT *)data_ptr; xstr_st xstr = { .wide = TRUE, .w = {(SQLWCHAR *)w_str, chars_0 - 1} }; BOOL to_utc; SQLRETURN ret; if (octet_len_ptr) { *octet_len_ptr = sizeof(*tss); } if (data_ptr) { /* right & left trim the data before attempting conversion */ wtrim_ws(&xstr.w); /*INDENT-OFF*/ switch (irec->concise_type) { do { case SQL_TYPE_DATE: /* ES/SQL passes date as DATETIME */ to_utc = TRUE; break; case SQL_TYPE_TIMESTAMP: to_utc = ! HDRH(stmt)->dbc->apply_tz; /*sql2c only*/ break; } while (0); ret = parse_iso8601_timestamp(stmt, &xstr, to_utc, tss); if (! SQL_SUCCEEDED(ret)) { /* rewrite code if call failed due to invalid format */ if (HDRH(stmt)->diag.state == SQL_STATE_22007) { RET_HDIAGS(stmt, SQL_STATE_22018); } return ret; } if (format) { *format = irec->concise_type; } break; case SQL_TYPE_TIME: case ES_VARCHAR_SQL: case ES_WVARCHAR_SQL: ret = parse_date_time_ts(stmt, &xstr, /*sql2c*/TRUE, tss, format); if (! SQL_SUCCEEDED(ret)) { /* rewrite code if call failed due to invalid format */ if (HDRH(stmt)->diag.state == SQL_STATE_22007) { RET_HDIAGS(stmt, SQL_STATE_22018); } } if (irec->concise_type == SQL_TYPE_TIME) { /* "...and the fractional seconds field of the timestamp * structure is set to zero" */ assert(! format || *format == SQL_TYPE_TIME); tss->fraction = 0; } return ret; case SQL_CHAR: case SQL_LONGVARCHAR: case SQL_WCHAR: case SQL_WLONGVARCHAR: BUGH(stmt, "unexpected (but permitted) SQL type."); RET_HDIAGS(stmt, SQL_STATE_HY004); default: BUGH(stmt, "uncought invalid conversion."); RET_HDIAGS(stmt, SQL_STATE_07006); } /*INDENT-ON*/ } else { DBGH(stmt, "REC@0x%p, NULL data_ptr", arec); } return SQL_SUCCESS; } /* * -> SQL_C_TYPE_DATE */ static SQLRETURN wstr_to_date_struct(esodbc_rec_st *arec, esodbc_rec_st *irec, void *data_ptr, SQLLEN *octet_len_ptr, const wchar_t *wstr, size_t chars_0) { esodbc_stmt_st *stmt = arec->desc->hdr.stmt; DATE_STRUCT *ds = (DATE_STRUCT *)data_ptr; TIMESTAMP_STRUCT tss; SQLRETURN ret; SQLSMALLINT fmt; esodbc_state_et state; if (octet_len_ptr) { *octet_len_ptr = sizeof(*ds); } if (data_ptr) { ret = wstr_to_timestamp_struct(arec, irec, &tss, NULL, wstr, chars_0, &fmt); if (! SQL_SUCCEEDED(ret)) { return ret; } if (fmt == SQL_TYPE_TIME) { /* it's a time-value */ RET_HDIAGS(stmt, SQL_STATE_22018); } ds->year = tss.year; ds->month = tss.month; ds->day = tss.day; if (tss.hour || tss.minute || tss.second || tss.fraction) { if (fmt == SQL_TYPE_TIMESTAMP) { /* value's truncated */ state = SQL_STATE_01S07; } else { ERRH(stmt, "DATE type value contains non-zero time " "components: `" LWPDL "`.", chars_0 - 1, wstr); state = SQL_STATE_22018; } RET_HDIAGS(stmt, state); } } else { DBGH(stmt, "REC@0x%p, NULL data_ptr", arec); } return SQL_SUCCESS; } /* * -> SQL_C_TYPE_TIME */ static SQLRETURN wstr_to_time_struct(esodbc_rec_st *arec, esodbc_rec_st *irec, void *data_ptr, SQLLEN *octet_len_ptr, const wchar_t *wstr, size_t chars_0) { esodbc_stmt_st *stmt = arec->desc->hdr.stmt; TIME_STRUCT *ts = (TIME_STRUCT *)data_ptr; TIMESTAMP_STRUCT tss; SQLRETURN ret; SQLSMALLINT fmt; if (octet_len_ptr) { *octet_len_ptr = sizeof(*ts); } if (data_ptr) { fmt = 0; ret = wstr_to_timestamp_struct(arec, irec, &tss, NULL, wstr, chars_0, &fmt); if (! SQL_SUCCEEDED(ret)) { return ret; } /* need to differentiate between: * - 1234-12-34T00:00:00Z : valid and * - 1234-12-34 : invalid */ if (fmt == SQL_TYPE_DATE) { RET_HDIAGS(stmt, SQL_STATE_22018); } ts->hour = tss.hour; ts->minute = tss.minute; ts->second = tss.second; if (tss.fraction) { /* value's truncated */ RET_HDIAGS(stmt, SQL_STATE_01S07); } } else { DBGH(stmt, "REC@0x%p, NULL data_ptr", arec); } return SQL_SUCCESS; } static inline SQLRETURN adjust_to_precision(esodbc_rec_st *rec, SQLUBIGINT *val, SQLSMALLINT prec_have) { esodbc_stmt_st *stmt = rec->desc->hdr.stmt; if (prec_have < rec->precision) { DBGH(stmt, "augmenting value %llu with %hd digits.", *val, rec->precision - prec_have); /* no overflow check: the target precision should have been checked */ *val *= pow10(rec->precision - prec_have); return SQL_SUCCESS; } else { DBGH(stmt, "truncating value %llu with %hd digits.", *val, prec_have - rec->precision); *val /= pow10(prec_have - rec->precision); RET_HDIAGS(stmt, SQL_STATE_01S07); } } static SQLRETURN parse_iso8601_number(esodbc_rec_st *rec, wstr_st *wstr, SQLUINTEGER *uint, char *sign, SQLUINTEGER *fraction, BOOL *has_fraction) { esodbc_stmt_st *stmt = rec->desc->hdr.stmt; char inc; wstr_st nr; int digits, fdigits; SQLBIGINT bint; SQLUBIGINT ubint; SQLRETURN ret = SQL_SUCCESS;; nr = *wstr; digits = str2bigint(&nr, /*w*/TRUE, &bint, /*strict*/FALSE); if (digits < 0) { return SQL_ERROR; } else if (wstr->cnt <= (size_t)digits) { return SQL_ERROR; /* a number can't end the ISO value */ } else if (wstr->str[digits] == L'.') { /* does it have a fraction? */ if (digits == 0) { bint = 0; /* value is `.<fraction>` */ } if (wstr->cnt <= (size_t)digits + 1) { return SQL_ERROR; /* ISO value ends with `.` */ } nr.str = wstr->str + digits + /*`.`*/1; nr.cnt = wstr->cnt - (digits + 1); fdigits = str2ubigint(&nr, /*w*/TRUE, &ubint, FALSE); if (fdigits < 0) { return SQL_ERROR; } else { digits += fdigits + /*`.`*/1; } if (ULONG_MAX < ubint) { ERRH(stmt, "fraction value too large (%llu).", ubint); return SQL_ERROR; } else { ret = adjust_to_precision(rec, &ubint, fdigits); assert(ubint < ULONG_MAX); /* due to previous sanity checks */ *fraction = (SQLUINTEGER)ubint; } *has_fraction = TRUE; } else { *has_fraction = FALSE; } if (bint < 0) { inc = -1; bint = -bint; } else { inc = 1; } if (ULONG_MAX < bint) { ERRH(stmt, "field value too large (%lld).", bint); return SQL_ERROR; } else { *uint = (SQLUINTEGER)bint; } if (*sign && ((inc ^ *sign) & 0x80)) { ERRH(stmt, "units with mixed signs not supported."); return SQL_ERROR; } else { *sign += inc; } wstr->str += digits; wstr->cnt -= digits; return ret; } /* Parse an ISO8601 period value. * Elasticsearch'es implementation deviates from the standard by, for example: * - demoting the day field: `INTERVAL '1 1' DAY TO HOUR` -> * `PT25H` instead of `P1DT1H`; `INTERVAL '1' DAY` -> `PT24H` vs. `P1D`; * - promoting the hour field: `INTERVAL '61:59' MINUTE TO SECOND` -> * `PT1H1M59S` instead of `PT61M59S`; `INTERVAL '60' MINUTE` -> `PT1H` vs. * `PT60M`. * - promoting the minute field: `INTERVAL '61' SECOND` -> `PT1M1S` vs. * `PT61S` */ static SQLRETURN parse_interval_iso8601(esodbc_rec_st *rec, SQLSMALLINT ctype, wstr_st *wstr, SQL_INTERVAL_STRUCT *ivl) { esodbc_stmt_st *stmt = rec->desc->hdr.stmt; char sign; SQLWCHAR *crr, *end; wstr_st nr; SQLUINTEGER uint, fraction; BOOL has_fraction; enum {ST_UNINITED, ST_PERIOD, ST_TIME, ST_NUMBER} state, saved; uint16_t fields_bm; /* read fields bit mask */ static const uint16_t type2bm[] = { 1 << SQL_IS_YEAR, 1 << SQL_IS_MONTH, 1 << SQL_IS_DAY, 1 << SQL_IS_HOUR, 1 << SQL_IS_MINUTE, 1 << SQL_IS_SECOND, (1 << SQL_IS_YEAR) | (1 << SQL_IS_MONTH), (1 << SQL_IS_DAY) | (1 << SQL_IS_HOUR), (1 << SQL_IS_DAY) | (1 << SQL_IS_HOUR) | (1 << SQL_IS_MINUTE), (1 << SQL_IS_DAY) | (1 << SQL_IS_HOUR) | (1 << SQL_IS_MINUTE) | (1 << SQL_IS_SECOND), (1 << SQL_IS_HOUR) | (1 << SQL_IS_MINUTE), (1 << SQL_IS_HOUR) | (1 << SQL_IS_MINUTE) | (1 << SQL_IS_SECOND), (1 << SQL_IS_MINUTE) | (1 << SQL_IS_SECOND), }; uint16_t type2bm_ivl; /* the type bit mask for the interval */ SQLRETURN ret; SQLUINTEGER secs; /* ~ond~ */ /* Sets a bit in a bitmask corresponding to one interval field, given * `_ivl`, or errs if already set. * Uses local vars: fields_bm. * Jumps to err_format on error. */ # define SET_BITMASK_OR_ERR(_ivl) \ do { \ uint8_t _bit = 1 << (_ivl); \ if (_bit & fields_bm) { \ ERRH(stmt, "field %d already set.", (_ivl)); \ goto err_format; \ } \ fields_bm |= _bit; \ } while (0) /* Assigns a value to an interval field, `_ivl_field` of type `_ivl_type`, * if current state is ST_NUMBER and the previous one matches the given * `_prev_state`. * Uses local vars: ivl, fields_bm, state, saved. * Jumps to err_format on error. */ # define ASSIGN_FIELD(_prev_state, _ivl_type, _ivl_field_) \ do { \ if (state != ST_NUMBER || saved != _prev_state) { \ goto err_format; \ } \ if (_ivl_type != SQL_IS_SECOND && has_fraction) { \ goto err_format; \ } \ SET_BITMASK_OR_ERR(_ivl_type); \ ivl->intval._ivl_field_ = uint; \ DBGH(stmt, "field %d assigned value %lu.", _ivl_type, uint); \ state = saved; \ } while (0) /* Safe ulong addition */ # define ULONG_SAFE_ADD(_to, _from) \ do { \ if (ULONG_MAX - (_from) < _to) { \ goto err_overflow; \ } else { \ _to += _from; \ } \ } while (0) /* the interval type will be used as bitmask indexes */ assert(0 < SQL_IS_YEAR && SQL_IS_MINUTE_TO_SECOND < 16); DBGH(stmt, "ISO 8601 to parse: [%zu] `" LWPDL "`, C target: %hd.", wstr->cnt, LWSTR(wstr), ctype); memset(ivl, 0, sizeof(*ivl)); sign = 0; fields_bm = 0; fraction = 0; state = saved = ST_UNINITED; for (crr = wstr->str, end = wstr->str + wstr->cnt; crr < end; ) { switch (*crr | 0x20) { /* ~tolower(), ascii vals only */ case L'p': if (state != ST_UNINITED) { goto err_parse; } state = ST_PERIOD; break; case L't': if (state != ST_PERIOD) { goto err_parse; } state = ST_TIME; break; case L'+': case L'-': case L'.': /* L'0' .. L'9' */ case L'0': case L'1': case L'2': case L'3': case L'4': case L'5': case L'6': case L'7': case L'8': case L'9': if (state != ST_PERIOD && state != ST_TIME) { goto err_parse; } nr.str = crr; nr.cnt = end - crr; ret = parse_iso8601_number(rec, &nr, &uint, &sign, &fraction, &has_fraction); if (! SQL_SUCCEEDED(ret)) { goto err_format; } else { crr = nr.str; } saved = state; state = ST_NUMBER; continue; case L'y': ASSIGN_FIELD(ST_PERIOD, SQL_IS_YEAR, year_month.year); break; case L'm': if (state != ST_NUMBER || (saved != ST_PERIOD && saved != ST_TIME)) { goto err_format; } if (has_fraction) { goto err_format; } if (saved == ST_PERIOD) { SET_BITMASK_OR_ERR(SQL_IS_MONTH); ivl->intval.year_month.month = uint; DBGH(stmt, "field %d assigned value %lu.", SQL_IS_MONTH, uint); } else { SET_BITMASK_OR_ERR(SQL_IS_MINUTE); ivl->intval.day_second.minute = uint; DBGH(stmt, "field %d assigned value %lu.", SQL_IS_MONTH, uint); } state = saved; break; case L'd': ASSIGN_FIELD(ST_PERIOD, SQL_IS_DAY, day_second.day); break; case L'h': ASSIGN_FIELD(ST_TIME, SQL_IS_HOUR, day_second.hour); break; case L's': ASSIGN_FIELD(ST_TIME, SQL_IS_SECOND, day_second.second); if (has_fraction) { ivl->intval.day_second.fraction = fraction; DBGH(stmt, "field fraction assigned value %lu.", fraction); } break; default: goto err_parse; } crr ++; } if (state != ST_PERIOD && state != ST_TIME) { goto err_format; } ivl->interval_sign = (sign < 0) ? SQL_TRUE : SQL_FALSE; assert(SQL_CODE_YEAR == SQL_IS_YEAR); ivl->interval_type = ctype - (SQL_INTERVAL_YEAR - SQL_CODE_YEAR); assert(0 < /*starts at 1*/ ivl->interval_type && ivl->interval_type < 8 * sizeof(type2bm)/sizeof(type2bm[0])); type2bm_ivl = type2bm[ivl->interval_type - 1]; /* If the expression set fields not directly relevant to the interval AND * the interval is not of type year/-/month one (which does seem to * conform to the standard), rebalance the member values as expected by * the interval type. */ if ((type2bm_ivl != fields_bm) && ((type2bm_ivl & ((1 << SQL_CODE_YEAR) | (1 << SQL_CODE_MONTH))) == 0)) { secs = ivl->intval.day_second.second; ULONG_SAFE_ADD(secs, 60 * ivl->intval.day_second.minute); //... ULONG_SAFE_ADD(secs, 3600 * ivl->intval.day_second.hour); ULONG_SAFE_ADD(secs, 24 * 3600 * ivl->intval.day_second.day); /* clear everything set, but reinstate any set fractions */ fields_bm = 0; memset(&ivl->intval.day_second, 0, sizeof(ivl->intval.day_second)); ivl->intval.day_second.fraction = fraction; if (type2bm_ivl & (1 << SQL_CODE_SECOND)) { ivl->intval.day_second.second = secs; fields_bm |= (1 << SQL_CODE_SECOND); } else if (has_fraction) { /* fraction val itself is truncated away due to precision * zero/null for intervals with no seconds component */ ERRH(stmt, "fraction in interval with no second component"); goto err_format; } if (type2bm_ivl & (1 << SQL_CODE_MINUTE)) { if (ivl->intval.day_second.second) { ivl->intval.day_second.minute = ivl->intval.day_second.second / 60; ivl->intval.day_second.second = secs % 60; } else { ivl->intval.day_second.minute = secs / 60; assert(secs % 60 == 0); } fields_bm |= (1 << SQL_CODE_MINUTE); } if (type2bm_ivl & (1 << SQL_CODE_HOUR)) { if (ivl->intval.day_second.minute) { ivl->intval.day_second.hour = ivl->intval.day_second.minute / 60; ivl->intval.day_second.minute %= 60; } else { ivl->intval.day_second.hour = secs / 3600; assert(secs % 3600 == 0); } fields_bm |= (1 << SQL_CODE_HOUR); } if (type2bm_ivl & (1 << SQL_CODE_DAY)) { if (ivl->intval.day_second.hour) { ivl->intval.day_second.day = ivl->intval.day_second.hour / 24; ivl->intval.day_second.hour %= 24; } else { ivl->intval.day_second.day = secs / (24 * 3600); assert(secs % (24 * 3600) == 0); } fields_bm |= (1 << SQL_CODE_DAY); } } /* Check that the ISO value has no fields set other than those allowed * for the advertised type. Since the year_month and day_second form a * union, this can't be done by checks against field values. */ if (~type2bm_ivl & fields_bm) { ERRH(stmt, "illegal fields (0x%hx) for interval type %hd (0x%hx).", fields_bm, ctype, type2bm_ivl); goto err_format; } return ret; err_overflow: ERRH(stmt, "integer overflow while normalizing ISO8601 format [%zu] `" LWPDL "`.", wstr->cnt, LWSTR(wstr)); RET_HDIAGS(stmt, SQL_STATE_22015); err_parse: ERRH(stmt, "unexpected current char `%c` in state %d.", *crr, state); err_format: ERRH(stmt, "invalid ISO8601 format [%zu] `" LWPDL "`.", wstr->cnt, LWSTR(wstr)); RET_HDIAG(stmt, SQL_STATE_22018, "Invalid server answer", 0); # undef ASSIGN_FIELD # undef SET_BITMASK_OR_ERR # undef ULONG_SAFE_ADD } /* Parse one field of the value. * If 'limit' is non-zero, the field must remain below it; otehrwise it's a * leading field and the precision must apply. * 'wstr' is adjusted to have the field removed. * The field value is stored in 'uiptr'. * */ static SQLRETURN parse_interval_field(esodbc_rec_st *rec, SQLUINTEGER limit, wstr_st *wstr, SQLUINTEGER *uiptr) { esodbc_stmt_st *stmt = rec->desc->hdr.stmt; SQLUBIGINT ubint; int digits; errno = 0; digits = str2ubigint(wstr, /*wide*/TRUE, &ubint, /*strict*/FALSE); if (digits <= 0) { ERRH(stmt, "can not parse `" LWPDL "` as uinteger.", LWSTR(wstr)); RET_HDIAGS(stmt, errno == ERANGE ? SQL_STATE_22015 : SQL_STATE_22018); } else { wstr->str += digits; wstr->cnt -= digits; } if (ULONG_MAX < ubint) { ERRH(stmt, "available value (%llu) larger than max (%lu).", ubint, ULONG_MAX); RET_HDIAGS(stmt, SQL_STATE_22015); } if (limit <= 0) { // TODO: digits could account for leading 0s (that could wrongly match // the condition) if (rec->datetime_interval_precision < digits) { ERRH(stmt, "interval's set dt_interval precision (%ld) lower than " "data's (%d).", rec->datetime_interval_precision, digits); RET_HDIAGS(stmt, SQL_STATE_22018); } } else if (limit < ubint) { ERRH(stmt, "interval field value (%lu) higher than max (%lu).", ubint, limit); RET_HDIAGS(stmt, SQL_STATE_22015); } *uiptr = (SQLUINTEGER)ubint; DBGH(stmt, "parsed field value: %lu.", *uiptr); return SQL_SUCCESS; } /* Interval precision: * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/interval-data-type-precision */ static SQLRETURN parse_interval_second(esodbc_rec_st *rec, SQLUINTEGER limit, wstr_st *wstr, SQL_INTERVAL_STRUCT *ivl) { esodbc_stmt_st *stmt = rec->desc->hdr.stmt; size_t digits; SQLRETURN ret; unsigned long long ull; ret = parse_interval_field(rec, limit, wstr, &ivl->intval.day_second.second); if (! SQL_SUCCEEDED(ret)) { return ret; } if (wstr->cnt <= 0) { /* fiels ends without fractions: `1` */ DBGH(stmt, "component 'second' has value: %lu.", ivl->intval.day_second.second); return SQL_SUCCESS; } if (wstr->str[0] != L'.') { ERRH(stmt, "unexpected character %c in value.", wstr->str[0]); RET_HDIAGS(stmt, SQL_STATE_22018); } else { wstr->str ++; wstr->cnt --; } if (wstr->cnt <= 0) { /* fiels ends after dot: `1.` */ return SQL_SUCCESS; } digits = wstr->cnt; ret = parse_interval_field(rec, ULONG_MAX, wstr, &ivl->intval.day_second.fraction); if (! SQL_SUCCEEDED(ret)) { return ret; } else { digits -= wstr->cnt; assert(digits < SHRT_MAX); } ull = ivl->intval.day_second.fraction; ret = adjust_to_precision(rec, &ull, (SQLSMALLINT)digits); assert(ull < ULONG_MAX); /* interval seconds precision limited to 9 */ ivl->intval.day_second.fraction = (SQLUINTEGER)ull; if (wstr->cnt) { ERRH(stmt, "invalid trailing chars: [%zu] `" LWPDL "`", wstr->cnt, LWSTR(wstr)); RET_HDIAGS(stmt, SQL_STATE_22018); } else { DBGH(stmt, "single component 'second' has value: %lu.%lu(%hd).", ivl->intval.day_second.second, ivl->intval.day_second.fraction, rec->precision); } return ret; } /* TODO: use rec's .datetime_interval_precision and .precision through a * "general" get_col_xxx() function (: decdigits or similar) */ /* parses the `<value>` in an interval literal */ static SQLRETURN parse_interval_literal_value(esodbc_rec_st *rec, wstr_st *wstr, SQL_INTERVAL_STRUCT *ivl) { esodbc_stmt_st *stmt = rec->desc->hdr.stmt; SQLUINTEGER *uiptr; SQLRETURN ret; /* Parses one (numeric) field in the interval value and checks that the * entire string has been parsed, if, `_at_the_end` is set (for single * field intervals). * Returns on error. */ # define PARSE_IVL_FLD_OR_RET(rec, limit, wstr, uiptr, _at_the_end) \ do { \ ret = parse_interval_field(rec, limit, wstr, uiptr); \ if (! SQL_SUCCEEDED(ret)) { \ return ret; \ } \ if (_at_the_end && wstr->cnt) { \ ERRH(stmt, "invalid trailing chars: [%zu] `" LWPDL "`", \ wstr->cnt, LWSTR(wstr)); \ RET_HDIAGS(stmt, SQL_STATE_22018); \ } \ } while (0) DBGH(stmt, "literal to parse: type: %d, value [%zu] `" LWPDL "`.", ivl->interval_type, wstr->cnt, LWSTR(wstr)); /*INDENT-OFF*/ switch (ivl->interval_type) { do { case SQL_IS_YEAR: uiptr = &ivl->intval.year_month.year; break; case SQL_IS_MONTH: uiptr = &ivl->intval.year_month.month; break; case SQL_IS_DAY: uiptr = &ivl->intval.day_second.day; break; case SQL_IS_HOUR: uiptr = &ivl->intval.day_second.hour; break; case SQL_IS_MINUTE: uiptr = &ivl->intval.day_second.minute; break; } while (0); PARSE_IVL_FLD_OR_RET(rec, /*limit*/0, wstr, uiptr, /*end?*/TRUE); DBGH(stmt, "single component of type %d has value: %lu.", ivl->interval_type, *uiptr); break; case SQL_IS_SECOND: ret = parse_interval_second(rec, /*limit*/0, wstr, ivl); if (! SQL_SUCCEEDED(ret)) { return ret; } break; case SQL_IS_YEAR_TO_MONTH: PARSE_IVL_FLD_OR_RET(rec, /*limit*/0, wstr, &ivl->intval.year_month.year, /*end?*/FALSE); wltrim_ws(wstr); if (wstr->str[0] != L'-') { ERRH(stmt, "invalid char as separator: `%c`.", wstr->str[0]); RET_HDIAGS(stmt, SQL_STATE_22018); } else { wstr->str ++; wstr->cnt --; } wltrim_ws(wstr); PARSE_IVL_FLD_OR_RET(rec, /*limit: months*/11, wstr, &ivl->intval.year_month.month, /*end?*/TRUE); break; case SQL_IS_DAY_TO_HOUR: case SQL_IS_DAY_TO_MINUTE: case SQL_IS_DAY_TO_SECOND: PARSE_IVL_FLD_OR_RET(rec, /*limit*/0, wstr, &ivl->intval.day_second.day, /*end?*/FALSE); wltrim_ws(wstr); PARSE_IVL_FLD_OR_RET(rec, /*hour limit*/23, wstr, &ivl->intval.day_second.hour, /*end?*/ivl->interval_type == SQL_IS_DAY_TO_HOUR); if (ivl->interval_type == SQL_IS_DAY_TO_HOUR) { break; } if (wstr->str[0] != L':') { ERRH(stmt, "invalid char as separator: `%c`.", wstr->str[0]); RET_HDIAGS(stmt, SQL_STATE_22018); } else { wstr->str ++; wstr->cnt --; } PARSE_IVL_FLD_OR_RET(rec, /*minute limit*/59, wstr, &ivl->intval.day_second.minute, /*end?*/ivl->interval_type == SQL_IS_DAY_TO_MINUTE); if (ivl->interval_type == SQL_IS_DAY_TO_MINUTE) { break; } if (wstr->str[0] != L':') { ERRH(stmt, "invalid char as separator: `%c`.", wstr->str[0]); RET_HDIAGS(stmt, SQL_STATE_22018); } else { wstr->str ++; wstr->cnt --; } ret = parse_interval_second(rec, /*second limit*/59, wstr, ivl); if (! SQL_SUCCEEDED(ret)) { return ret; } break; case SQL_IS_HOUR_TO_MINUTE: case SQL_IS_HOUR_TO_SECOND: PARSE_IVL_FLD_OR_RET(rec, /*limit*/0, wstr, &ivl->intval.day_second.hour, /*end?*/FALSE); if (wstr->str[0] != L':') { ERRH(stmt, "invalid char as separator: `%c`.", wstr->str[0]); RET_HDIAGS(stmt, SQL_STATE_22018); } else { wstr->str ++; wstr->cnt --; } /*no break*/ case SQL_IS_MINUTE_TO_SECOND: PARSE_IVL_FLD_OR_RET(rec, /*minute limit*/ ivl->interval_type == SQL_IS_MINUTE_TO_SECOND ? 0 : 59, wstr, &ivl->intval.day_second.minute, /*end?*/ivl->interval_type == SQL_IS_HOUR_TO_MINUTE); if (ivl->interval_type == SQL_IS_HOUR_TO_MINUTE) { break; } if (wstr->str[0] != L':') { ERRH(stmt, "invalid char as separator: `%c`.", wstr->str[0]); RET_HDIAGS(stmt, SQL_STATE_22018); } else { wstr->str ++; wstr->cnt --; } ret = parse_interval_second(rec, /*second limit*/59, wstr, ivl); if (! SQL_SUCCEEDED(ret)) { return ret; } break; } /*INDENT-ON*/ return SQL_SUCCESS; # undef PARSE_IVL_FLD_OR_RET } /* parse the qualifier in `± '<value>' <qualifier>`, rtrim-ing it from wstr */ static SQLSMALLINT parse_interval_type(wstr_st *wstr) { /* compares the end of the local `wstr` against given `_end` string and * trims the former, on match. * Uses local var: wstr. * Returns on no match (the qualifier is incorrect). */ # define TRIM_IF_ENDS_WITH_OR_RET(_end) \ do { \ wstr_st _wend = MK_WSTR(_end); \ if (wstr->cnt <= _wend.cnt || \ wmemncasecmp(&wstr->str[wstr->cnt - _wend.cnt], \ _wend.str, _wend.cnt)) { \ return 0; \ } \ wstr->cnt -= _wend.cnt; \ wrtrim_ws(wstr); \ } while (0) # define IS_DELIM(wc) \ (wc == L'\'' || wc == L'.' || (L'0' <= wc && wc <= L'9' )) if (wstr->cnt <= /* "day", smallest interval qualifier */3) { return 0; } /* split by last letter */ switch (wstr->str[wstr->cnt - 1] | 0x20) { /* ~tolower(), ascii val only */ case L'y': /* day */ TRIM_IF_ENDS_WITH_OR_RET("day"); return SQL_IS_DAY; case L'r': /* year, hour, day to hour */ switch (wstr->str[wstr->cnt - 2] | 0x20) { /* ~tolower() */ case L'a': /* ...in year */ TRIM_IF_ENDS_WITH_OR_RET("year"); return SQL_IS_YEAR; case L'u': /* ...in hour */ TRIM_IF_ENDS_WITH_OR_RET("hour"); if (IS_DELIM(wstr->str[wstr->cnt - 1])) { return SQL_IS_HOUR; } TRIM_IF_ENDS_WITH_OR_RET("to"); TRIM_IF_ENDS_WITH_OR_RET("day"); return SQL_IS_DAY_TO_HOUR; } break; case L'e': /* minute, day/hour to minute */ TRIM_IF_ENDS_WITH_OR_RET("minute"); if (IS_DELIM(wstr->str[wstr->cnt - 1])) { return SQL_IS_MINUTE; } TRIM_IF_ENDS_WITH_OR_RET("to"); switch (wstr->str[wstr->cnt - 1] | 0x20) { /* ~tolower() */ case L'y': /* ...in "day" */ TRIM_IF_ENDS_WITH_OR_RET("day"); return SQL_IS_DAY_TO_MINUTE; case L'r': /* ...in "hour" */ TRIM_IF_ENDS_WITH_OR_RET("hour"); return SQL_IS_HOUR_TO_MINUTE; } case L'h': /* month, year to month */ TRIM_IF_ENDS_WITH_OR_RET("month"); if (IS_DELIM(wstr->str[wstr->cnt - 1])) { return SQL_IS_MONTH; } TRIM_IF_ENDS_WITH_OR_RET("to"); TRIM_IF_ENDS_WITH_OR_RET("year"); return SQL_IS_YEAR_TO_MONTH; case L'd': /* second, day/hour/minute to second */ TRIM_IF_ENDS_WITH_OR_RET("second"); if (IS_DELIM(wstr->str[wstr->cnt - 1])) { return SQL_IS_SECOND; } TRIM_IF_ENDS_WITH_OR_RET("to"); switch (wstr->str[wstr->cnt - 1] | 0x20) { /* ~tolower() */ case L'y': /* ...in "day" */ TRIM_IF_ENDS_WITH_OR_RET("day"); return SQL_IS_DAY_TO_SECOND; case L'r': /* ...in "hour" */ TRIM_IF_ENDS_WITH_OR_RET("hour"); return SQL_IS_HOUR_TO_SECOND; case L'e': /* ...in "minute" */ TRIM_IF_ENDS_WITH_OR_RET("minute"); return SQL_IS_MINUTE_TO_SECOND; } } return 0; # undef TRIM_IF_ENDS_WITH_OR_RET } /* parse `INTERVAL ± '<value>' <qualifier>`, each space being * optional and extendable; the entire expression can be enclosed in {} */ static SQLRETURN parse_interval_literal(esodbc_rec_st *rec, wstr_st *wstr, SQL_INTERVAL_STRUCT *ivl) { esodbc_stmt_st *stmt = rec->desc->hdr.stmt; static const wstr_st INTERVAL = WSTR_INIT("INTERVAL"); memset(ivl, 0, sizeof(*ivl)); if (wstr->cnt < INTERVAL.cnt + /* "INTERVAL1DAY" */4) { ERRH(stmt, "too short for interval literal."); RET_HDIAGS(stmt, SQL_STATE_22018); } if (wstr->str[0] == L'{' && wstr->str[wstr->cnt - 1] == L'}') { /* strip enclosing {} */ wstr->str += 1; wstr->cnt -= 2; wtrim_ws(wstr); if (wstr->cnt <= INTERVAL.cnt) { ERRH(stmt, "not an interval literal."); RET_HDIAGS(stmt, SQL_STATE_22018); } } if (wmemncasecmp(wstr->str, INTERVAL.str, INTERVAL.cnt)) { ERRH(stmt, "not an interval literal."); RET_HDIAGS(stmt, SQL_STATE_22018); } else { wstr->str += INTERVAL.cnt; wstr->cnt -= INTERVAL.cnt; } wltrim_ws(wstr); ivl->interval_type = parse_interval_type(wstr); if (! ivl->interval_type) { ERRH(stmt, "failed to extract interval type in [%zu] `" LWPDL "`.", wstr->cnt, LWSTR(wstr)); RET_HDIAGS(stmt, SQL_STATE_22018); } else { DBGH(stmt, "parsed interval qualifier: %d", ivl->interval_type); } /* wstr is now adjusted to `± '<value>'` */ if (wstr->str[0] == L'-' || wstr->str[0] == L'+') { ivl->interval_sign = (wstr->str[0] == L'-') ? SQL_TRUE : SQL_FALSE; /* "trim" +/- */ wstr->str ++; wstr->cnt --; wltrim_ws(wstr); } else { ivl->interval_sign = SQL_FALSE; } DBGH(stmt, "parsed interval sign: %d", ivl->interval_sign); /* strip enclosing single quotes */ if (wstr->str[0] == '\'' && wstr->str[wstr->cnt - 1] == '\'') { wstr->str += 1; wstr->cnt -= 2; } return parse_interval_literal_value(rec, wstr, ivl); } static SQLRETURN sql2c_interval(esodbc_rec_st *arec, SQLSMALLINT sqltype, SQLSMALLINT ctype, void *data_ptr, wstr_st *wstr) { esodbc_stmt_st *stmt = arec->desc->hdr.stmt; SQLRETURN ret; SQL_INTERVAL_STRUCT ivl; static const SQLSMALLINT ivl_type2c_type[] = { SQL_C_INTERVAL_YEAR, /* = 1, ++ */ SQL_C_INTERVAL_MONTH, SQL_C_INTERVAL_DAY, SQL_C_INTERVAL_HOUR, SQL_C_INTERVAL_MINUTE, SQL_C_INTERVAL_SECOND, SQL_C_INTERVAL_YEAR_TO_MONTH, SQL_C_INTERVAL_DAY_TO_HOUR, SQL_C_INTERVAL_DAY_TO_MINUTE, SQL_C_INTERVAL_DAY_TO_SECOND, SQL_C_INTERVAL_HOUR_TO_MINUTE, SQL_C_INTERVAL_HOUR_TO_SECOND, SQL_C_INTERVAL_MINUTE_TO_SECOND }; if (wstr->cnt <= 0) { ERRH(stmt, "too short for INTERVAL."); goto err_22018; } /* split processing by the source type */ if (sqltype == ES_WVARCHAR_SQL || sqltype == ES_VARCHAR_SQL) { ret = parse_interval_literal(arec, wstr, &ivl); assert(0 <= ivl.interval_type && ivl.interval_type <= sizeof(ivl_type2c_type)/sizeof(ivl_type2c_type[0])); if (ivl_type2c_type[ivl.interval_type - 1] != ctype) { /* intra-interval conversion not supported */ ERRH(stmt, "parsed interval type (%hd) differs from C type (%hd).", ivl_type2c_type[ivl.interval_type - 1], ctype); RET_HDIAGS(stmt, SQL_STATE_07006); } } else { /* single-component intervals would be intra-convertible: TODO? */ assert(sqltype == ctype); /* C == SQL types, for intervals */ ret = parse_interval_iso8601(arec, ctype, wstr, &ivl); } if (SQL_SUCCEEDED(ret)) { DBGH(stmt, "interval succesfully parsed."); assert(data_ptr); *(SQL_INTERVAL_STRUCT *)data_ptr = ivl; } else { ERRH(stmt, "failed to convert [%zu] `" LWPDL "` to an interval.", wstr->cnt, LWSTR(wstr)); } return ret; err_22018: ERRH(stmt, "not a valid interval: [%zu] `" LWPDL "`.", wstr->cnt, LWSTR(wstr)); RET_HDIAGS(stmt, SQL_STATE_22018); } /* print just the 'second[.fraction]' field of an interval */ static size_t print_interval_sec(esodbc_rec_st *rec, SQL_INTERVAL_STRUCT *ivl, void *dest, BOOL wide) { esodbc_stmt_st *stmt = rec->desc->hdr.stmt; wchar_t wfmt[] = L"%.0lf"; char cfmt[] = "%.0lf"; double dbl; size_t res; if (ivl->intval.day_second.fraction && rec->precision) { assert(ESODBC_MAX_SEC_PRECISION < 10); assert(0 <= rec->precision && rec->precision <= ESODBC_MAX_SEC_PRECISION); dbl = (double)ivl->intval.day_second.fraction; dbl /= pow10(rec->precision); dbl += (double)ivl->intval.day_second.second; if (wide) { wfmt[2] = L'0' + rec->precision; /* printf's limits: max length of '<second>.<fraction>', accounted * in buffer's max len estimation. */ res = swprintf((wchar_t *)dest, 2 * sizeof("4294967295") + 1, wfmt, dbl); } else { cfmt[2] = '0' + rec->precision; res = snprintf((char *)dest, 2 * sizeof("4294967295") + 1, cfmt, dbl); } if (res < 0) { ERRNH(stmt, "failed to print the 'second' component for " "second: %lu, fraction: %lu, precision: %hd.", ivl->intval.day_second.second, ivl->intval.day_second.fraction, rec->precision); return 0; } } else { res = ui64tot(ivl->intval.day_second.second, dest, wide); } return res; } /* Convert an interval struct to a SQL literal (value). * There's no reference for the sign in: * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/interval-data-type-length */ static size_t print_interval_sql(esodbc_rec_st *arec, SQL_INTERVAL_STRUCT *ivl, SQLWCHAR *dest) { esodbc_stmt_st *stmt = arec->desc->hdr.stmt; size_t pos, res; SQLUINTEGER uint; wchar_t fmt[] = L"%.0f";; pos = 0; if (ivl->interval_sign) { dest[pos ++] = L'-'; } /*INDENT-OFF*/ switch (ivl->interval_type) { do { case SQL_IS_YEAR: uint = ivl->intval.year_month.year; break; case SQL_IS_MONTH: uint = ivl->intval.year_month.month; break; case SQL_IS_DAY: uint = ivl->intval.day_second.day; break; case SQL_IS_HOUR: uint = ivl->intval.day_second.hour; break; case SQL_IS_MINUTE: uint = ivl->intval.day_second.minute; break; } while (0); pos += ui64tot(uint, dest + pos, /*wide*/TRUE); break; case SQL_IS_YEAR_TO_MONTH: pos += ui64tot(ivl->intval.year_month.year, dest + pos, TRUE); dest[pos ++] = L'-'; pos += ui64tot(ivl->intval.year_month.month, dest + pos, TRUE); break; case SQL_IS_DAY_TO_HOUR: pos += ui64tot(ivl->intval.day_second.day, dest + pos, TRUE); dest[pos ++] = L' '; pos += ui64tot(ivl->intval.day_second.hour, dest + pos, TRUE); break; case SQL_IS_DAY_TO_MINUTE: pos += ui64tot(ivl->intval.day_second.day, dest + pos, TRUE); dest[pos ++] = L' '; pos += ui64tot(ivl->intval.day_second.hour, dest + pos, TRUE); dest[pos ++] = L':'; pos += ui64tot(ivl->intval.day_second.minute, dest + pos, TRUE); break; case SQL_IS_DAY_TO_SECOND: pos += ui64tot(ivl->intval.day_second.day, dest + pos, TRUE); dest[pos ++] = L' '; case SQL_IS_HOUR_TO_SECOND: pos += ui64tot(ivl->intval.day_second.hour, dest + pos, TRUE); dest[pos ++] = L':'; case SQL_IS_MINUTE_TO_SECOND: pos += ui64tot(ivl->intval.day_second.minute, dest + pos, TRUE); dest[pos ++] = L':'; case SQL_IS_SECOND: res = print_interval_sec(arec, ivl, dest + pos, /*wide*/TRUE); if (res <= 0) { return 0; } else { pos += res; } break; case SQL_IS_HOUR_TO_MINUTE: pos += ui64tot(ivl->intval.day_second.hour, dest + pos, TRUE); dest[pos ++] = L':'; pos += ui64tot(ivl->intval.day_second.minute, dest + pos, TRUE); break; default: BUGH(stmt, "unexpected interval type %d.", ivl->interval_type); return 0; } /*INDENT-ON*/ return pos; } /* Convert an interval struct to a ISO8601 value. */ static size_t print_interval_iso8601(esodbc_rec_st *rec, SQL_INTERVAL_STRUCT *ivl, SQLCHAR *dest) { esodbc_stmt_st *stmt = rec->desc->hdr.stmt; size_t res, pos; BOOL t_added; /* Prints one interval field, if non-zero. * Uses local vars: ivl, dest, pos, t_added */ # define PRINT_FIELD(_ivl_field, _field_qualif, _is_time) \ do { \ if (ivl->intval._ivl_field) { \ if (_is_time && !t_added) { \ dest[pos ++] = 'T'; \ t_added = TRUE; \ } \ if (ivl->interval_sign) { \ dest[pos ++] = '-'; \ } \ pos += ui64tot(ivl->intval._ivl_field, dest + pos, \ /*wide*/FALSE); \ dest[pos ++] = _field_qualif; \ } \ } while (0) pos = 0; dest[pos ++] = 'P'; switch (ivl->interval_type) { case SQL_IS_YEAR: case SQL_IS_MONTH: case SQL_IS_YEAR_TO_MONTH: PRINT_FIELD(year_month.year, 'Y', /* is time comp. */FALSE); PRINT_FIELD(year_month.month, 'M', /* is time comp. */FALSE); if (pos <= /*leading 'P'*/1) { /* 0 interval */ dest[pos ++] = '0'; dest[pos ++] = 'M'; } break; case SQL_IS_DAY: case SQL_IS_HOUR: case SQL_IS_MINUTE: case SQL_IS_SECOND: case SQL_IS_DAY_TO_HOUR: case SQL_IS_DAY_TO_MINUTE: case SQL_IS_DAY_TO_SECOND: case SQL_IS_HOUR_TO_MINUTE: case SQL_IS_HOUR_TO_SECOND: case SQL_IS_MINUTE_TO_SECOND: PRINT_FIELD(day_second.day, 'D', /* is time comp. */FALSE); t_added = FALSE; PRINT_FIELD(day_second.hour, 'H', /*is time*/TRUE); PRINT_FIELD(day_second.minute, 'M', /*is time*/TRUE); if (ivl->intval.day_second.second | ivl->intval.day_second.fraction) { if (! t_added) { dest[pos ++] = 'T'; } if (ivl->interval_sign) { dest[pos ++] = '-'; } res = print_interval_sec(rec, ivl, dest + pos, /*wide*/FALSE); if (res <= 0) { return 0; } else { pos += res; } dest[pos ++] = 'S'; } if (pos <= /*leading 'P'*/1) { /* 0 interval */ dest[pos ++] = 'T'; dest[pos ++] = '0'; dest[pos ++] = 'S'; } break; default: /* an error if user-provided interval buffer is incorrect */ ERRH(stmt, "unexpected interval type %d.", ivl->interval_type); return 0; } DBGH(stmt, "interval of type %d printed as [%zu] `" LCPDL "`.", ivl->interval_type, pos, pos, dest); return pos; # undef PRINT_FIELD } /* translate the string representation of an interval value from the ISO8601 * to SQL */ static SQLRETURN interval_iso8601_to_sql(esodbc_rec_st *arec, esodbc_rec_st *irec, const wchar_t *wstr, size_t *chars_0, wchar_t *lit) { esodbc_stmt_st *stmt = arec->desc->hdr.stmt; SQLRETURN ret; wstr_st ivl_wstr; SQL_INTERVAL_STRUCT ivl; size_t cnt; ivl_wstr.str = (SQLWCHAR *)wstr; ivl_wstr.cnt = *chars_0 - 1; ret = parse_interval_iso8601(irec, irec->es_type->data_type, &ivl_wstr, &ivl); if (! SQL_SUCCEEDED(ret)) { return ret; } cnt = print_interval_sql(irec, &ivl, (SQLWCHAR *)lit); if (cnt <= 0) { ERRH(stmt, "sql interval printing failed for ISO8601`" LWPDL "`.", chars_0 - 1, wstr); RET_HDIAG(stmt, SQL_STATE_HY000, "internal printing failed", 0); } DBGH(arec->desc->hdr.stmt, "convered `" LWPDL "` to `" LWPDL "`.", chars_0 - 1, wstr, cnt, lit); lit[cnt ++] = '\0'; *chars_0 = cnt; return SQL_SUCCESS; } /* copy and truncate received DATE formatted as ES' DATETIME */ static SQLRETURN translate_date_iso8601_to_sql(esodbc_rec_st *arec, esodbc_rec_st *irec, const wchar_t *wstr, size_t *chars_0, wchar_t *date) { esodbc_stmt_st *stmt = HDRH(arec->desc)->stmt; if (*chars_0 - 1 < DATE_TEMPLATE_LEN) { ERRH(stmt, "string `" LWPDL "` is not a date.", *chars_0 - 1, wstr); RET_HDIAG(stmt, SQL_STATE_HY000, "Invalid server answer", 0); } wmemcpy(date, wstr, DATE_TEMPLATE_LEN); date[DATE_TEMPLATE_LEN] = 0; /* keep the _0 in chars_0 */ *chars_0 = DATE_TEMPLATE_LEN + 1; return SQL_SUCCESS; } /* Print timestamp, 0-terminated (printf'd, so \0 not counted). * Function will assume that 'dest' is at least ISO8601_TIMESTAMP_MAX_LEN + 1 * characters long. */ static int print_timestamp(TIMESTAMP_STRUCT *tss, BOOL iso8601, SQLULEN colsize, SQLSMALLINT decdigits, wchar_t *dest) { int n; size_t lim; const wchar_t *fmt; SQLUINTEGER nsec; /* "fraction" */ const static wchar_t *fmt_millis = L"%04d-%02d-%02d %02d:%02d:%02d.%.*lu"; const static wchar_t *fmt_nomillis = L"%04d-%02d-%02d %02d:%02d:%02d"; /* see c2sql_date_time() for an explanation of these values */ assert((! colsize) || (colsize == 16 || colsize == 19 || 20 < colsize)); lim = colsize ? colsize : TIMESTAMP_TEMPLATE_LEN(ESODBC_MAX_SEC_PRECISION); nsec = tss->fraction; if (0 < decdigits) { assert(decdigits <= ESODBC_MAX_SEC_PRECISION); nsec /= (SQLUINTEGER)pow10(ESODBC_MAX_SEC_PRECISION - decdigits); /* only print millis if they are non-null */ fmt = nsec ? fmt_millis : fmt_nomillis; } else { fmt = fmt_nomillis; } /* swprintf and now (=14.15.26706) also _snwprintf() both fail instead of * truncating, despite the documentation indicating otherwise => give full * buff length as limit and cut it short afterwards */ n = _snwprintf(dest, ISO8601_TIMESTAMP_MAX_LEN + /*\0*/1, fmt, tss->year, tss->month, tss->day, tss->hour, tss->minute, tss->second, /* fraction is always provided, but only printed if 'decdigits' */ decdigits, nsec); if (n <= 0) { return n; } if ((int)lim < n) { n = (int)lim; } if (iso8601) { dest[DATE_TEMPLATE_LEN] = L'T'; /* The SQL column sizes are considered for ISO format too, to * allow the case where the client app specifies a timestamp with * non-zero seconds, but wants to cut those away in the parameter. * The 'Z' would then be on top of the colsize. */ dest[n] = L'Z'; n ++; } dest[n] = L'\0'; DBG("printed UTC %s timestamp (colsz: %lu, decdig: %hd): " "[%d] `" LWPDL "`.", iso8601 ? "ISO8601" : "SQL", (SQLUINTEGER)colsize, decdigits, n, n, dest); return n; } /* transform an ISO8601 timestamp str. to SQL/ODBC timestamp str. */ static SQLRETURN translate_timestamp_iso8601_to_sql(esodbc_rec_st *arec, esodbc_rec_st *irec, const wchar_t *wstr, size_t *chars_0, wchar_t *timestamp) { esodbc_stmt_st *stmt = HDRH(arec->desc)->stmt; esodbc_dbc_st *dbc = HDRH(stmt)->dbc; xstr_st xstr; TIMESTAMP_STRUCT tss; int n; SQLRETURN ret; if (dbc->apply_tz) { /* sql2c only */ xstr.wide = TRUE; xstr.w.str = (SQLWCHAR *)wstr; xstr.w.cnt = *chars_0 - 1; ret = parse_iso8601_timestamp(stmt, &xstr, /*to UTC*/FALSE, &tss); if (! SQL_SUCCEEDED(ret)) { return ret; } /* colsize=0: the translated timestamp will be as long as the * ES SQL/ISO size */ n = print_timestamp(&tss, /*ISO?*/FALSE, /*colsize*/0, ESODBC_DEF_SEC_PRECISION, timestamp); if (n <= 0) { ERRNH(stmt, "failed to print TZ-adjusted TIMESTAMP."); RET_HDIAGS(stmt, SQL_STATE_HY000); } *chars_0 = (size_t)n + /*count \0*/1; } else { n = (int)*chars_0 - 1; if (n < ISO8601_TIMESTAMP_MIN_LEN) { ERRH(stmt, "string `" LWPDL "` is not a DATETIME.", n, wstr); RET_HDIAG(stmt, SQL_STATE_HY000, "Invalid server answer", 0); } wmemcpy(timestamp, wstr, n); timestamp[DATE_TEMPLATE_LEN] = L' '; timestamp[-- n] = L'\0'; *chars_0 = (size_t)n + 1; } return SQL_SUCCESS; } /* transform an ISO8601/ES time str. to SQL/ODBC timestamp str. */ static SQLRETURN translate_time_iso8601_to_sql(esodbc_rec_st *arec, esodbc_rec_st *irec, const wchar_t *wstr, size_t *chars_0, /*out*/wchar_t *time) { esodbc_stmt_st *stmt = HDRH(arec->desc)->stmt; esodbc_dbc_st *dbc = HDRH(stmt)->dbc; int cnt; xstr_st xstr; SQLRETURN res; TIMESTAMP_STRUCT tss; SQLSMALLINT fmt; cnt = (int)*chars_0 - 1; /* len w/o the \0 */ if (cnt < TIME_TEMPLATE_LEN(0)) { ERRH(stmt, "`" LWPDL "` not an ISO8601 TIME value.", cnt, wstr); RET_HDIAG(stmt, SQL_STATE_HY000, "Invalid server answer", 0); } /* if time localization OR the received TIME is not in UTC (ex. * `:ss+02:00`), normalize the format: bring to UTC and apply time offset, * if needed (i.e. apply_tz==TRUE). */ if (dbc->apply_tz || wstr[cnt - 1] != L'Z') { xstr.wide = TRUE; xstr.w.str = (SQLWCHAR *)wstr; xstr.w.cnt = cnt; res = parse_date_time_ts(stmt, &xstr, /*sql2c*/TRUE, &tss, &fmt); if (! SQL_SUCCEEDED(res)) { return res; } assert(fmt == SQL_TYPE_TIME); cnt = print_timestamp(&tss, /*ISO?*/FALSE, /*colsz*/0, ESODBC_DEF_SEC_PRECISION, time); if (cnt <= 0) { ERRNH(stmt, "failed to print TZ-adjusted TIME(STAMP)."); RET_HDIAGS(stmt, SQL_STATE_HY000); } cnt -= DATE_TEMPLATE_LEN + /*' '*/1; wmemmove(time, time + DATE_TEMPLATE_LEN + /*' '*/1, cnt + /*\0*/1); *chars_0 = cnt + 1; } else { /* TIME in usable format already */ cnt --; /* strip trailing 'Z' */ wmemcpy(time, wstr, cnt); time[cnt] = L'\0'; *chars_0 = cnt + 1; } return SQL_SUCCESS; } static SQLRETURN wstr_to_string(esodbc_rec_st *arec, esodbc_rec_st *irec, void *data_ptr, SQLLEN *octet_len_ptr, const wchar_t *wstr, size_t chars_0) { esodbc_stmt_st *stmt = arec->desc->hdr.stmt; wchar_t buff[INTERVAL_VAL_MAX_LEN + /*0*/1]; SQLRETURN ret; SQLSMALLINT arec_type; esodbc_state_et state; size_t octets, usize, min_xfer; /* to make use of the buffer for date_time transformations */ assert(ISO8601_TIMESTAMP_MAX_LEN <= INTERVAL_VAL_MAX_LEN); arec_type = get_rec_c_type(arec, irec); min_xfer = 0; /* The time data types - intervals, date, timestamp/"datetime" - are * received from ES in ISO8601, not SQL format and need translation. This * is done in the local 'buff'er, then copied out as the required * character type string. */ do { if (irec->type == SQL_INTERVAL) { ret = interval_iso8601_to_sql(arec, irec, wstr, &chars_0, buff); } else if (irec->type == SQL_DATETIME) { switch (irec->concise_type) { case SQL_TYPE_DATE: /* Note: this call could be optimized (to avoid the extra * copy) by directly modifying the wstr/JSON OR having * the below called wstr_to_Xstr() no longer expect the \0 * counted. */ ret = translate_date_iso8601_to_sql(arec, irec, wstr, &chars_0, buff); min_xfer = DATE_TEMPLATE_LEN + /*\0*/1; break; case SQL_TYPE_TIMESTAMP: ret = translate_timestamp_iso8601_to_sql(arec, irec, wstr, &chars_0, buff); min_xfer = TIMESTAMP_TEMPLATE_LEN(0) + /*\0*/1; break; case SQL_TYPE_TIME: ret = translate_time_iso8601_to_sql(arec, irec, wstr, &chars_0, buff); min_xfer = TIME_TEMPLATE_LEN(0) + /*\0*/1; break; default: BUGH(stmt, "unexpected concise type for datetime."); RET_HDIAG(stmt, SQL_STATE_HY000, "bug encoding type", 0); } } else { break; /* no translation needed */ } if (! SQL_SUCCEEDED(ret)) { return ret; } else { wstr = buff; DBGH(stmt, "time value translated to [%zu]:`" LWPDL "`.", chars_0 - 1, chars_0 - 1, wstr); } } while (0); ret = (arec_type == SQL_C_CHAR) ? wstr_to_cstr(arec, irec, data_ptr, octet_len_ptr, wstr, chars_0) : wstr_to_wstr(arec, irec, data_ptr, octet_len_ptr, wstr, chars_0); /* if truncation occured, only succeed if fractional seconds are cut out */ if (ret == SQL_SUCCESS_WITH_INFO && min_xfer && HDRH(stmt)->diag.state == SQL_STATE_01004) { assert(SQL_SUCCEEDED(ret)); usize = (arec_type == SQL_C_CHAR) ? sizeof(SQLCHAR) : sizeof(SQLWCHAR); assert((state = SQL_STATE_00000) == SQL_STATE_00000); /* dbg only */ octets = buff_octet_size(chars_0 * usize, usize, arec, irec, &state); assert(state == SQL_STATE_01004); if (octets/usize < min_xfer) { ERRH(stmt, "buffer too small (%zu/%zu) for val [%zu] `" LWPDL "`.", octets, usize, chars_0 - 1, chars_0 -1, wstr); RET_HDIAGS(stmt, SQL_STATE_22003); } } return ret; } /* apply early truncation to the source data, to the limit enforced by the * varchar limit; many functions downstream expect the 0-terminator in place, * so an update of the count only would not suffice. */ void varchar_limit_apply(esodbc_rec_st *irec, wchar_t *wstr, size_t *chars_0) { esodbc_stmt_st *stmt = HDRH(irec->desc)->stmt; SQLUINTEGER varchar_limit = HDRH(stmt)->dbc->varchar_limit; if (varchar_limit <= 0) { return; } if (*chars_0 - /*\0*/1 <= varchar_limit) { return; } DBGH(stmt, "applying varchar limit truncation: %zu -> [%lu] `" LWPDL "`.", *chars_0 - 1, varchar_limit, varchar_limit, wstr); *chars_0 = varchar_limit + 1; wstr[varchar_limit] = L'\0'; } /* * wstr: is 0-terminated and terminator is counted in 'chars_0'. * However: "[w]hen C strings are used to hold character data, the * null-termination character is not considered to be part of the data and is * not counted as part of its byte length." * "If the data was converted to a variable-length data type, such as * character or binary [...][i]t then null-terminates the data." */ SQLRETURN sql2c_string(esodbc_rec_st *arec, esodbc_rec_st *irec, SQLULEN pos, const wchar_t *wstr, size_t chars_0) { esodbc_stmt_st *stmt; void *data_ptr; SQLLEN *octet_len_ptr; SQLSMALLINT ctarget; long long ll; unsigned long long ull; wstr_st wval; double dbl; SQLWCHAR *endp; stmt = arec->desc->hdr.stmt; /* function updates the const wstr; this is however not the * originally received network buffer, but a re-allocated chunk by the * respective parsing library (json/cbor), so it should be safe; */ varchar_limit_apply(irec, (wchar_t *)wstr, &chars_0); assert(1 <= chars_0); /* _0 is really counted */ /* pointer where to write how many characters we will/would use */ octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); /* pointer to app's buffer */ data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); ctarget = get_rec_c_type(arec, irec); switch (ctarget) { case SQL_C_CHAR: case SQL_C_BINARY: /* treat binary as WCHAR */ // TODO: add \0??? case SQL_C_WCHAR: return wstr_to_string(arec, irec, data_ptr, octet_len_ptr, wstr, chars_0); case SQL_C_TYPE_TIMESTAMP: return wstr_to_timestamp_struct(arec, irec, data_ptr, octet_len_ptr, wstr, chars_0, NULL); case SQL_C_TYPE_DATE: return wstr_to_date_struct(arec, irec, data_ptr, octet_len_ptr, wstr, chars_0); case SQL_C_TYPE_TIME: return wstr_to_time_struct(arec, irec, data_ptr, octet_len_ptr, wstr, chars_0); } wval = (wstr_st) { (SQLWCHAR *)wstr, chars_0 - 1 }; /* trim any white spaces */ wtrim_ws(&wval); switch (ctarget) { case SQL_C_TINYINT: case SQL_C_STINYINT: case SQL_C_SHORT: case SQL_C_SSHORT: case SQL_C_LONG: case SQL_C_SLONG: case SQL_C_SBIGINT: /* convert to integer type */ errno = 0; if (str2bigint(&wval, /*wide?*/TRUE, (SQLBIGINT *)&ll, TRUE) < 0) { ERRH(stmt, "can't convert `" LWPD "` to long long.", wstr); RET_HDIAGS(stmt, errno == ERANGE ? SQL_STATE_22003 : SQL_STATE_22018); } DBGH(stmt, "string `" LWPD "` converted to LL=%lld.", wstr, ll); /* delegate to existing functionality */ return sql2c_longlong(arec, irec, pos, ll); case SQL_C_UTINYINT: case SQL_C_USHORT: case SQL_C_ULONG: case SQL_C_UBIGINT: /* convert to integer type */ errno = 0; if (str2ubigint(&wval, /*wide?*/TRUE, (SQLUBIGINT *)&ull, /*strict*/TRUE) <= 0) { ERRH(stmt, "can't convert `" LWPD "` to unsigned long long.", wstr); RET_HDIAGS(stmt, errno == ERANGE ? SQL_STATE_22003 : SQL_STATE_22018); } DBGH(stmt, "string `" LWPD "` converted to ULL=%llu.", wstr, ull); if (ull <= LLONG_MAX) { /* the cast is safe, delegate to existing functionality */ return sql2c_longlong(arec, irec, pos, (long long)ull); } /* value is larger than what long long can hold: can only convert * to SQLUBIGINT (and SQLULONG, if it has the same size), or fail * as out-of-range */ assert(sizeof(SQLUBIGINT) == sizeof(unsigned long long)); if ((ctarget == SQL_C_UBIGINT) || (ctarget == SQL_C_ULONG && sizeof(SQLUINTEGER) == sizeof(SQLUBIGINT))) { /* write out the converted value */ REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); *(SQLUBIGINT *)data_ptr = (SQLUBIGINT)ull; write_out_octets(octet_len_ptr, sizeof(SQLUBIGINT), irec); DBGH(stmt, "converted string `" LWPD "` to " "unsigned long long %llu.", wstr, ull); } else { REJECT_AS_OOR(stmt, ull, /*fixed?*/TRUE, "non-ULL"); } break; case SQL_C_FLOAT: case SQL_C_DOUBLE: case SQL_C_NUMERIC: case SQL_C_BIT: /* convert to double */ errno = 0; dbl = wcstod((wchar_t *)wval.str, (wchar_t **)&endp); DBGH(stmt, "string `" LWPD "` converted to dbl=%.6e.", wstr, dbl); /* if empty string, non-numeric or under/over-flow, bail out */ if ((! wval.cnt) || (wval.str + wval.cnt != endp) || errno) { ERRH(stmt, "can't convert `" LWPD "` to double.", wstr); RET_HDIAGS(stmt, errno == ERANGE ? SQL_STATE_22003 : SQL_STATE_22018); } /* delegate to existing functionality */ return sql2c_double(arec, irec, pos, dbl); case SQL_C_INTERVAL_YEAR: case SQL_C_INTERVAL_MONTH: case SQL_C_INTERVAL_DAY: case SQL_C_INTERVAL_HOUR: case SQL_C_INTERVAL_MINUTE: case SQL_C_INTERVAL_SECOND: case SQL_C_INTERVAL_YEAR_TO_MONTH: case SQL_C_INTERVAL_DAY_TO_HOUR: case SQL_C_INTERVAL_DAY_TO_MINUTE: case SQL_C_INTERVAL_DAY_TO_SECOND: case SQL_C_INTERVAL_HOUR_TO_MINUTE: case SQL_C_INTERVAL_HOUR_TO_SECOND: case SQL_C_INTERVAL_MINUTE_TO_SECOND: REJECT_IF_NULL_DEST_BUFF(stmt, data_ptr); write_out_octets(octet_len_ptr, sizeof(SQLUBIGINT), irec); DBGH(stmt, "source for interval: [%zu] `" LWPDL "` as " LWPDL ".", wval.cnt, LWSTR(&wval), LWSTR(&irec->es_type->type_name)); return sql2c_interval(arec, irec->es_type->data_type, ctarget, data_ptr, &wval); default: BUGH(stmt, "unexpected unhandled data type: %d.", ctarget); RET_HDIAG(stmt, SQL_STATE_HY000, "Unexpected application data " "type for source of type string", 0); } return SQL_SUCCESS; # undef INTERVAL_ISO8601_TO_SQL } /* TODO: implementation for the below */ static inline BOOL conv_implemented(SQLSMALLINT sqltype, SQLSMALLINT ctype) { switch (ctype) { case SQL_C_GUID: // case SQL_C_TYPE_TIMESTAMP_WITH_TIMEZONE: // case SQL_C_TYPE_TIME_WITH_TIMEZONE: return FALSE; } switch (sqltype) { case SQL_GUID: // case SQL_TYPE_TIMESTAMP_WITH_TIMEZONE: // case SQL_TYPE_TIME_WITH_TIMEZONE: return FALSE; } return TRUE; } /* 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) { SQLINTEGER i, start, stop; esodbc_desc_st *axd, *ixd; esodbc_rec_st *arec, *irec; assert(idx); if (idx < 0) { /* * bound columns check */ assert(stmt->hdr.dbc->es_types); assert(STMT_HAS_RESULTSET(stmt)); axd = stmt->ard; ixd = stmt->ird; /* 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 { /* * binding paramter check */ start = idx - 1; stop = idx; axd = stmt->apd; ixd = stmt->ipd; } for (i = start; i < stop; i ++) { assert(i < axd->count); arec = &axd->recs[i]; if ((idx < 0) && (! REC_IS_BOUND(arec))) { /* skip not bound columns */ continue; } assert(i < ixd->count); irec = &ixd->recs[i]; assert(arec && irec); if (! ESODBC_TYPES_COMPATIBLE(irec->concise_type,arec->concise_type)) { ERRH(stmt, "conversion not possible on ordinal #%d: IRD: %hd, " "ARD: %hd.", i + 1, irec->concise_type, arec->concise_type); if (conv_code) { *conv_code = CONVERSION_VIOLATION; } RET_HDIAGS(stmt, SQL_STATE_07006); } if (! conv_implemented(irec->concise_type, arec->concise_type)) { ERRH(stmt, "conversion not supported on ordinal #%d : IRD: %hd, " "ARD: %hd.", i + 1, irec->concise_type, arec->concise_type); if (conv_code) { *conv_code = CONVERSION_UNSUPPORTED; } RET_HDIAGS(stmt, SQL_STATE_HYC00); } } if (conv_code) { *conv_code = CONVERSION_SUPPORTED; } DBGH(stmt, "convertibility check: OK."); return SQL_SUCCESS; } /* Converts a C/W-string to a u/llong or double; the xstr->wide must be set. * Returns success of conversion, converted value, its type and trimmed string * source. */ static BOOL xstr_to_number(esodbc_stmt_st *stmt, void *data_ptr, SQLLEN *octet_len_ptr, xstr_st *xstr, t_number_st *dest) { int res; void *xptr; /* "If StrLen_or_IndPtr is a null pointer, the driver assumes that all * input parameter values are non-NULL and that character and binary data * is null-terminated." */ if (xstr->wide) { xstr->w.str = (SQLWCHAR *)data_ptr; if ((! octet_len_ptr) || (*octet_len_ptr == SQL_NTSL)) { xstr->w.cnt = wcslen(xstr->w.str); } else { xstr->w.cnt = (size_t)*octet_len_ptr / sizeof(*xstr->w.str); } } else { xstr->c.str = (SQLCHAR *)data_ptr; if ((! octet_len_ptr) || (*octet_len_ptr == SQL_NTSL)) { xstr->c.cnt = strlen(xstr->c.str); } else { xstr->c.cnt = (size_t)*octet_len_ptr; } } if (! dest) { return TRUE; } if (xstr->wide) { wtrim_ws(&xstr->w); xptr = &xstr->w; DBGH(stmt, "converting paramter value `" LWPDL "` to number.", LWSTR(&xstr->w)); } else { trim_ws(&xstr->c); xptr = &xstr->c; DBGH(stmt, "converting paramter value `" LCPDL "` to number.", LCSTR(&xstr->c)); } /* try to parse the number sequentially as long long, unsigned long long * and finally double */ dest->type = SQL_C_SBIGINT; res = str2bigint(xptr, xstr->wide, &dest->bint, /*strict?*/TRUE); if (res < 0) { dest->type = SQL_C_UBIGINT; res = str2ubigint(xptr, xstr->wide, &dest->ubint, /*strict?*/TRUE); if (res < 0) { dest->type = SQL_C_DOUBLE; res = str2double(xptr, xstr->wide, &dest->dbl, /*strct?*/TRUE); } } if (res < 0) { if (xstr->wide) { ERRH(stmt, "can't convert `" LWPDL "` to a number.", LWSTR(&xstr->w)); } else { ERRH(stmt, "can't convert `" LCPDL "` to a number.", LCSTR(&xstr->c)); } return FALSE; } DBGH(stmt, "parameter value converted as type: %hd.", dest->type); return TRUE; } SQLRETURN c2sql_null(esodbc_rec_st *arec, esodbc_rec_st *irec, char *dest, size_t *len) { if (dest) { memcpy(dest, JSON_VAL_NULL, sizeof(JSON_VAL_NULL) - /*\0*/1); } *len = sizeof(JSON_VAL_NULL) - /*\0*/1; return SQL_SUCCESS; } static SQLRETURN double_to_bool(esodbc_stmt_st *stmt, double dbl, BOOL *val) { DBGH(stmt, "converting double %.6e to bool.", dbl); #ifdef BOOLEAN_IS_BIT if (dbl < 0. && 2. < dbl) { ERRH(stmt, "double %.6e out of range.", dbl); RET_HDIAGS(stmt, SQL_STATE_22003); } if (0. < dbl && dbl < 2. && dbl != 1.) { /* it's a failure, since SUCCESS_WITH_INFO would be returned only * after data is sent to the server. */ ERRH(stmt, "double %.6e right truncated.", dbl); RET_HDIAGS(stmt, SQL_STATE_22003); } #endif /* BOOLEAN_IS_BIT */ *val = dbl != 0.; return SQL_SUCCESS; } SQLRETURN c2sql_boolean(esodbc_rec_st *arec, esodbc_rec_st *irec, SQLULEN pos, char *dest, size_t *len) { BOOL val; SQLSMALLINT ctype; SQLRETURN ret; void *data_ptr; SQLLEN *octet_len_ptr; xstr_st xstr; SQLDOUBLE dbl; t_number_st xnr; esodbc_stmt_st *stmt = arec->desc->hdr.stmt; if (! dest) { /* return the "worst case" len (and only convert data at copy time) */ *len = sizeof(JSON_VAL_FALSE) - 1; return SQL_SUCCESS; } /* pointer to app's buffer */ data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); /*INDENT-OFF*/ switch ((ctype = get_rec_c_type(arec, irec))) { case SQL_C_CHAR: case SQL_C_WCHAR: /* pointer to read from how many bytes we have */ octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); xstr.wide = ctype == SQL_C_WCHAR; if (! xstr_to_number(stmt, data_ptr, octet_len_ptr, &xstr, &xnr)) { RET_HDIAGS(stmt, SQL_STATE_22018); } switch (xnr.type) { case SQL_C_SBIGINT: dbl = (double)xnr.bint; break; case SQL_C_UBIGINT: dbl = (double)xnr.ubint; break; case SQL_C_DOUBLE: dbl = xnr.dbl; break; default: BUGH(stmt, "unexpected number type: %hd.", xnr.type); RET_HDIAGS(stmt, SQL_STATE_HY000); } ret = double_to_bool(stmt, dbl, &val); if (! SQL_SUCCEEDED(ret)) { return ret; } break; do { case SQL_C_BINARY: /* pointer to read from how many bytes we have */ octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); if (! octet_len_ptr) { if (((char *)data_ptr)[0]) { RET_HDIAGS(stmt, SQL_STATE_22003); } } else if (*octet_len_ptr != sizeof(SQLCHAR)) { RET_HDIAGS(stmt, SQL_STATE_22003); } /* no break */ case SQL_C_BIT: case SQL_C_UTINYINT: dbl = (double)*(SQLCHAR *)data_ptr; break; case SQL_C_TINYINT: case SQL_C_STINYINT: dbl = (double)*(SQLSCHAR *)data_ptr; break; case SQL_C_SHORT: case SQL_C_SSHORT: dbl = (double)*(SQLSMALLINT *)data_ptr; break; case SQL_C_USHORT: dbl = (double)*(SQLUSMALLINT *)data_ptr; break; case SQL_C_LONG: case SQL_C_SLONG: dbl = (double)*(SQLINTEGER *)data_ptr; break; case SQL_C_ULONG: dbl = (double)*(SQLUINTEGER *)data_ptr; break; case SQL_C_SBIGINT: dbl = (double)*(SQLBIGINT *)data_ptr; break; case SQL_C_UBIGINT: dbl = (double)*(SQLUBIGINT *)data_ptr; break; case SQL_C_FLOAT: dbl = (double)*(SQLREAL *)data_ptr; break; case SQL_C_DOUBLE: dbl = (double)*(SQLDOUBLE *)data_ptr; break; case SQL_C_NUMERIC: // TODO: better? *(uul *)val[0] != 0 && *[uul *]val[8] != 0 */ ret = numeric_to_double(irec, data_ptr, &dbl); if (! SQL_SUCCEEDED(ret)) { return ret; } break; } while (0); ret = double_to_bool(stmt, dbl, &val); if (! SQL_SUCCEEDED(ret)) { return ret; } break; //case SQL_C_BOOKMARK: //case SQL_C_VARBOOKMARK: default: BUGH(stmt, "can't convert SQL C type %hd to boolean.", get_rec_c_type(arec, irec)); RET_HDIAG(stmt, SQL_STATE_HY000, "bug converting parameter", 0); } /*INDENT-ON*/ DBGH(stmt, "parameter (pos#%llu) converted to boolean: %d.", (uint64_t)pos, val); if (val) { memcpy(dest, JSON_VAL_TRUE, sizeof(JSON_VAL_TRUE) - /*\0*/1); *len = sizeof(JSON_VAL_TRUE) - 1; } else { memcpy(dest, JSON_VAL_FALSE, sizeof(JSON_VAL_FALSE) - /*\0*/1); *len = sizeof(JSON_VAL_FALSE) - 1; } return SQL_SUCCESS; } /* SQL_BIGINT will be provided for both signed (SQL_C_SBIGINT) and unsigned * (SQL_C_UBIGINT) types. SQLBindParameter()'s size and decimal digits * parameters aren't relevant when parameter type is SQL_BIGINT. Which means * that setting the LONG vs UNSIGNED_LONG ES type can only be value-dependent. * ES will convert whatever value is given according to the provided type, so * this needs to be set right by the driver. */ void irec_promote_bigint_type(esodbc_rec_st *irec, t_number_st *tnr) { esodbc_stmt_st *stmt; if (irec->es_type->data_type != SQL_BIGINT) { return; } switch (tnr->type) { default: return; case SQL_C_UBIGINT: if (tnr->ubint <= LLONG_MAX) { return; } break; case SQL_C_DOUBLE: /* only convert doubles that won't fit in a long long, but will in * an unsigned long long */ if (tnr->dbl <= LLONG_MAX || ULLONG_MAX < tnr->dbl) { return; } break; } stmt = irec->desc->hdr.stmt; irec->es_type = stmt->hdr.dbc->ulong; DBGH(stmt, "number type %hd promoted to UBIGINT.", tnr->type); } /* * Copies a C/W-string representing a number out to the send buffer. So no * string->number->string conversion is performed, any needed conversion is * delegated to ES (which will for instance accept a float value with interer * type or the other way around). Range and precision checks are performed. * wide: type of string; * min, max: target SQL numeric type's limits; * dest: buffer's pointer; can be null (when eval'ing) needed space; * len: how much of the buffer is needed or has been used. */ static SQLRETURN string_to_number(esodbc_rec_st *arec, esodbc_rec_st *irec, SQLULEN pos, BOOL wide, t_number_st *min, t_number_st *max, char *dest, size_t *len) { void *data_ptr, *src; size_t src_len; SQLLEN *octet_len_ptr; xstr_st xstr; t_number_st xnr; SQLDOUBLE abs_dbl; int ret; BOOL fixed; SQLWCHAR wbuff[ESODBC_PRECISION_UINT64 + /*`+`*/1]; SQLCHAR buff[ESODBC_PRECISION_UINT64 + /*`+`*/1]; esodbc_stmt_st *stmt = arec->desc->hdr.stmt; /* pointer to app's buffer */ data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); /* pointer to read from how many bytes we have */ octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); xstr.wide = wide; if (! xstr_to_number(stmt, data_ptr, octet_len_ptr, &xstr, &xnr)) { ERRH(stmt, "failed to convert param value to a number."); RET_HDIAGS(stmt, SQL_STATE_22018); } irec_promote_bigint_type(irec, &xnr); if (! dest) { *len = wide ? xstr.w.cnt : xstr.c.cnt; return SQL_SUCCESS; } src = data_ptr; src_len = wide ? xstr.w.cnt : xstr.c.cnt; /* check against truncation, limits and any given precision */ if (min && max) { fixed = min->type != SQL_C_DOUBLE; /* TODO: spec requires 22001 instead of 22003 for out of bounds * conversion, but that seems incorrect? */ switch (xnr.type) { case SQL_C_SBIGINT: if (fixed && (xnr.bint < min->bint || (xnr.bint > 0 && max->ubint < (SQLUBIGINT)xnr.bint))) { ERRH(stmt, "converted long long %lld out of bounds " "[%ld, %llu].", xnr.bint, min->bint, max->ubint); RET_HDIAGS(stmt, SQL_STATE_22003); } break; case SQL_C_UBIGINT: if (fixed && max->ubint < xnr.ubint) { ERRH(stmt, "converted unsigned long long %llu out of " "bounds [0, %llu].", xnr.ubint, max->ubint); RET_HDIAGS(stmt, SQL_STATE_22003); } break; case SQL_C_DOUBLE: abs_dbl = xnr.dbl < 0 ? -xnr.dbl : xnr.dbl; if (fixed) { if (xnr.dbl < min->bint || max->ubint < xnr.dbl) { ERRH(stmt, "converted double %.6e out of bounds " "[0/%lld, %llu].", xnr.dbl, min->bint, max->ubint); RET_HDIAGS(stmt, SQL_STATE_22003); } if (0 < abs_dbl - (SQLUBIGINT)abs_dbl) { ERRH(stmt, "conversion of double %.6e to fixed would" " truncate fractional digits", xnr.dbl); RET_HDIAGS(stmt, SQL_STATE_22001); } /* xxx_MAX limits will be rounded up when cast to double, * the comparisons won't be "accurate" -> numbers higher * than long long could be sent as long and higher than * unsigned long long as such => cast to fixed point and * serialize the result. */ src = wide ? (void *)wbuff : (void *)buff; src_len = irec->es_type == stmt->hdr.dbc->ulong ? ui64tot((SQLUBIGINT)xnr.dbl, src, wide) : i64tot((SQLBIGINT)xnr.dbl, src, wide); if (src_len < 0) { ERRNH(stmt, "failed to convert %.6e to its fixed point" " string representation.", xnr.dbl); RET_HDIAGS(stmt, SQL_STATE_HY000); } assert(0 < src_len); } else { /* string_double -> floating point */ if (abs_dbl < min->dbl || max->dbl < abs_dbl) { ERRH(stmt, "converted double double %.6e out of bounds" " [%.6e, %.6e].", abs_dbl, min->dbl, max->dbl); RET_HDIAGS(stmt, SQL_STATE_22003); } } break; default: BUGH(stmt, "unexpected number type: %hd.", xnr.type); RET_HDIAGS(stmt, SQL_STATE_HY000); } } // // TODO: check IRD precision against avail value? // /* copy values directly from app's buffer */ if (wide) { /* need a conversion to ascii */ ret = ascii_w2c((SQLWCHAR *)src, dest, src_len); assert(0 < ret); /* it converted to a number already */ } else { memcpy(dest, src, src_len); } *len = src_len; return SQL_SUCCESS; } /* signed fixed-point number source to (stringified) number destination */ static SQLRETURN sfixed_to_number(esodbc_stmt_st *stmt, SQLBIGINT src, t_number_st *min, t_number_st *max, char *dest, size_t *len) { if (! dest) { /* largest space it could occupy */ *len = ESODBC_PRECISION_INT64; return SQL_SUCCESS; } DBGH(stmt, "converting paramter value %lld as number.", src); if (min && max) { if (min->type != SQL_C_DOUBLE) { /* target number is fixed type */ if (src < min->bint || (src > 0 && max->ubint < (SQLUBIGINT)src)) { ERRH(stmt, "source value %lld out of range [%lld, %llu] for " "dest type", src, min->bint, max->ubint); RET_HDIAGS(stmt, SQL_STATE_22003); } } else { if ((SQLDOUBLE)src < min->dbl || max->dbl < (SQLDOUBLE)src) { ERRH(stmt, "source value %lld out of range [%e, %e] for dest " "type", src, min->dbl, max->dbl); RET_HDIAGS(stmt, SQL_STATE_22003); } } } assert(sizeof(SQLBIGINT) == sizeof(int64_t)); *len = i64tot(src, dest, /*wide?*/FALSE); assert(*len <= ESODBC_PRECISION_INT64); return SQL_SUCCESS; } /* unsigned fixed-point number source to (stringified) number destination */ static SQLRETURN ufixed_to_number(esodbc_rec_st *irec, SQLUBIGINT src, t_number_st *max, char *dest, size_t *len) { t_number_st tnr = (t_number_st){.ubint = src, .type = SQL_C_UBIGINT}; esodbc_stmt_st *stmt = irec->desc->hdr.stmt; irec_promote_bigint_type(irec, &tnr); if (! dest) { /* largest space it could occupy */ *len = ESODBC_PRECISION_UINT64; return SQL_SUCCESS; } DBGH(stmt, "converting paramter value %llu as number.", src); if (max) { if (max->type != SQL_C_DOUBLE) { /* target number is fixed type */ if (max->ubint < src) { ERRH(stmt, "source value %llu out of range [0, %llu] for dest " "type", src, max->ubint); RET_HDIAGS(stmt, SQL_STATE_22003); } } } assert(sizeof(SQLUBIGINT) == sizeof(int64_t)); *len = ui64tot(src, dest, /*wide?*/FALSE); assert(*len <= ESODBC_PRECISION_UINT64); return SQL_SUCCESS; } /* convert a floating point source to a "numeric" destination, which can be * any of the allowed SQL target type - numeric (fixed, floating, * decimal/numeric), string, binary - except boolean/bit. */ static SQLRETURN floating_to_number(esodbc_rec_st *irec, SQLDOUBLE src, t_number_st *min, t_number_st *max, char *dest, size_t *len) { SQLSMALLINT prec; SQLULEN colsize; int cnt; SQLDOUBLE abs_src; t_number_st tnr = (t_number_st){.dbl = src, .type = SQL_C_DOUBLE}; esodbc_stmt_st *stmt = irec->desc->hdr.stmt; irec_promote_bigint_type(irec, &tnr); if (! dest) { /* largest space it could occupy */ *len = DBL_BASE10_MAX_LEN + /*0-term, for printf*/1; return SQL_SUCCESS; } abs_src = src < 0 ? -src : src; if (min && max) { if (min->type != SQL_C_DOUBLE) { /* target number is fixed type */ if (src < min->bint || max->ubint < src) { ERRH(stmt, "source value %e out of range [%lld, %llu] for dest" " type", src, min->bint, max->ubint); RET_HDIAGS(stmt, SQL_STATE_22003); } } else { if (abs_src < min->dbl || max->dbl < abs_src) { ERRH(stmt, "source value %e out of range [%e, %e] for dest " "type", src, min->dbl, max->dbl); RET_HDIAGS(stmt, SQL_STATE_22003); } } } switch (irec->es_type->meta_type) { case METATYPE_EXACT_NUMERIC: case METATYPE_FLOAT_NUMERIC: assert(0 <= irec->es_type->maximum_scale); prec = irec->es_type->maximum_scale; break; case METATYPE_STRING: /* print at source's scale and trim at given size afterwards */ prec = ESODBC_DEF_FLOAT_PRECISION; break; default: BUGH(stmt, "unexpected IREC type %d.", irec->es_type->data_type); RET_HDIAGS(stmt, SQL_STATE_HY000); } DBGH(stmt, "converting double param %.*f with precision %hd.", prec, src, prec); cnt = snprintf(dest, DBL_BASE10_MAX_LEN + /*\0*/1, "%.*f", (int)prec, src); if (cnt < 0) { ERRH(stmt, "failed to print double %.*f with precision %hd.", prec, src, prec); RET_HDIAGS(stmt, SQL_STATE_HY000); } else { if (irec->es_type->meta_type == METATYPE_STRING) { /* can decimals be cut away? */ colsize = get_param_size(irec); if ((unsigned long long)abs_src < pow10((unsigned)colsize /* if source is negative, reserve space for it */ - (src < 0))) { *len = (SQLULEN)cnt < colsize ? cnt : colsize; /* trim trailing `.` */ if (dest[*len - 1] == '.') { /* TODO: i18 */ (*len) --; } } else { ERRH(stmt, "non-fractional truncation for [%d] %.*f on %hd " "column size.", cnt, prec, src, prec); RET_HDIAGS(stmt, SQL_STATE_22003); } } else { *len = cnt; } DBGH(stmt, "floating value printed as [%zu] `" LCPDL "`.", *len, *len, dest); } return SQL_SUCCESS; } static SQLRETURN binary_to_number(esodbc_rec_st *arec, esodbc_rec_st *irec, SQLULEN pos, char *dest, size_t *len) { void *data_ptr; SQLLEN *octet_len_ptr, osize /*octet~*/; SQLBIGINT llng; SQLDOUBLE dbl; esodbc_stmt_st *stmt = irec->desc->hdr.stmt; if (! dest) { if (irec->meta_type == METATYPE_EXACT_NUMERIC) { return sfixed_to_number(stmt, 0LL, NULL, NULL, NULL, len); } else { assert(irec->meta_type == METATYPE_FLOAT_NUMERIC); return floating_to_number(irec, 0., NULL, NULL, NULL, len); } } /* pointer to read from how many bytes we have */ octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); /* pointer to app's buffer */ data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); if (! octet_len_ptr) { if (0 < arec->octet_length) { osize = arec->octet_length; } else { /* "If [...] is a null pointer, the driver assumes [...] that * character and binary data is null-terminated." */ WARNH(stmt, "no length information provided for binary type: " "calculating it as a C-string!"); osize = strlen((char *)data_ptr); } } else { osize = *octet_len_ptr; } # define CHK_SIZES(_sqlc_type) \ do { \ if (osize != sizeof(_sqlc_type)) { \ ERRH(stmt, "binary data length (%zu) misaligned with target" \ " data type (%hd) size (%lld)", sizeof(_sqlc_type), \ irec->es_type->data_type, (int64_t)osize); \ RET_HDIAGS(stmt, SQL_STATE_HY090); \ } \ } while (0) # define BIN_TO_LLNG(_sqlc_type) \ do { \ CHK_SIZES(_sqlc_type); \ llng = (SQLBIGINT)*(_sqlc_type *)data_ptr; \ } while (0) # define BIN_TO_DBL(_sqlc_type) \ do { \ CHK_SIZES(_sqlc_type); \ dbl = (SQLDOUBLE)*(_sqlc_type *)data_ptr; \ } while (0) /*INDENT-OFF*/ switch (irec->es_type->data_type) { do { /* JSON long */ case SQL_BIGINT: BIN_TO_LLNG(SQLBIGINT); break; /* LONG */ case SQL_INTEGER: BIN_TO_LLNG(SQLINTEGER); break; /* INTEGER */ case SQL_SMALLINT: BIN_TO_LLNG(SQLSMALLINT); break; /* SHORT */ case SQL_TINYINT: BIN_TO_LLNG(SQLSCHAR); break; /* BYTE */ } while (0); return sfixed_to_number(stmt, llng, NULL, NULL, dest, len); /* JSON double */ do { // TODO: check accurate limits for floats in ES/SQL case SQL_FLOAT: /* HALF_FLOAT */ case SQL_REAL: BIN_TO_DBL(SQLREAL); break; /* FLOAT */ case SQL_DOUBLE: BIN_TO_DBL(SQLDOUBLE); break; /* DOUBLE */ } while (0); return floating_to_number(irec, dbl, NULL, NULL, dest, len); } /*INDENT-ON*/ # undef BIN_TO_LLNG # undef BIN_TO_DBL # undef CHK_SIZES BUGH(arec->desc->hdr.stmt, "unexpected ES/SQL type %hd.", irec->es_type->data_type); RET_HDIAG(arec->desc->hdr.stmt, SQL_STATE_HY000, "parameter conversion bug", 0); } static SQLRETURN numeric_to_number(esodbc_rec_st *irec, void *data_ptr, char *dest, size_t *len) { SQLDOUBLE dbl; SQLRETURN ret; if (! dest) { return floating_to_number(irec, 0., NULL, NULL, NULL, len); } ret = numeric_to_double(irec, data_ptr, &dbl); if (! SQL_SUCCEEDED(ret)) { return ret; } return floating_to_number(irec, dbl, NULL, NULL, dest, len); } /* convertion to (stringified) number */ SQLRETURN c2sql_number(esodbc_rec_st *arec, esodbc_rec_st *irec, SQLULEN pos, t_number_st *min, t_number_st *max, char *dest, size_t *len) { void *data_ptr; SQLSMALLINT ctype; SQLBIGINT llng; SQLUBIGINT ullng; SQLDOUBLE dbl; esodbc_stmt_st *stmt = arec->desc->hdr.stmt; /* pointer to app's buffer */ data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); /*INDENT-OFF*/ switch ((ctype = get_rec_c_type(arec, irec))) { case SQL_C_WCHAR: case SQL_C_CHAR: return string_to_number(arec, irec, pos, ctype == SQL_C_WCHAR, min, max, dest, len); case SQL_C_BINARY: return binary_to_number(arec, irec, pos, dest, len); do { case SQL_C_TINYINT: case SQL_C_STINYINT: llng = (SQLBIGINT)*(SQLSCHAR *)data_ptr; break; case SQL_C_SHORT: case SQL_C_SSHORT: llng = (SQLBIGINT)*(SQLSMALLINT *)data_ptr; break; case SQL_C_LONG: case SQL_C_SLONG: llng = (SQLBIGINT)*(SQLINTEGER *)data_ptr; break; case SQL_C_SBIGINT: llng = *(SQLBIGINT *)data_ptr; break; } while (0); return sfixed_to_number(stmt, llng, min, max, dest, len); do { case SQL_C_BIT: // XXX: check if 0/1? case SQL_C_UTINYINT: ullng = (SQLUBIGINT)*(SQLCHAR *)data_ptr; break; case SQL_C_USHORT: ullng = (SQLUBIGINT)*(SQLUSMALLINT *)data_ptr;break; case SQL_C_ULONG: ullng = (SQLUBIGINT)*(SQLUINTEGER *)data_ptr; break; case SQL_C_UBIGINT: ullng = *(SQLUBIGINT *)data_ptr; break; } while (0); return ufixed_to_number(irec, ullng, max, dest, len); do { case SQL_C_FLOAT: dbl = (SQLDOUBLE)*(SQLREAL *)data_ptr; break; case SQL_C_DOUBLE: dbl = *(SQLDOUBLE *)data_ptr; break; } while (0); return floating_to_number(irec, dbl, min, max, dest, len); case SQL_C_NUMERIC: return numeric_to_number(irec, data_ptr, dest, len); //case SQL_C_BOOKMARK: //case SQL_C_VARBOOKMARK: default: BUGH(stmt, "can't convert SQL C type %hd to long long.", get_rec_c_type(arec, irec)); RET_HDIAG(stmt, SQL_STATE_HY000, "bug converting parameter", 0); } /*INDENT-ON*/ return SQL_SUCCESS; } static SQLRETURN str_to_iso8601_timestamp(esodbc_stmt_st *stmt, SQLLEN *octet_len_ptr, void *data_ptr, BOOL wide, SQLULEN colsize, SQLSMALLINT decdigits, wchar_t *dest, size_t *cnt, SQLSMALLINT *format) { xstr_st xstr; TIMESTAMP_STRUCT tss; int n; SQLRETURN ret; xstr.wide = wide; if (wide) { xstr.w.str = (SQLWCHAR *)data_ptr; if (octet_len_ptr) { xstr.w.cnt = *octet_len_ptr / sizeof(SQLWCHAR) - /*\0*/1; } else { xstr.w.cnt = wcslen(xstr.w.str); } wtrim_ws(&xstr.w); } else { xstr.c.str = (SQLCHAR *)data_ptr; if (octet_len_ptr) { xstr.c.cnt = *octet_len_ptr / sizeof(SQLCHAR) - /*\0*/1; } else { xstr.c.cnt = strlen(xstr.c.str); } trim_ws(&xstr.c); } ret = parse_date_time_ts(stmt, &xstr, /*sql2c*/FALSE, &tss, format); if (! SQL_SUCCEEDED(ret)) { if (HDRH(stmt)->diag.state == SQL_STATE_22007) { RET_HDIAGS(stmt, SQL_STATE_22008); // TODO: check the code } } n = print_timestamp(&tss, /*ISO?*/TRUE, colsize, decdigits, dest); if (n <= 0) { ERRNH(stmt, "printing TIMESTAMP failed."); RET_HDIAGS(stmt, SQL_STATE_HY000); } *cnt = (size_t)n; return SQL_SUCCESS; } static SQLRETURN struct_to_iso8601_timestamp(esodbc_stmt_st *stmt, SQLLEN *octet_len_ptr, void *data_ptr, SQLSMALLINT ctype, SQLULEN colsize, SQLSMALLINT decdigits, wchar_t *dest, size_t *cnt) { TIMESTAMP_STRUCT *tss, buff; DATE_STRUCT *ds; TIME_STRUCT *ts; int n; size_t osize; SQLRETURN ret; switch (ctype) { case SQL_C_TYPE_TIME: TM_TO_TIMESTAMP_STRUCT(&today, &buff, 0LU); ts = (TIME_STRUCT *)data_ptr; buff.hour = ts->hour; buff.minute = ts->minute; buff.second = ts->second; tss = &buff; break; case SQL_C_TYPE_DATE: ds = (DATE_STRUCT *)data_ptr; memset(&buff, 0, sizeof(buff)); buff.year = ds->year; buff.month = ds->month; buff.day = ds->day; tss = &buff; break; case SQL_C_BINARY: if (! octet_len_ptr) { WARNH(stmt, "no length information provided for binary type: " "calculating it as a C-string!"); osize = strlen((char *)data_ptr); } else { osize = *octet_len_ptr; } if (osize != sizeof(TIMESTAMP_STRUCT)) { ERRH(stmt, "incorrect binary object size: %zu; expected: %zu.", osize, sizeof(TIMESTAMP_STRUCT)); RET_HDIAGS(stmt, SQL_STATE_22003); } /* no break */ case SQL_C_TYPE_TIMESTAMP: tss = (TIMESTAMP_STRUCT *)data_ptr; break; default: BUGH(stmt, "unexpected SQL C type %hd.", ctype); RET_HDIAG(stmt, SQL_STATE_HY000, "param conversion bug", 0); } if (HDRH(stmt)->dbc->apply_tz) { /* used for C2SQL conversions only: localtime to UTC */ buff = *tss; ret = tss_local_to_utc(stmt, &buff); if (! SQL_SUCCEEDED(ret)) { return ret; } tss = &buff; } n = print_timestamp(tss, /*ISO?*/TRUE, colsize, decdigits, dest); if (n <= 0) { ERRNH(stmt, "printing TIMESTAMP failed."); RET_HDIAGS(stmt, SQL_STATE_HY000); } *cnt = (size_t)n; return SQL_SUCCESS; } /* apply corrections depending on the (column) size and decimal digits * values given at binding time: nullify or trim the resulted string: * https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/column-size * */ static SQLRETURN size_decdigits_for_iso8601(esodbc_rec_st *irec, SQLULEN *_colsize, SQLSMALLINT *_decdigits) { SQLULEN colsize; SQLSMALLINT decdigits; esodbc_stmt_st *stmt = HDRH(irec->desc)->stmt; colsize = get_param_size(irec); DBGH(stmt, "requested column size: %llu.", (uint64_t)colsize); decdigits = get_param_decdigits(irec); DBGH(stmt, "requested decimal digits: %hd.", decdigits); if (ESODBC_MAX_SEC_PRECISION < decdigits) { WARNH(stmt, "requested decimal digits adjusted from %hd to %d (max).", decdigits, ESODBC_MAX_SEC_PRECISION); decdigits = ESODBC_MAX_SEC_PRECISION; } switch (irec->es_type->data_type) { case SQL_TYPE_TIME: if (colsize) { if (colsize < TIME_TEMPLATE_LEN(0) || colsize == TIME_TEMPLATE_LEN(1) - 1 /* `:ss.`*/) { ERRH(stmt, "invalid column size value: %llu; allowed: " "8 or 9 + fractions count.", (uint64_t)colsize); RET_HDIAGS(stmt, SQL_STATE_HY104); } colsize += DATE_TEMPLATE_LEN + /* ` `/`T` */1; } break; case SQL_TYPE_DATE: /* if origin is a timestamp (struct or string), the time part * needs to be zeroed. */ if (colsize) { if (colsize != DATE_TEMPLATE_LEN) { ERRH(stmt, "invalid column size value: %llu; allowed: " "%zu.", (uint64_t)colsize, DATE_TEMPLATE_LEN); RET_HDIAGS(stmt, SQL_STATE_HY104); } colsize += /* ` `/`T` */1 + TIME_TEMPLATE_LEN(0); } if (decdigits) { ERRH(stmt, "invalid decimal digits %hd for TIME type.", decdigits); RET_HDIAGS(stmt, SQL_STATE_HY104); } break; case SQL_TYPE_TIMESTAMP: if (colsize && (colsize < TIMESTAMP_NOSEC_TEMPLATE_LEN || colsize == 17 || colsize == 18)) { ERRH(stmt, "invalid column size value: %llu; allowed: " "16, 19 or 20 + fractions count.", (uint64_t)colsize); RET_HDIAGS(stmt, SQL_STATE_HY104); } break; default: assert(0); } DBGH(stmt, "applying: column size: %llu, decimal digits: %hd.", (uint64_t)colsize, decdigits); *_colsize = colsize; *_decdigits = decdigits; return SQL_SUCCESS; } SQLRETURN c2sql_date_time(esodbc_rec_st *arec, esodbc_rec_st *irec, SQLULEN pos, char *dest, size_t *len) { static const wstr_st time_0_z = WSTR_INIT("00:00:00Z"); esodbc_stmt_st *stmt; void *data_ptr; SQLLEN *octet_len_ptr; SQLSMALLINT ctype; SQLRETURN ret; SQLULEN colsize; SQLSMALLINT decdigits; wchar_t wbuff[ISO8601_TIMESTAMP_MAX_LEN + /*\0*/1]; size_t cnt; SQLSMALLINT format; int n; if (! dest) { /* maximum possible space it can take */ *len = /*2x `"`*/2 + ISO8601_TIMESTAMP_MAX_LEN + /*\0 for printf*/1; return SQL_SUCCESS; } else { *dest = '"'; *len = 1; } stmt = HDRH(arec->desc)->stmt; /* pointer to read from how many bytes we have */ octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); /* pointer to app's buffer */ data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); ret = size_decdigits_for_iso8601(irec, &colsize, &decdigits); if (! SQL_SUCCEEDED(ret)) { return ret; } format = 0; /*INDENT-OFF*/ switch ((ctype = get_rec_c_type(arec, irec))) { case SQL_C_CHAR: case SQL_C_WCHAR: ret = str_to_iso8601_timestamp(stmt, octet_len_ptr, data_ptr, ctype == SQL_C_WCHAR, colsize, decdigits, wbuff, &cnt, &format); if (! SQL_SUCCEEDED(ret)) { return ret; } /* disallow DATE <-> TIME conversions */ if ((irec->es_type->data_type == SQL_C_TYPE_TIME && format == SQL_TYPE_DATE) || (format == SQL_TYPE_TIME && irec->es_type->data_type == SQL_C_TYPE_DATE)) { ERRH(stmt, "TIME-DATE conversions are not possible."); RET_HDIAGS(stmt, SQL_STATE_22018); } break; do { case SQL_C_TYPE_TIME: /* case should have been caught by the convertibility checker */ assert(irec->es_type->data_type != SQL_C_TYPE_DATE); break; case SQL_C_TYPE_DATE: assert(irec->es_type->data_type != SQL_C_TYPE_TIME); break; } while (0); case SQL_C_BINARY: case SQL_C_TYPE_TIMESTAMP: ret = struct_to_iso8601_timestamp(stmt, octet_len_ptr, data_ptr, ctype, colsize, decdigits, wbuff, &cnt); if (! SQL_SUCCEEDED(ret)) { return ret; } break; default: BUGH(stmt, "can't convert SQL C type %hd to timestamp.", ctype); RET_HDIAG(stmt, SQL_STATE_HY000, "bug converting parameter", 0); } /*INDENT-ON*/ /* Note: these post-print fixes could be optimized out by passing the SQL * type to the respective *_to_iso8601_timestamp(), to some code clarity * expense. */ /* Adapt the resulting ISO8601 value to the target data type */ switch (irec->es_type->data_type) { case SQL_TYPE_TIME: /* shift value + \0 upwards over the DATE component */ /* Note: by the book, non-0 fractional seconds in timestamp should * lead to 22008 a failure. However, ES/SQL's TIME supports * fractions, so will just ignore this provision. */ cnt -= DATE_TEMPLATE_LEN + /*'T'*/1; wmemmove(wbuff, wbuff + DATE_TEMPLATE_LEN + /*'T'*/1, cnt + 1); break; case SQL_TYPE_DATE: /* if origin is a timestamp (struct or string), the time part * needs to be zeroed. */ if (ctype == SQL_C_TYPE_TIMESTAMP || format == SQL_TYPE_TIMESTAMP) { assert(ISO8601_TIMESTAMP_MIN_LEN <= cnt); wmemcpy(wbuff + DATE_TEMPLATE_LEN + /*'T'*/1, (wchar_t *)time_0_z.str, time_0_z.cnt + /*\0*/1); cnt = ISO8601_TIMESTAMP_MIN_LEN; } break; default: assert(irec->es_type->data_type == SQL_TYPE_TIMESTAMP); } DBGH(stmt, "converted value: [%zu] `" LWPDL "`.", cnt, cnt, wbuff); n = ascii_w2c(wbuff, dest + *len, cnt); assert(0 < n); /* printed "in-house", no special chars, shouldn't fail */ *len += (size_t)n - /*\0*/1; dest[(*len) ++] = '"'; return SQL_SUCCESS; } /* parses an interval literal string from app's char/wchar_t buffer */ static SQLRETURN c2sql_str2interval(esodbc_rec_st *arec, esodbc_rec_st *irec, SQLSMALLINT ctype, void *data_ptr, SQLLEN *octet_len_ptr, SQL_INTERVAL_STRUCT *ivl) { esodbc_stmt_st *stmt = arec->desc->hdr.stmt; wstr_st wstr; SQLWCHAR wbuff[128], *wptr; SQLLEN octet_len; int ret; if ((! octet_len_ptr) || (*octet_len_ptr == SQL_NTSL)) { octet_len = (ctype == SQL_C_CHAR) ? strlen((SQLCHAR *)data_ptr) : wcslen((SQLWCHAR *)data_ptr); } else { octet_len = *octet_len_ptr; if (octet_len <= 0) { ERRH(stmt, "invalid interval buffer length: %lld.", (int64_t)octet_len); RET_HDIAGS(stmt, SQL_STATE_HY090); } } if (ctype == SQL_C_CHAR) { if (sizeof(wbuff)/sizeof(wbuff[0]) < (size_t)octet_len) { INFOH(stmt, "translation buffer too small (%zu < %zu), " "allocation needed.", sizeof(wbuff)/sizeof(wbuff[0]), (size_t)octet_len); /* 0-term is most of the time not counted in input str and * ascii_c2w() writes it -> always allocate space for it */ wptr = malloc((octet_len + 1) * sizeof(SQLWCHAR)); if (! wptr) { ERRNH(stmt, "OOM for %lld x SQLWCHAR", (int64_t)octet_len); RET_HDIAGS(stmt, SQL_STATE_HY001); } } else { wptr = wbuff; } ret = ascii_c2w((SQLCHAR *)data_ptr, wptr, octet_len); if (ret <= 0) { ERRH(stmt, "SQLCHAR-to-SQLWCHAR conversion failed for " "[%lld] `" LCPDL "`.", (int64_t)octet_len, octet_len, (char *)data_ptr); if (wptr != wbuff) { free(wptr); wptr = NULL; } /* should only happen on too short input string */ RET_HDIAGS(stmt, SQL_STATE_22018); } else { assert(ret <= octet_len + 1); /* no overrun */ } wstr.str = wptr; wstr.cnt = (size_t)octet_len; } else { assert(ctype == SQL_C_WCHAR); wstr.str = (SQLWCHAR *)data_ptr; wstr.cnt = (size_t)octet_len; wptr = NULL; } /* trim trailing NTS, if any */ if (wstr.str[wstr.cnt - 1] == L'\0') { wstr.cnt --; } /* TODO: spec is not clear if we'd get here a literal, or a value (which * would make sense, given that the interval type is passed as SQL type; * however, inter-interval types conversions are also not clearly spec'd: * we could get a literal of one type and different SQL type in bound * param). --> parse_interval_literal_value()?? */ /* BindParam sets IPD's fields -> use irec */ ret = parse_interval_literal(irec, &wstr, ivl); if (wptr && wptr != wbuff) { free(wptr); wptr = NULL; } if (! SQL_SUCCEEDED(ret)) { return ret; } return SQL_SUCCESS; } SQLRETURN c2sql_interval(esodbc_rec_st *arec, esodbc_rec_st *irec, SQLULEN pos, char *dest, size_t *len) { esodbc_stmt_st *stmt = arec->desc->hdr.stmt; void *data_ptr; SQLLEN *octet_len_ptr; SQLSMALLINT ctype; SQLUBIGINT ubint; SQLBIGINT bint; SQLUINTEGER uint; size_t res; SQL_INTERVAL_STRUCT ivl = {0}; SQLRETURN ret; /* Assign the temporary `ubint` the value passed in the client app buffer, * setting interval's sign, if negative. * Uses local vars: bint, ubint, ivl */ # define ASSIGN_SIGNED(_sqltype) \ do { \ bint = (SQLBIGINT)*(_sqltype *)data_ptr; \ if (bint < 0) { \ ivl.interval_sign = SQL_TRUE; \ ubint = -bint; \ } else { \ ubint = bint; \ } \ } while (0) if (! dest) { /* maximum possible space it can take */ *len = INTERVAL_VAL_MAX_LEN; return SQL_SUCCESS; } else { *dest = '"'; *len = 1; } /* pointer to app's buffer */ data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); assert(SQL_FALSE == 0); /* == {0}'d above */ /*INDENT-OFF*/ switch ((ctype = get_rec_c_type(arec, irec))) { case SQL_C_CHAR: case SQL_C_WCHAR: /* pointer to read from how many bytes we have */ octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); ret = c2sql_str2interval(arec, irec, ctype, data_ptr, octet_len_ptr, &ivl); if (! SQL_SUCCEEDED(ret)) { return ret; } break; /* numeric exact */ do { case SQL_C_BIT: ubint = *(SQLCHAR *)data_ptr ? 1 : 0; break; case SQL_C_SHORT: case SQL_C_SSHORT: ASSIGN_SIGNED(SQLSMALLINT); break; case SQL_C_USHORT: ubint = (SQLUBIGINT)*(SQLUSMALLINT*)data_ptr; break; case SQL_C_LONG: case SQL_C_SLONG: ASSIGN_SIGNED(SQLINTEGER); break; case SQL_C_ULONG: ubint = (SQLUBIGINT)*(SQLUINTEGER *)data_ptr; break; case SQL_C_TINYINT: case SQL_C_STINYINT: ASSIGN_SIGNED(SQLSCHAR); break; case SQL_C_UTINYINT: ubint = (SQLUBIGINT)*(SQLCHAR *)data_ptr; break; case SQL_C_SBIGINT: ASSIGN_SIGNED(SQLBIGINT); break; case SQL_C_UBIGINT: ubint = *(SQLUBIGINT *)data_ptr; break; } while (0); if (/*SQLUINTEGER*/ULONG_MAX < ubint) { ERRH(stmt, "value too large for interval field: %llu.", ubint); RET_HDIAGS(stmt, SQL_STATE_22015); } else { uint = (SQLUINTEGER)ubint; } assert(SQL_CODE_YEAR == SQL_IS_YEAR); ivl.interval_type = irec->es_type->data_type - (SQL_INTERVAL_YEAR - SQL_CODE_YEAR); DBGH(stmt, "converting integer value %lu to interval.type: %d.", uint, ivl.interval_type); // TODO: precision checks? (ES/SQL takes already a u32/SQLUINT.) switch (irec->es_type->data_type) { case SQL_INTERVAL_YEAR: ivl.intval.year_month.year = uint; break; case SQL_INTERVAL_MONTH: ivl.intval.year_month.month = uint; break; case SQL_INTERVAL_DAY: ivl.intval.day_second.day = uint; break; case SQL_INTERVAL_HOUR: ivl.intval.day_second.hour = uint; break; case SQL_INTERVAL_MINUTE: ivl.intval.day_second.minute = uint; break; case SQL_INTERVAL_SECOND: ivl.intval.day_second.second = uint; break; default: // shold never get here BUGH(stmt, "conversion not supported."); RET_HDIAGS(stmt, SQL_STATE_HY000); } break; case SQL_C_INTERVAL_YEAR: case SQL_C_INTERVAL_MONTH: case SQL_C_INTERVAL_DAY: case SQL_C_INTERVAL_HOUR: case SQL_C_INTERVAL_MINUTE: case SQL_C_INTERVAL_SECOND: case SQL_C_INTERVAL_YEAR_TO_MONTH: case SQL_C_INTERVAL_DAY_TO_HOUR: case SQL_C_INTERVAL_DAY_TO_MINUTE: case SQL_C_INTERVAL_DAY_TO_SECOND: case SQL_C_INTERVAL_HOUR_TO_MINUTE: case SQL_C_INTERVAL_HOUR_TO_SECOND: case SQL_C_INTERVAL_MINUTE_TO_SECOND: // by data compatibility assert (irec->es_type->data_type == ctype); /* no break! */ case SQL_C_BINARY: ivl = *(SQL_INTERVAL_STRUCT *)data_ptr; break; case SQL_C_NUMERIC: BUGH(stmt, "conversion not yet supported."); RET_HDIAGS(stmt, SQL_STATE_HYC00); break; default: BUGH(stmt, "can't convert SQL C type %hd to interval.", ctype); RET_HDIAG(stmt, SQL_STATE_HY000, "bug converting parameter", 0); } /*INDENT-ON*/ res = print_interval_iso8601(irec, &ivl, dest + *len); if (res <= 0) { ERRH(stmt, "printing interval of type %hd failed.", ivl.interval_type); RET_HDIAG(stmt, SQL_STATE_HY000, "interval printing failed", 0); } else { *len += res; } dest[(*len) ++] = '"'; return SQL_SUCCESS; # undef ASSIGN_SIGNED } static inline SQLLEN get_octet_len(SQLLEN *octet_len_ptr, void *data_ptr, BOOL wide) { SQLLEN cnt; assert(data_ptr); if (! octet_len_ptr) { /* "If [...] is a null pointer, the driver assumes that all input * parameter values are non-NULL and that character and binary data is * null-terminated." */ cnt = wide ? wcslen((wchar_t *)data_ptr) : strlen((char *)data_ptr); } else { cnt = *octet_len_ptr; switch (cnt) { case SQL_NTSL: cnt = wide ? wcslen((wchar_t *)data_ptr) : strlen((char *)data_ptr); break; case SQL_NULL_DATA: BUG("converting SQL_NULL_DATA"); cnt = -1; /* UTF16/8 will fail */ break; default: /* get characters count from octets count */ cnt /= wide ? sizeof(SQLWCHAR) : sizeof(SQLCHAR); } } return cnt; } static SQLRETURN c2sql_cstr2qstr(esodbc_rec_st *arec, esodbc_rec_st *irec, SQLULEN pos, char *dest, size_t *len) { void *data_ptr; SQLLEN *octet_len_ptr, cnt; esodbc_stmt_st *stmt = arec->desc->hdr.stmt; /* pointer to read from how many bytes we have */ octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); /* pointer to app's buffer */ data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); cnt = get_octet_len(octet_len_ptr, data_ptr, /*wide*/FALSE); if (dest) { *dest = '"'; } else if ((SQLLEN)get_param_size(irec) < cnt) { ERRH(stmt, "string's length (%lld) longer than parameter size (%llu).", (int64_t)cnt, (uint64_t)get_param_size(irec)); RET_HDIAGS(stmt, SQL_STATE_22001); } *len = json_escape((char *)data_ptr, cnt, dest + !!dest, SIZE_MAX); if (dest) { dest[*len + /*1st `"`*/1] = '"'; } *len += /*`"`*/2; return SQL_SUCCESS; } static SQLRETURN c2sql_wstr2qstr(esodbc_rec_st *arec, esodbc_rec_st *irec, SQLULEN pos, char *dest, size_t *len) { void *data_ptr; SQLLEN *octet_len_ptr, cnt, octets; int err; esodbc_stmt_st *stmt = arec->desc->hdr.stmt; /* pointer to read from how many bytes we have */ octet_len_ptr = deferred_address(SQL_DESC_OCTET_LENGTH_PTR, pos, arec); /* pointer to app's buffer */ data_ptr = deferred_address(SQL_DESC_DATA_PTR, pos, arec); cnt = get_octet_len(octet_len_ptr, data_ptr, /*wide*/TRUE); if (dest) { *dest = '"'; } else { if ((SQLLEN)get_param_size(irec) < cnt) { ERRH(stmt, "string's length (%lld) longer than parameter " "size (%llu).", (int64_t)cnt, (uint64_t)get_param_size(irec)); RET_HDIAGS(stmt, SQL_STATE_22001); } } DBGH(stmt, "converting w-string [%lld] `" LWPDL "`; target@0x%p.", (int64_t)cnt, cnt, (wchar_t *)data_ptr, dest); if (cnt) { /* U16WC_TO_MBU8 will fail with empty string, but set no err */ WAPI_CLR_ERRNO(); octets = U16WC_TO_MBU8((wchar_t *)data_ptr, cnt, dest + !!dest, dest ? INT_MAX : 0); if ((err = WAPI_ERRNO()) != ERROR_SUCCESS) { ERRH(stmt, "converting to multibyte string failed: 0x%x", err); RET_HDIAGS(stmt, SQL_STATE_HY000); } assert(0 < octets); /* should not fail and return negative */ } else { octets = 0; } *len = (size_t)octets; if (dest) { /* last param - buffer len - is calculated as if !dest */ *len = json_escape_overlapping(dest + /*1st `"`*/1, octets, JSON_ESC_SEQ_SZ * octets); dest[*len + /*1st `"`*/1] = '"'; } else { /* UCS*-to-UTF8 converted buffer is not yet available, so an accurate * estimation of how long the JSON-escaping would take is not possible * => estimate a worst case: 6x */ *len *= JSON_ESC_SEQ_SZ; } *len += /*2x `"`*/2; return SQL_SUCCESS; } static SQLRETURN c2sql_number2qstr(esodbc_rec_st *arec, esodbc_rec_st *irec, SQLULEN pos, char *dest, size_t *len) { SQLRETURN ret; esodbc_stmt_st *stmt = arec->desc->hdr.stmt; if (dest) { *dest = '"'; } ret = c2sql_number(arec, irec, pos, NULL, NULL, dest + !!dest, len); if (dest) { /* compare lengths only once number has actually been converted */ if (get_param_size(irec) < *len) { ERRH(stmt, "converted number length (%zu) larger than parameter " "size (%llu)", *len, (uint64_t)get_param_size(irec)); RET_HDIAGS(stmt, SQL_STATE_22003); } dest[*len + /*1st `"`*/1] = '"'; } *len += /*2x `"`*/2; return ret; } SQLRETURN c2sql_varchar(esodbc_rec_st *arec, esodbc_rec_st *irec, SQLULEN pos, char *dest, size_t *len) { SQLSMALLINT ctype; esodbc_stmt_st *stmt = arec->desc->hdr.stmt; switch ((ctype = get_rec_c_type(arec, irec))) { case SQL_C_CHAR: return c2sql_cstr2qstr(arec, irec, pos, dest, len); case SQL_C_WCHAR: return c2sql_wstr2qstr(arec, irec, pos, dest, len); case SQL_C_BINARY: // XXX: json_escape ERRH(stmt, "conversion from SQL C BINARY not implemented."); RET_HDIAG(stmt, SQL_STATE_HYC00, "conversion from SQL C BINARY " "not yet supported", 0); break; case SQL_C_TINYINT: case SQL_C_STINYINT: case SQL_C_SHORT: case SQL_C_SSHORT: case SQL_C_LONG: case SQL_C_SLONG: case SQL_C_SBIGINT: case SQL_C_BIT: case SQL_C_UTINYINT: case SQL_C_USHORT: case SQL_C_ULONG: case SQL_C_UBIGINT: case SQL_C_FLOAT: case SQL_C_DOUBLE: case SQL_C_NUMERIC: return c2sql_number2qstr(arec, irec, pos, dest, len); case SQL_C_TYPE_DATE: case SQL_C_TYPE_TIME: case SQL_C_TYPE_TIMESTAMP: // TODO: leave it a timestamp, or actual DATE/TIME/TS? return c2sql_date_time(arec, irec, pos, dest, len); // case SQL_C_GUID: default: BUGH(stmt, "can't convert SQL C type %hd to timestamp.", get_rec_c_type(arec, irec)); RET_HDIAG(stmt, SQL_STATE_HY000, "bug converting parameter", 0); } } /* vim: set noet fenc=utf-8 ff=dos sts=0 sw=4 ts=4 tw=78 : */