From 8437248c9b4e0faaef3a29415eb9afdccb1c0866 Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Wed, 27 Feb 2019 14:34:49 +0100 Subject: [PATCH 1/4] run integration tests with custom driver install - add CLI option to take a connection string and use that to connect to a custom driver installation. --- test/integration/ites.py | 7 +++++-- test/integration/testing.py | 13 ++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/test/integration/ites.py b/test/integration/ites.py index 737a16c2..10636b77 100644 --- a/test/integration/ites.py +++ b/test/integration/ites.py @@ -65,7 +65,7 @@ def ites(args): # run the tests if not args.skip_tests: assert(data is not None) - tests = Testing(data) + tests = Testing(data, args.dsn) tests.perform() def main(): @@ -78,8 +78,11 @@ def main(): stage_grp.add_argument("-p", "--pre-staged", help="Use a pre-staged and running Elasticsearch instance", action="store_true", default=False) - parser.add_argument("-d", "--driver", help="The path to the driver file to test; if not provided, the driver " + driver_grp = parser.add_mutually_exclusive_group() + driver_grp.add_argument("-d", "--driver", help="The path to the driver file to test; if not provided, the driver " "is assumed to have been installed.") + driver_grp.add_argument("-c", "--dsn", help="The connection string to use with a preinstalled driver; the DSN must" + " contain the name under which the driver to test is registered.") parser.add_argument("-o", "--offline_dir", help="The directory path holding the files to copy the test data from, " "as opposed to downloading them.") parser.add_argument("-e", "--ephemeral", help="Remove the staged Elasticsearch and installed driver after testing" diff --git a/test/integration/testing.py b/test/integration/testing.py index 81bef35d..f83d456f 100644 --- a/test/integration/testing.py +++ b/test/integration/testing.py @@ -16,12 +16,15 @@ class Testing(object): _data = None + _dsn = None - def __init__(self, test_data): + def __init__(self, test_data, dsn=None): self._data = test_data + self._dsn = dsn if dsn else CONNECT_STRING + print("Using DSN: '%s'." % self._dsn) def _reconstitute_csv(self, index_name): - with pyodbc.connect(CONNECT_STRING) as cnxn: + with pyodbc.connect(self._dsn) as cnxn: cnxn.autocommit = True csv = u"" cols = self._data.csv_attributes(index_name)[1] @@ -54,7 +57,7 @@ def _as_csv(self, index_name): def _count_all(self, index_name): cnt = 0 - with pyodbc.connect(CONNECT_STRING) as cnxn: + with pyodbc.connect(self._dsn) as cnxn: cnxn.autocommit = True with cnxn.execute("select 1 from %s" % index_name) as curs: while curs.fetchone(): @@ -65,7 +68,7 @@ def _count_all(self, index_name): (index_name, cnt, csv_lines)) def _clear_cursor(self, index_name): - conn_str = CONNECT_STRING + ";MaxFetchSize=5" + conn_str = self._dsn + ";MaxFetchSize=5" with pyodbc.connect(conn_str) as cnxn: cnxn.autocommit = True with cnxn.execute("select 1 from %s limit 10" % index_name) as curs: @@ -77,7 +80,7 @@ def _clear_cursor(self, index_name): # no exception raised -> passed def _current_user(self): - with pyodbc.connect(CONNECT_STRING) as cnxn: + with pyodbc.connect(self._dsn) as cnxn: cnxn.autocommit = True user = cnxn.getinfo(pyodbc.SQL_USER_NAME) if user != "elastic": From a5cc8d5040ad730a2fbd8804a6e841920f55000b Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Wed, 27 Feb 2019 14:38:22 +0100 Subject: [PATCH 2/4] add support for timestamps in local timezone With this commit the driver will be able to use local timestamps towards the application and do the translation between that and ES/SQL (UTC) A connection string parameter controlls if this behavior is enabled; it is disabled by default. The timezone is read from the TZ environment variable, or the system if that isn't set. The driver will now also provide the SQL format of date/timestamp when a DATE/DATETIME value is converted to string. --- driver/connect.c | 15 +- driver/convert.c | 650 +++++++++++++++++++++++++++++++---------------- driver/defs.h | 13 +- driver/dsn.c | 11 + driver/dsn.h | 4 +- driver/handles.h | 1 + driver/log.c | 1 + driver/queries.c | 2 +- driver/util.h | 18 ++ 9 files changed, 481 insertions(+), 234 deletions(-) diff --git a/driver/connect.c b/driver/connect.c index 309f7685..7fb6b3a9 100644 --- a/driver/connect.c +++ b/driver/connect.c @@ -1092,6 +1092,10 @@ SQLRETURN config_dbc(esodbc_dbc_st *dbc, esodbc_dsn_attrs_st *attrs) } INFOH(dbc, "pack JSON: %s.", dbc->pack_json ? "true" : "false"); + /* "apply TZ" param for time conversions */ + dbc->apply_tz = wstr2bool(&attrs->apply_tz); + INFOH(dbc, "apply TZ: %s.", dbc->apply_tz ? "true" : "false"); + /* * Version checking mode */ @@ -1781,7 +1785,8 @@ static void set_display_size(esodbc_estype_st *es_type) case SQL_TYPE_DATE: case SQL_TYPE_TIME: case SQL_TYPE_TIMESTAMP: /* SQL/ES DATE */ - es_type->display_size = sizeof(ESODBC_ISO8601_TEMPLATE) - /*0*/1; + es_type->display_size = + ISO8601_TIMESTAMP_LEN(ESODBC_DEF_SEC_PRECISION); break; @@ -1820,7 +1825,7 @@ static void set_display_size(esodbc_estype_st *es_type) break; case SQL_INTERVAL_SECOND: es_type->display_size = ESODBC_MAX_IVL_SECOND_LEAD_PREC + /*.*/1 + - ESODBC_MAX_SEC_PRECISION; + ESODBC_DEF_SEC_PRECISION; break; case SQL_INTERVAL_DAY_TO_HOUR: es_type->display_size = 3 + ESODBC_MAX_IVL_DAY_LEAD_PREC; @@ -1830,18 +1835,18 @@ static void set_display_size(esodbc_estype_st *es_type) break; case SQL_INTERVAL_DAY_TO_SECOND: es_type->display_size = 10 + ESODBC_MAX_IVL_DAY_LEAD_PREC + - ESODBC_MAX_SEC_PRECISION; + ESODBC_DEF_SEC_PRECISION; break; case SQL_INTERVAL_HOUR_TO_MINUTE: es_type->display_size = 3 + ESODBC_MAX_IVL_HOUR_LEAD_PREC; break; case SQL_INTERVAL_HOUR_TO_SECOND: es_type->display_size = 7 + ESODBC_MAX_IVL_HOUR_LEAD_PREC + - ESODBC_MAX_SEC_PRECISION; + ESODBC_DEF_SEC_PRECISION; break; case SQL_INTERVAL_MINUTE_TO_SECOND: es_type->display_size = 4 + ESODBC_MAX_IVL_MINUTE_LEAD_PREC + - ESODBC_MAX_SEC_PRECISION; + ESODBC_DEF_SEC_PRECISION; break; /* diff --git a/driver/convert.c b/driver/convert.c index 1379bd1b..d6dacada 100644 --- a/driver/convert.c +++ b/driver/convert.c @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -27,6 +28,17 @@ (_tsp)->second = (_tmp)->tm_sec; \ } while (0) +#ifdef _WIN32 +# ifdef _WIN64 +# define MKTIME_YEAR_RANGE "1970-3000" +# else /* _WIN64 */ +# define MKTIME_YEAR_RANGE "1970-2038" +# endif /* _WIN64 */ +#else /* _WIN32 */ +# error "platform not supported" +#endif /* _WIN32 */ +#define MKTIME_FAIL_MSG "Outside of the " MKTIME_YEAR_RANGE "year range?" + /* For fixed size (destination) types, the target buffer can't be NULL. */ #define REJECT_IF_NULL_DEST_BUFF(_s/*tatement*/, _p/*ointer*/) \ do { \ @@ -148,6 +160,8 @@ void convert_init() SQL_C_TYPE_TIMESTAMP }; + char *tz; + /* 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 ++) { @@ -243,6 +257,13 @@ void convert_init() sql = SQL_GUID; csql = SQL_C_GUID; compat_matrix[ESSQL_TYPE_IDX(sql)][ESSQL_C_TYPE_IDX(csql)] = TRUE; + + /* TZ conversions */ + tzset(); + tz = getenv(ESODBC_TZ_ENV_VAR); + INFO("TZ: `%s`, timezone offset: %ld seconds, daylight saving: " + "%sapplicable, standard: %s, daylight: %s.", tz ? tz : "", + _timezone, _daylight ? "" : "not ", _tzname[0], _tzname[1]); } @@ -1457,7 +1478,7 @@ static SQLRETURN wstr_to_cstr(esodbc_rec_st *arec, esodbc_rec_st *irec, * Note: chars_0 accounts for 0-term, but length indicated back to the * application must not. */ -static SQLRETURN wstr_to_wstr(esodbc_rec_st *arec, esodbc_rec_st *irec, +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) { @@ -1470,171 +1491,213 @@ static SQLRETURN wstr_to_wstr(esodbc_rec_st *arec, esodbc_rec_st *irec, return transfer_xstr0(arec, irec, &xsrc, data_ptr, octet_len_ptr); } -/* Converts an ISO 8601-formated xstr to a TS. - * xstr needs to be trimmed to exact data (no padding, no 0-term counted). - * If ts_buff is non-NULL, the xstr will be copied (possibly W-to-C converted) - * into it. */ -static BOOL xstr_to_timestamp_struct(esodbc_stmt_st *stmt, xstr_st *xstr, - TIMESTAMP_STRUCT *tss, cstr_st *ts_buff) +/* Parses an ISO8601 timestamp and returns the result into a TIMESTAMP_STRUCT. + * 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[sizeof(ESODBC_ISO8601_TEMPLATE)/*+\0*/]; - cstr_st ts_str, *ts_ptr; + char buff[ISO8601_TIMESTAMP_MAX_LEN + /*\0*/1]; + cstr_st ts_str; timestamp_t tsp; - struct tm tmp; - - if (ts_buff) { - assert(sizeof(ESODBC_ISO8601_TEMPLATE) - 1 <= ts_buff->cnt); - ts_ptr = ts_buff; - } else { - ts_str.str = buff; - ts_str.cnt = sizeof(buff); - ts_ptr = &ts_str; - } + struct tm tm; if (xstr->wide) { - DBGH(stmt, "converting ISO 8601 `" LWPDL "` to timestamp.", - LWSTR(&xstr->w)); - if (sizeof(ESODBC_ISO8601_TEMPLATE) - 1 < xstr->w.cnt) { - ERRH(stmt, "`" LWPDL "` not a TIMESTAMP.", LWSTR(&xstr->w)); - return FALSE; + 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; } - /* convert the W-string to C-string; also, copy it directly into out - * ts_buff, if given (thus saving one extra copying) */ - ts_ptr->cnt = ascii_w2c(xstr->w.str, ts_ptr->str, xstr->w.cnt) - 1; } else { - DBGH(stmt, "converting ISO 8601 `" LCPDL "` to timestamp.", - LCSTR(&xstr->c)); - if (sizeof(ESODBC_ISO8601_TEMPLATE) - 1 < xstr->c.cnt) { - ERRH(stmt, "`" LCPDL "` not a TIMESTAMP.", LCSTR(&xstr->c)); - return FALSE; - } - /* no conversion needed; but copying to the out ts_buff, if given */ - if (ts_buff) { - memcpy(ts_ptr->str, xstr->c.str, xstr->c.cnt); - ts_ptr->cnt = xstr->c.cnt; + 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_ptr = &xstr->c; + ts_str = xstr->c; } } - /* len counts the 0-term */ - if (ts_ptr->cnt <= 1 || timestamp_parse(ts_ptr->str, ts_ptr->cnt, &tsp) || - (! timestamp_to_tm_local(&tsp, &tmp))) { - ERRH(stmt, "data `" LCPDL "` not an ANSI ISO 8601 format.", - LCSTR(ts_ptr)); - return FALSE; + 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) { + /* apply local offset */ + tm.tm_sec -= _timezone; /* : time.h externs */ + tm.tm_isdst = -1; /* let the system determine the daylight saving */ + /* rebalance member values */ + if (mktime(&tm) == (time_t)-1) { + ERRH(stmt, "failed to adjust timestamp `" LCPDL "` by TZ (%ld). " + MKTIME_FAIL_MSG, LCSTR(&ts_str), _timezone); + RET_HDIAG(stmt, SQL_STATE_22008, "Timestamp timezone adjustment " + "failed. " MKTIME_FAIL_MSG, 0); + } } - TM_TO_TIMESTAMP_STRUCT(&tmp, tss); - tss->fraction = tsp.nsec / 1000000; - DBGH(stmt, "parsed ISO 8601: `%04d-%02d-%02dT%02d:%02d:%02d.%u+%dm`.", - tss->year, tss->month, tss->day, - tss->hour, tss->minute, tss->second, tss->fraction, - tsp.offset); + TM_TO_TIMESTAMP_STRUCT(&tm, tss); + /* "the fraction field is the number of billionths of a second" */ + tss->fraction = tsp.nsec; - return TRUE; + 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; } -/* Analyzes the received string as time/date/timedate(timestamp) and parses it + +/* Analyzes the received string as time/date/timestamp(timedate) and parses it * into received 'tss' struct, indicating detected format in 'format'. */ -static BOOL parse_date_time_ts(esodbc_stmt_st *stmt, xstr_st *xstr, - TIMESTAMP_STRUCT *tss, SQLSMALLINT *format, cstr_st *ts_buff) +static SQLRETURN parse_date_time_ts(esodbc_stmt_st *stmt, xstr_st *xstr, + BOOL sql2c, TIMESTAMP_STRUCT *tss, SQLSMALLINT *format) { + esodbc_dbc_st *dbc; + SQLRETURN ret; /* template buffer: date or time values will be copied in place and * evaluated as a timestamp (needs to be valid) */ - SQLCHAR templ[] = "0001-01-01T00:00:00.0000000Z"; + SQLCHAR templ[] = "0001-01-01T00:00:00.000000000+00:00"; /* conversion Wide to C-string buffer */ - SQLCHAR w2c[sizeof(ESODBC_ISO8601_TEMPLATE)/*+\0*/]; + SQLCHAR w2c[ISO8601_TIMESTAMP_MAX_LEN]; cstr_st td;/* timedate string */ xstr_st xtd; - - /* is this a TIMESTAMP? */ - if (sizeof(ESODBC_TIME_TEMPLATE) - 1 < XSTR_LEN(xstr)) { - /* longer than a date-value -> try a timestamp */ - if (! xstr_to_timestamp_struct(stmt, xstr, tss, ts_buff)) { - return FALSE; - } - if (format) { - *format = SQL_TYPE_TIMESTAMP; - } - return TRUE; - } + long hours, mins, n; + BOOL to_utc; /* 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) - 1; + td.cnt = ascii_w2c(xstr->w.str, w2c, xstr->w.cnt) - /*\0*/1; td.str = w2c; } else { td = xstr->c; } xtd.wide = FALSE; - /* could this be a TIME-val? */ - if (/*hh:mm:ss*/8 <= td.cnt && td.str[2] == ':' && td.str[5] == ':') { + /* 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); + + dbc = HDRH(stmt)->dbc; + /* is this a SQL-format timestamp? (vs ISO8601) */ + if (td.str[DATE_TEMPLATE_LEN] == ' ') { /* vs. 'T' */ + td.str[DATE_TEMPLATE_LEN] = 'T'; + + /* if c2sql, apply_tz will tell if time is UTC already or not */ + if ((! sql2c) && dbc->apply_tz) { + /* TODO: does _timezone account for DST?? */ + hours = -_timezone / 3600; + mins = _timezone < 0 ? -_timezone : _timezone; + mins = mins % 3600; + assert(mins % 60 == 0); + mins /= 60; + n = snprintf(td.str + td.cnt, /*±00:00\0*/7, "%+03ld:%02ld", + hours, mins); + if (n <= 0) { + ERRNH(stmt, "failed to print TZ offset (%ld).", _timezone); + RET_HDIAGS(stmt, SQL_STATE_HY000); + } + td.cnt += (size_t)n; + } else { + /* TZ not applicable (UTC is used) or it's an ES-originated + * value => assume UTC: the query can construct a SQL + * timestamp that the app then wants translated to timestamp */ + td.str[td.cnt] = 'Z'; + td.cnt ++; + } + DBGH(stmt, "SQL format translated to ISO: [%zu] `" LCPDL "`.", + td.cnt, LCSTR(&td)); + } /* else: already in ISO8601 format */ + + xtd.c = td; + to_utc = sql2c ? (! dbc->apply_tz) : TRUE; + ret = parse_iso8601_timestamp(stmt, &xtd, to_utc, tss); + if (SQL_SUCCEEDED(ret) && format) { + *format = SQL_TYPE_TIMESTAMP; + } else { + ERRH(stmt, "`" LCPDL "` is not a TIMESTAMP value.", LCSTR(&td)); + } + return ret; + } + + /* could this be a TIME value? */ + if (TIME_TEMPLATE_LEN(0) <= td.cnt && + td.str[2] == ':' && td.str[5] == ':') { /* copy active value in template and parse it as TS */ /* copy is safe: cnt <= [time template] < [templ] */ - memcpy(templ + sizeof(ESODBC_DATE_TEMPLATE) - 1, td.str, td.cnt); + memcpy(templ + DATE_TEMPLATE_LEN + /*'T'*/1, td.str, td.cnt); /* there could be a varying number of fractional digits */ - templ[sizeof(ESODBC_DATE_TEMPLATE) - 1 + td.cnt] = 'Z'; + templ[DATE_TEMPLATE_LEN + /*'T'*/1 + td.cnt] = 'Z'; + xtd.c.cnt = td.cnt + DATE_TEMPLATE_LEN + /*'T'*/1 + /*Z*/1; xtd.c.str = templ; - xtd.c.cnt = td.cnt + sizeof(ESODBC_DATE_TEMPLATE); - if (! xstr_to_timestamp_struct(stmt, &xtd, tss, ts_buff)) { - ERRH(stmt, "`" LCPDL "` not a TIME.", LCSTR(&td)); - return FALSE; - } else { + + ret = parse_iso8601_timestamp(stmt, &xtd, /*to UTC*/TRUE, tss); + if (SQL_SUCCEEDED(ret)) { tss->year = tss->month = tss->day = 0; if (format) { *format = SQL_TYPE_TIME; } + } else { + ERRH(stmt, "`" LCPDL "` is not a TIME value.", LCSTR(&td)); } - return TRUE; + return ret; } - /* could this be a DATE-val? */ - if (/*yyyy-mm-dd*/10 <= td.cnt && td.str[4] == '-' && td.str[7] == '-') { + /* could this be a DATE value? */ + 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]) - 1; - if (! xstr_to_timestamp_struct(stmt, &xtd, tss, ts_buff)) { - ERRH(stmt, "`" LCPDL "` not a DATE.", LCSTR(&td)); - return FALSE; - } else { + xtd.c.cnt = sizeof(templ)/sizeof(templ[0]) - /*0*/1; + + ret = parse_iso8601_timestamp(stmt, &xtd, /*to UTC*/TRUE, tss); + if (SQL_SUCCEEDED(ret)) { tss->hour = tss->minute = tss->second = 0; tss->fraction = 0; if (format) { *format = SQL_TYPE_DATE; } + } else { + ERRH(stmt, "`" LCPDL "` is not a DATE value.", LCSTR(&td)); } - return TRUE; + return ret; } - ERRH(stmt, "`" LCPDL "` not a Time/Date/Timestamp.", LCSTR(&td)); - return FALSE; + ERRH(stmt, "`" LCPDL "` is not a TIME/DATE/TIMESTAMP value.", LCSTR(&td)); + RET_HDIAGS(stmt, SQL_STATE_22007); } /* * -> SQL_C_TYPE_TIMESTAMP * - * Conversts an ES/SQL 'date' or a text representation of a + * 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(esodbc_rec_st *arec, esodbc_rec_st *irec, - void *data_ptr, SQLLEN *octet_len_ptr, +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 = (xstr_st) { + xstr_st xstr = { .wide = TRUE, - .w = (wstr_st) { - (SQLWCHAR *)w_str, chars_0 - 1 - } + .w = {(SQLWCHAR *)w_str, chars_0 - 1} }; + BOOL to_utc; + SQLRETURN ret; if (octet_len_ptr) { *octet_len_ptr = sizeof(*tss); @@ -1644,21 +1707,38 @@ static SQLRETURN wstr_to_timestamp(esodbc_rec_st *arec, esodbc_rec_st *irec, /* right & left trim the data before attempting conversion */ wtrim_ws(&xstr.w); + /*INDENT-OFF*/ switch (irec->concise_type) { - case SQL_TYPE_TIMESTAMP: + do { case SQL_TYPE_DATE: /* ES/SQL passes date as DATETIME */ - if (! xstr_to_timestamp_struct(stmt, &xstr, tss, NULL)) { - RET_HDIAGS(stmt, SQL_STATE_22018); + to_utc = TRUE; + break; + case SQL_TYPE_TIMESTAMP: + to_utc = ! HDRH(stmt)->dbc->apply_tz; + 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_VARCHAR: - if (! parse_date_time_ts(stmt, &xstr, tss, format, NULL)) { - RET_HDIAGS(stmt, SQL_STATE_22018); + 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); + } } - break; + return ret; case SQL_CHAR: case SQL_LONGVARCHAR: @@ -1671,6 +1751,7 @@ static SQLRETURN wstr_to_timestamp(esodbc_rec_st *arec, esodbc_rec_st *irec, BUGH(stmt, "uncought invalid conversion."); RET_HDIAGS(stmt, SQL_STATE_07006); } + /*INDENT-ON*/ } else { DBGH(stmt, "REC@0x%p, NULL data_ptr", arec); } @@ -1681,7 +1762,7 @@ static SQLRETURN wstr_to_timestamp(esodbc_rec_st *arec, esodbc_rec_st *irec, /* * -> SQL_C_TYPE_DATE */ -static SQLRETURN wstr_to_date(esodbc_rec_st *arec, esodbc_rec_st *irec, +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) { @@ -1697,7 +1778,8 @@ static SQLRETURN wstr_to_date(esodbc_rec_st *arec, esodbc_rec_st *irec, } if (data_ptr) { - ret = wstr_to_timestamp(arec, irec, &tss, NULL, wstr, chars_0, &fmt); + ret = wstr_to_timestamp_struct(arec, irec, &tss, NULL, wstr, chars_0, + &fmt); if (! SQL_SUCCEEDED(ret)) { return ret; } @@ -1729,7 +1811,7 @@ static SQLRETURN wstr_to_date(esodbc_rec_st *arec, esodbc_rec_st *irec, /* * -> SQL_C_TYPE_TIME */ -static SQLRETURN wstr_to_time(esodbc_rec_st *arec, esodbc_rec_st *irec, +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) { @@ -1744,7 +1826,8 @@ static SQLRETURN wstr_to_time(esodbc_rec_st *arec, esodbc_rec_st *irec, } if (data_ptr) { - ret = wstr_to_timestamp(arec, irec, &tss, NULL, wstr, chars_0, &fmt); + ret = wstr_to_timestamp_struct(arec, irec, &tss, NULL, wstr, chars_0, + &fmt); if (! SQL_SUCCEEDED(ret)) { return ret; } @@ -2775,6 +2858,180 @@ static SQLRETURN interval_iso8601_to_sql(esodbc_rec_st *arec, *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; +} + +/* 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; + wchar_t *fmt; + SQLUINTEGER nsec; /* "fraction" */ +# define FMT_TIMESTAMP_MILLIS "%04d-%02d-%02d %02d:%02d:%02d.%lu" +# define FMT_TIMESTAMP_NOMILLIS "%04d-%02d-%02d %02d:%02d:%02d" + + /* see c2sql_timestamp() 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) { + fmt = MK_WPTR(FMT_TIMESTAMP_MILLIS); + assert(decdigits <= ESODBC_MAX_SEC_PRECISION); + nsec /= (SQLUINTEGER)pow10(ESODBC_MAX_SEC_PRECISION - decdigits); + } else { + fmt = MK_WPTR(FMT_TIMESTAMP_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' */ + nsec); + if ((int)lim < n) { + n = (int)lim; + } + if (0 < n) { + 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 ++; + } + DBG("printed UTC %s timestamp: [%d] `" LWPDL "`.", + iso8601 ? "ISO8601" : "SQL", n, n, dest); + } + + + return n; +# undef FMT_TIMESTAMP_MILLIS +# undef FMT_TIMESTAMP_NOMILLIS +} + +/* 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) { + 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; +} + +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; + + /* to make use of the buffer for date_time transformations */ + assert(ISO8601_TIMESTAMP_MAX_LEN <= INTERVAL_VAL_MAX_LEN); + + /* 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); + break; + case SQL_TYPE_TIMESTAMP: + ret = translate_timestamp_iso8601_to_sql(arec, irec, wstr, + &chars_0, buff); + break; + + case SQL_TYPE_TIME: + RET_HDIAGS(stmt, SQL_STATE_HYC00); // todo in ES/SQL + 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); + + return (get_rec_c_type(arec, irec) == 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); +} + /* * wstr: is 0-terminated and terminator is counted in 'chars_0'. * However: "[w]hen C strings are used to hold character data, the @@ -2789,36 +3046,14 @@ SQLRETURN sql2c_string(esodbc_rec_st *arec, esodbc_rec_st *irec, esodbc_stmt_st *stmt; void *data_ptr; SQLLEN *octet_len_ptr; - esodbc_desc_st *ard, *ird; SQLSMALLINT ctarget; long long ll; unsigned long long ull; wstr_st wval; double dbl; SQLWCHAR *endp; - wchar_t buff[INTERVAL_VAL_MAX_LEN + /*0*/1]; - SQLRETURN ret; - - /* The interval strings are received from ES in ISO8601, not SQL format: - * if received value is of type interval, translate the format and set the - * local `wstr` to the static buffer containing the translation. - * Uses local vars: irec, ret, wstr. - * Returns on failure. */ -# define INTERVAL_ISO8601_TO_SQL() \ - do { \ - if (irec->type != SQL_INTERVAL) { \ - break; \ - } \ - ret = interval_iso8601_to_sql(arec, irec, wstr, &chars_0, buff); \ - if (! SQL_SUCCEEDED(ret)) { \ - return ret; \ - } \ - wstr = buff; \ - } while (0) stmt = arec->desc->hdr.stmt; - ird = stmt->ird; - ard = stmt->ard; assert(1 <= chars_0); /* _0 is really counted */ @@ -2830,23 +3065,19 @@ SQLRETURN sql2c_string(esodbc_rec_st *arec, esodbc_rec_st *irec, ctarget = get_rec_c_type(arec, irec); switch (ctarget) { case SQL_C_CHAR: - INTERVAL_ISO8601_TO_SQL(); - return wstr_to_cstr(arec, irec, data_ptr, octet_len_ptr, - wstr, chars_0); case SQL_C_BINARY: /* treat binary as WCHAR */ // TODO: add \0??? case SQL_C_WCHAR: - INTERVAL_ISO8601_TO_SQL(); - return wstr_to_wstr(arec, irec, data_ptr, octet_len_ptr, + return wstr_to_string(arec, irec, data_ptr, octet_len_ptr, wstr, chars_0); case SQL_C_TYPE_TIMESTAMP: - return wstr_to_timestamp(arec, irec, data_ptr, octet_len_ptr, - wstr, chars_0, NULL); + 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(arec, irec, data_ptr, octet_len_ptr, + return wstr_to_date_struct(arec, irec, data_ptr, octet_len_ptr, wstr, chars_0); case SQL_C_TYPE_TIME: - return wstr_to_time(arec, irec, data_ptr, octet_len_ptr, + return wstr_to_time_struct(arec, irec, data_ptr, octet_len_ptr, wstr, chars_0); } @@ -3616,14 +3847,15 @@ SQLRETURN c2sql_number(esodbc_rec_st *arec, esodbc_rec_st *irec, return SQL_SUCCESS; } -static SQLRETURN convert_str_to_timestamp(esodbc_stmt_st *stmt, +static SQLRETURN str_to_iso8601_timestamp(esodbc_stmt_st *stmt, SQLLEN *octet_len_ptr, void *data_ptr, BOOL wide, - char *dest, size_t *len) + SQLULEN colsize, SQLSMALLINT decdigits, wchar_t *dest, size_t *cnt) { xstr_st xstr; TIMESTAMP_STRUCT tss; SQLSMALLINT format; - cstr_st ts_buff; + int n; + SQLRETURN ret; xstr.wide = wide; @@ -3645,31 +3877,33 @@ static SQLRETURN convert_str_to_timestamp(esodbc_stmt_st *stmt, trim_ws(&xstr.c); } - assert(dest); - ts_buff.str = dest; - ts_buff.cnt = sizeof(ESODBC_ISO8601_TEMPLATE) - 1; - if (! parse_date_time_ts(stmt, &xstr, &tss, &format, &ts_buff)) { - ERRH(stmt, "failed to parse input as Time/Date/Timestamp"); - RET_HDIAGS(stmt, SQL_STATE_22008); + 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 + } } else if (format == SQL_TYPE_TIME) { - ERRH(stmt, "can not convert a Time to a Timestamp value"); + ERRH(stmt, "can not convert a TIME to a TIMESTAMP value"); RET_HDIAGS(stmt, SQL_STATE_22018); - } else { - /* conversion from TIME to TIMESTAMP should have been deined earlier */ - assert(format != SQL_TYPE_TIME); - *len += ts_buff.cnt; } + 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 convert_ts_to_timestamp(esodbc_stmt_st *stmt, +static SQLRETURN struct_to_iso8601_timestamp(esodbc_stmt_st *stmt, SQLLEN *octet_len_ptr, void *data_ptr, SQLSMALLINT ctype, - char *dest, size_t *len) + SQLULEN colsize, SQLSMALLINT decdigits, wchar_t *dest, size_t *cnt) { TIMESTAMP_STRUCT *tss, buff; DATE_STRUCT *ds; - int cnt; + int n; size_t osize; switch (ctype) { @@ -3704,16 +3938,12 @@ static SQLRETURN convert_ts_to_timestamp(esodbc_stmt_st *stmt, RET_HDIAG(stmt, SQL_STATE_HY000, "param conversion bug", 0); } - assert(dest); - cnt = snprintf(dest, sizeof(ESODBC_ISO8601_TEMPLATE) - 1, - "%04d-%02d-%02dT%02d:%02d:%02d.%03uZ", - tss->year, tss->month, tss->day, - tss->hour, tss->minute, tss->second, tss->fraction); - if (cnt < 0) { - ERRH(stmt, "failed printing timestamp struct: %s.", strerror(errno)); - RET_HDIAG(stmt, SQL_STATE_HY000, "C runtime error", 0); + n = print_timestamp(tss, /*ISO?*/TRUE, colsize, decdigits, dest); + if (n <= 0) { + ERRNH(stmt, "printing TIMESTAMP failed."); + RET_HDIAGS(stmt, SQL_STATE_HY000); } - *len = cnt; + *cnt = (size_t)n; return SQL_SUCCESS; } @@ -3721,38 +3951,64 @@ static SQLRETURN convert_ts_to_timestamp(esodbc_stmt_st *stmt, SQLRETURN c2sql_timestamp(esodbc_rec_st *arec, esodbc_rec_st *irec, SQLULEN pos, char *dest, size_t *len) { + esodbc_stmt_st *stmt; void *data_ptr; SQLLEN *octet_len_ptr; SQLSMALLINT ctype; SQLRETURN ret; - SQLULEN colsize, offt, decdigits; - esodbc_stmt_st *stmt = arec->desc->hdr.stmt; + SQLULEN colsize; + SQLSMALLINT decdigits; + wchar_t wbuff[ISO8601_TIMESTAMP_MAX_LEN + /*\0*/1]; + size_t cnt; + int n; if (! dest) { /* maximum possible space it can take */ - *len = /*2x `"`*/2 + sizeof(ESODBC_ISO8601_TEMPLATE) - 1; + *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); + /* 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 + * */ + colsize = get_param_size(irec); + DBGH(stmt, "requested column size: %llu.", colsize); + if (colsize && (colsize < sizeof("yyyy-mm-dd hh:mm") - 1 || + colsize == 17 || colsize == 18)) { + ERRH(stmt, "invalid column size value: %llu; allowed: 16, 19, 20+f.", + colsize); + RET_HDIAGS(stmt, SQL_STATE_HY104); + } + decdigits = get_param_decdigits(irec); + DBGH(stmt, "requested decimal digits: %llu.", 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 ((ctype = get_rec_c_type(arec, irec))) { case SQL_C_CHAR: case SQL_C_WCHAR: - ret = convert_str_to_timestamp(stmt, octet_len_ptr, data_ptr, - ctype == SQL_C_WCHAR, dest + /*`"`*/1, len); + ret = str_to_iso8601_timestamp(stmt, octet_len_ptr, data_ptr, + ctype == SQL_C_WCHAR, colsize, decdigits, wbuff, &cnt); if (! SQL_SUCCEEDED(ret)) { return ret; } break; case SQL_C_TYPE_TIME: - // TODO + // TODO; ES just prepends the Epoch date ERRH(stmt, "conversion from time to timestamp not implemented."); RET_HDIAG(stmt, SQL_STATE_HYC00, "conversion time to timestamp " "not yet supported", 0); @@ -3760,8 +4016,8 @@ SQLRETURN c2sql_timestamp(esodbc_rec_st *arec, esodbc_rec_st *irec, case SQL_C_TYPE_DATE: case SQL_C_BINARY: case SQL_C_TYPE_TIMESTAMP: - ret = convert_ts_to_timestamp(stmt, octet_len_ptr, data_ptr, - ctype, dest + /*`"`*/1, len); + ret = struct_to_iso8601_timestamp(stmt, octet_len_ptr, data_ptr, + ctype, colsize, decdigits, wbuff, &cnt); if (! SQL_SUCCEEDED(ret)) { return ret; } @@ -3772,55 +4028,9 @@ SQLRETURN c2sql_timestamp(esodbc_rec_st *arec, esodbc_rec_st *irec, RET_HDIAG(stmt, SQL_STATE_HY000, "bug converting parameter", 0); } - /* 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 - * */ - colsize = get_param_size(irec); - DBGH(stmt, "requested column size: %llu.", colsize); - if (colsize) { - if (colsize < sizeof("yyyy-mm-dd hh:mm") - 1 || - colsize == 17 || colsize == 18 ) { - ERRH(stmt, "invalid column size value: %llu; " - "allowed: 16, 19, 20+f.", colsize); - RET_HDIAGS(stmt, SQL_STATE_HY104); - } else if (colsize == sizeof("yyyy-mm-dd hh:mm") - 1) { - offt = sizeof("yyyy-mm-ddThh:mm:") - 1; - offt += /*leading `"`*/1; - dest[offt ++] = '0'; - dest[offt ++] = '0'; - dest[offt ++] = 'Z'; - *len = offt; - } else if (colsize == sizeof("yyyy-mm-dd hh:mm:ss") - 1) { - offt = sizeof("yyyy-mm-ddThh:mm:ss") - 1; - offt += /*leading `"`*/1; - dest[offt ++] = 'Z'; - *len = offt; - } else { - assert(20 < colsize); - decdigits = get_param_decdigits(irec); - DBGH(stmt, "requested decimal digits: %llu.", decdigits); - if (/*count of fractions in ISO8601 template*/7 < decdigits) { - INFOH(stmt, "decimal digits value (%hd) reset to 7."); - decdigits = 7; - } else if (decdigits == 0) { - decdigits = -1; /* shave the `.` away */ - } - if (colsize < decdigits + sizeof("yyyy-mm-ddThh:mm:ss.") - 1) { - decdigits = colsize - (sizeof("yyyy-mm-ddThh:mm:ss.") - 1); - WARNH(stmt, "column size adjusted to %hd to fit into a %llu" - " columns size.", decdigits, colsize); - } - offt = sizeof("yyyy-mm-ddThh:mm:ss.") - 1; - offt += /*leading `"`*/1; - offt += decdigits; - dest[offt ++] = 'Z'; - *len = offt; - } - } else { - WARNH(stmt, "column size given 0 -- column size check skipped"); - (*len) ++; /* initial `"` */ - } + 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; diff --git a/driver/defs.h b/driver/defs.h index 7e44bb76..cea27a73 100644 --- a/driver/defs.h +++ b/driver/defs.h @@ -21,6 +21,9 @@ /* number of consecutive logging failures that will disable logging */ #define ESODBC_LOG_MAX_RETRY 5 +/* the (POSIX) timezone environment variable */ +#define ESODBC_TZ_ENV_VAR "TZ" + #define ESODBC_MAX_ROW_ARRAY_SIZE USHRT_MAX /* max number of ES/SQL types supported */ #define ESODBC_MAX_NO_TYPES 64 @@ -79,6 +82,7 @@ /* Seconds precision is currently 3, with ES/SQL's ISO8601 millis. * (Should move to 9 with nanosecond implementation) */ #define ESODBC_MAX_SEC_PRECISION 9 +#define ESODBC_DEF_SEC_PRECISION 3 /* * standard specified defaults: * https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqlsetdescfield-function##record-fields @@ -165,6 +169,8 @@ #define ESODBC_DEF_TRACE_ENABLED "0" /* default tracing level */ #define ESODBC_DEF_TRACE_LEVEL "WARN" +/* default TZ handling */ +#define ESODBC_DEF_APPLY_TZ "no" #define ESODBC_PWD_VAL_SUBST "" /* default version checking mode: strict, major, none (dbg only) */ #define ESODBC_DEF_VERSION_CHECKING ESODBC_DSN_VC_STRICT @@ -383,13 +389,6 @@ #define ESODBC_SQL_OBJECT 2002 #define ESODBC_SQL_NESTED 2002 -/* - * ISO8601 template ('yyyy-mm-ddThh:mm:ss.sss+hh:mm') - */ -#define ESODBC_ISO8601_TEMPLATE "yyyy-mm-ddThh:mm:ss.sssssssZ" -/* https://docs.microsoft.com/en-us/sql/relational-databases/native-client-odbc-date-time/data-type-support-for-odbc-date-and-time-improvements */ -#define ESODBC_DATE_TEMPLATE "yyyy-mm-ddT" -#define ESODBC_TIME_TEMPLATE "hh:mm:ss.9999999" #endif /* __DEFS_H__ */ diff --git a/driver/dsn.c b/driver/dsn.c index e9746f73..b0cf99ea 100644 --- a/driver/dsn.c +++ b/driver/dsn.c @@ -73,6 +73,7 @@ int assign_dsn_attr(esodbc_dsn_attrs_st *attrs, {&MK_WSTR(ESODBC_DSN_PACKING), &attrs->packing}, {&MK_WSTR(ESODBC_DSN_MAX_FETCH_SIZE), &attrs->max_fetch_size}, {&MK_WSTR(ESODBC_DSN_MAX_BODY_SIZE_MB), &attrs->max_body_size}, + {&MK_WSTR(ESODBC_DSN_APPLY_TZ), &attrs->apply_tz}, {&MK_WSTR(ESODBC_DSN_VERSION_CHECKING), &attrs->version_checking}, {&MK_WSTR(ESODBC_DSN_TRACE_ENABLED), &attrs->trace_enabled}, {&MK_WSTR(ESODBC_DSN_TRACE_FILE), &attrs->trace_file}, @@ -401,6 +402,7 @@ long TEST_API write_00_list(esodbc_dsn_attrs_st *attrs, {&MK_WSTR(ESODBC_DSN_PACKING), &attrs->packing}, {&MK_WSTR(ESODBC_DSN_MAX_FETCH_SIZE), &attrs->max_fetch_size}, {&MK_WSTR(ESODBC_DSN_MAX_BODY_SIZE_MB), &attrs->max_body_size}, + {&MK_WSTR(ESODBC_DSN_APPLY_TZ), &attrs->apply_tz}, {&MK_WSTR(ESODBC_DSN_VERSION_CHECKING), &attrs->version_checking}, {&MK_WSTR(ESODBC_DSN_TRACE_ENABLED), &attrs->trace_enabled}, {&MK_WSTR(ESODBC_DSN_TRACE_FILE), &attrs->trace_file}, @@ -649,6 +651,10 @@ BOOL write_system_dsn(esodbc_dsn_attrs_st *new_attrs, &MK_WSTR(ESODBC_DSN_MAX_BODY_SIZE_MB), &new_attrs->max_body_size, old_attrs ? &old_attrs->max_body_size : NULL }, + { + &MK_WSTR(ESODBC_DSN_APPLY_TZ), &new_attrs->apply_tz, + old_attrs ? &old_attrs->apply_tz : NULL + }, { &MK_WSTR(ESODBC_DSN_VERSION_CHECKING), &new_attrs->version_checking, @@ -738,6 +744,7 @@ long TEST_API write_connection_string(esodbc_dsn_attrs_st *attrs, {&attrs->packing, &MK_WSTR(ESODBC_DSN_PACKING)}, {&attrs->max_fetch_size, &MK_WSTR(ESODBC_DSN_MAX_FETCH_SIZE)}, {&attrs->max_body_size, &MK_WSTR(ESODBC_DSN_MAX_BODY_SIZE_MB)}, + {&attrs->apply_tz, &MK_WSTR(ESODBC_DSN_APPLY_TZ)}, {&attrs->version_checking, &MK_WSTR(ESODBC_DSN_VERSION_CHECKING)}, {&attrs->trace_enabled, &MK_WSTR(ESODBC_DSN_TRACE_ENABLED)}, {&attrs->trace_file, &MK_WSTR(ESODBC_DSN_TRACE_FILE)}, @@ -821,6 +828,10 @@ void assign_dsn_defaults(esodbc_dsn_attrs_st *attrs) &MK_WSTR(ESODBC_DEF_VERSION_CHECKING), /*overwrite?*/FALSE); + res |= assign_dsn_attr(attrs, + &MK_WSTR(ESODBC_DSN_APPLY_TZ), &MK_WSTR(ESODBC_DEF_APPLY_TZ), + /*overwrite?*/FALSE); + /* default: no trace file */ res |= assign_dsn_attr(attrs, &MK_WSTR(ESODBC_DSN_TRACE_ENABLED), diff --git a/driver/dsn.h b/driver/dsn.h index 6ba451e3..3ce510ef 100644 --- a/driver/dsn.h +++ b/driver/dsn.h @@ -32,6 +32,7 @@ #define ESODBC_DSN_PACKING "Packing" #define ESODBC_DSN_MAX_FETCH_SIZE "MaxFetchSize" #define ESODBC_DSN_MAX_BODY_SIZE_MB "MaxBodySizeMB" +#define ESODBC_DSN_APPLY_TZ "ApplyTZ" #define ESODBC_DSN_VERSION_CHECKING "VersionChecking" #define ESODBC_DSN_TRACE_ENABLED "TraceEnabled" #define ESODBC_DSN_TRACE_FILE "TraceFile" @@ -64,11 +65,12 @@ typedef struct { wstr_st packing; wstr_st max_fetch_size; wstr_st max_body_size; + wstr_st apply_tz; wstr_st version_checking; wstr_st trace_enabled; wstr_st trace_file; wstr_st trace_level; -#define ESODBC_DSN_ATTRS_COUNT 21 +#define ESODBC_DSN_ATTRS_COUNT 22 SQLWCHAR buff[ESODBC_DSN_ATTRS_COUNT * ESODBC_DSN_MAX_ATTR_LEN]; } esodbc_dsn_attrs_st; diff --git a/driver/handles.h b/driver/handles.h index 0f342bce..ba8c5b98 100644 --- a/driver/handles.h +++ b/driver/handles.h @@ -156,6 +156,7 @@ typedef struct struct_dbc { char slen; /* string's length (w/o terminator) */ } fetch; BOOL pack_json; /* should JSON be used in REST bodies? (vs. CBOR) */ + BOOL apply_tz; /* should the times be converted from UTC to local TZ? */ esodbc_estype_st *es_types; /* array with ES types */ SQLULEN no_types; /* number of types in array */ diff --git a/driver/log.c b/driver/log.c index c064c113..accbc126 100644 --- a/driver/log.c +++ b/driver/log.c @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +#include #include #include #include diff --git a/driver/queries.c b/driver/queries.c index 774accc6..24ecb636 100644 --- a/driver/queries.c +++ b/driver/queries.c @@ -2261,7 +2261,7 @@ static inline SQLSMALLINT get_col_decdigits(esodbc_rec_st *rec) switch (rec->meta_type) { case METATYPE_DATE_TIME: case METATYPE_INTERVAL_WSEC: - return ESODBC_MAX_SEC_PRECISION; + return ESODBC_DEF_SEC_PRECISION; case METATYPE_EXACT_NUMERIC: return rec->es_type->maximum_scale; diff --git a/driver/util.h b/driver/util.h index 5a508ce9..2a4abb5e 100644 --- a/driver/util.h +++ b/driver/util.h @@ -303,6 +303,24 @@ cstr_st TEST_API *wstr_to_utf8(wstr_st *src, cstr_st *dst); #endif /* _WIN32 */ +/* ISO time formats lenghts. + * ES/SQL interface should only use UTC ('Z'ulu offset). */ +#define ISO8601_TIMESTAMP_LEN(prec) \ + (sizeof("yyyy-mm-ddThh:mm:ss+hh:mm") - /*\0*/1 + /*'.'*/!!prec + prec) +#define ISO8601_TS_UTC_LEN(prec) \ + (sizeof("yyyy-mm-ddThh:mm:ssZ") - /*\0*/1 + /*'.'*/!!prec + prec) +#define ISO8601_TIMESTAMP_MAX_LEN \ + ISO8601_TIMESTAMP_LEN(ESODBC_MAX_SEC_PRECISION) +#define ISO8601_TIMESTAMP_MIN_LEN \ + ISO8601_TS_UTC_LEN(0) + +#define DATE_TEMPLATE_LEN \ + (sizeof("yyyy-mm-dd") - /*\0*/1) +#define TIME_TEMPLATE_LEN(prec) \ + (sizeof("hh:mm:ss") - /*\0*/1 + /*'.'*/!!prec + prec) +#define TIMESTAMP_TEMPLATE_LEN(prec) \ + (DATE_TEMPLATE_LEN + /*' '*/1 + TIME_TEMPLATE_LEN(prec)) + #endif /* __UTIL_H__ */ /* vim: set noet fenc=utf-8 ff=dos sts=0 sw=4 ts=4 : */ From 12e21ecec06b99b820a04008df04a2e0e8fcefd9 Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Wed, 27 Feb 2019 14:47:11 +0100 Subject: [PATCH 3/4] add the integration tests for the timezone support - the commit also corrects some of the existing tests to respect the column size and/or decimal digits. --- test/connected_dbc.cc | 2 + test/test_conversion_c2sql_timestamp.cc | 116 +++++++- test/test_conversion_sql2c_date.cc | 172 +++++++---- test/test_conversion_sql2c_time.cc | 161 ++++++---- test/test_conversion_sql2c_timestamp.cc | 378 +++++++++++++++++------- 5 files changed, 596 insertions(+), 233 deletions(-) diff --git a/test/connected_dbc.cc b/test/connected_dbc.cc index b1b32e69..855cc1fa 100644 --- a/test/connected_dbc.cc +++ b/test/connected_dbc.cc @@ -121,6 +121,8 @@ ConnectedDBC::ConnectedDBC() SQLRETURN ret; cstr_st types; + assert(getenv("TZ") == NULL); + ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env); assert(SQL_SUCCEEDED(ret)); diff --git a/test/test_conversion_c2sql_timestamp.cc b/test/test_conversion_c2sql_timestamp.cc index 99592435..46dc8cdf 100644 --- a/test/test_conversion_c2sql_timestamp.cc +++ b/test/test_conversion_c2sql_timestamp.cc @@ -88,7 +88,7 @@ TEST_F(ConvertC2SQL_Timestamp, WStr_Timestamp2Timestamp_colsize_16) cstr_st expect = CSTR_INIT( "{\"query\": \"WStr_Timestamp2Timestamp_colsize_16\", " "\"params\": [{\"type\": \"DATETIME\", " - "\"value\": \"1234-12-23T12:34:00Z\"}], " + "\"value\": \"1234-12-23T12:34Z\"}], " "\"mode\": \"ODBC\", " CLIENT_ID "}"); ASSERT_CSTREQ(buff, expect); @@ -194,7 +194,7 @@ TEST_F(ConvertC2SQL_Timestamp, Timestamp2Timestamp_decdigits_7) val.hour = 12; val.minute = 34; val.second = 56; - val.fraction = 78901234; + val.fraction = 789012340; ret = SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_TYPE_TIMESTAMP, SQL_TYPE_TIMESTAMP, /*size*/35, /*decdigits*/7, &val, sizeof(val), /*IndLen*/NULL); @@ -239,7 +239,7 @@ TEST_F(ConvertC2SQL_Timestamp, Binary2Timestamp_colsize_0) cstr_st expect = CSTR_INIT( "{\"query\": \"Binary2Timestamp_colsize_0\", " "\"params\": [{\"type\": \"DATETIME\", " - "\"value\": \"2345-01-23T12:34:56.789Z\"}], " + "\"value\": \"2345-01-23T12:34:56Z\"}], " "\"mode\": \"ODBC\", " CLIENT_ID "}"); ASSERT_CSTREQ(buff, expect); @@ -267,7 +267,7 @@ TEST_F(ConvertC2SQL_Timestamp, Date2Timestamp) cstr_st expect = CSTR_INIT( "{\"query\": \"Date2Timestamp\", " "\"params\": [{\"type\": \"DATETIME\", " - "\"value\": \"2345-01-23T00:00:00.000Z\"}], " + "\"value\": \"2345-01-23T00:00:00Z\"}], " "\"mode\": \"ODBC\", " CLIENT_ID "}"); ASSERT_CSTREQ(buff, expect); @@ -290,6 +290,114 @@ TEST_F(ConvertC2SQL_Timestamp, Time2Timestamp_unimplemented_HYC00) } +class ConvertC2SQL_Timestamp_TZ : public ConvertC2SQL_Timestamp +{ + protected: + void SetUp() override + { + ((esodbc_dbc_st *)dbc)->apply_tz = TRUE; + ASSERT_EQ(putenv("TZ=NPT-5:45NTP"), 0); + tzset(); + } + + void TearDown() override + { + ASSERT_EQ(putenv("TZ="), 0); + } +}; + +TEST_F(ConvertC2SQL_Timestamp_TZ, WStr_iso8601_Timestamp2Timestamp_colsize_16) +{ + prepareStatement(); + + SQLWCHAR val[] = L"1234-12-23T12:34:56.7890123Z"; + ret = SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_WCHAR, + SQL_TYPE_TIMESTAMP, /*size*/16, /*decdigits*/10, val, sizeof(val), + /*IndLen*/NULL); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + cstr_st buff = {NULL, 0}; + ret = serialize_statement((esodbc_stmt_st *)stmt, &buff); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + cstr_st expect = CSTR_INIT( + "{\"query\": \"WStr_iso8601_Timestamp2Timestamp_colsize_16\", " + "\"params\": [{\"type\": \"DATETIME\", " + "\"value\": \"1234-12-23T12:34Z\"}], " + "\"mode\": \"ODBC\", " CLIENT_ID "}"); + + ASSERT_CSTREQ(buff, expect); +} + +TEST_F(ConvertC2SQL_Timestamp_TZ, WStr_iso8601_Timestamp2Timestamp_sz23_dd4) +{ + prepareStatement(); + + SQLWCHAR val[] = L"1234-12-23T12:34:56.7890123Z"; + ret = SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_WCHAR, + SQL_TYPE_TIMESTAMP, /*size*/23, /*decdigits*/4, val, sizeof(val), + /*IndLen*/NULL); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + cstr_st buff = {NULL, 0}; + ret = serialize_statement((esodbc_stmt_st *)stmt, &buff); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + cstr_st expect = CSTR_INIT( + "{\"query\": \"WStr_iso8601_Timestamp2Timestamp_sz23_dd4\", " + "\"params\": [{\"type\": \"DATETIME\", " + "\"value\": \"1234-12-23T12:34:56.789Z\"}], " + "\"mode\": \"ODBC\", " CLIENT_ID "}"); + + ASSERT_CSTREQ(buff, expect); +} + +TEST_F(ConvertC2SQL_Timestamp_TZ, WStr_iso8601_Timestamp2Timestamp_decdig4) +{ + prepareStatement(); + + SQLWCHAR val[] = L"1234-12-23T12:34:56.7890123Z"; + ret = SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_WCHAR, + SQL_TYPE_TIMESTAMP, /*size*/0, /*decdigits*/4, val, sizeof(val), + /*IndLen*/NULL); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + cstr_st buff = {NULL, 0}; + ret = serialize_statement((esodbc_stmt_st *)stmt, &buff); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + cstr_st expect = CSTR_INIT( + "{\"query\": \"WStr_iso8601_Timestamp2Timestamp_decdig4\", " + "\"params\": [{\"type\": \"DATETIME\", " + "\"value\": \"1234-12-23T12:34:56.7890Z\"}], " + "\"mode\": \"ODBC\", " CLIENT_ID "}"); + + ASSERT_CSTREQ(buff, expect); +} + +TEST_F(ConvertC2SQL_Timestamp_TZ, WStr_SQL_Timestamp_local2Timestamp) +{ + prepareStatement(); + + SQLWCHAR val[] = L"2000-12-23 17:45:56.7890123"; + ret = SQLBindParameter(stmt, 1, SQL_PARAM_INPUT, SQL_C_WCHAR, + SQL_TYPE_TIMESTAMP, /*size*/0, /*decdigits*/4, val, sizeof(val), + /*IndLen*/NULL); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + cstr_st buff = {NULL, 0}; + ret = serialize_statement((esodbc_stmt_st *)stmt, &buff); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + cstr_st expect = CSTR_INIT( + "{\"query\": \"WStr_SQL_Timestamp_local2Timestamp\", " + "\"params\": [{\"type\": \"DATETIME\", " + "\"value\": \"2000-12-23T12:00:56.7890Z\"}], " + "\"mode\": \"ODBC\", " CLIENT_ID "}"); + + ASSERT_CSTREQ(buff, expect); +} + } // test namespace /* vim: set noet fenc=utf-8 ff=dos sts=0 sw=4 ts=4 : */ diff --git a/test/test_conversion_sql2c_date.cc b/test/test_conversion_sql2c_date.cc index a4bcc058..3fc21d39 100644 --- a/test/test_conversion_sql2c_date.cc +++ b/test/test_conversion_sql2c_date.cc @@ -15,30 +15,31 @@ namespace test { -class ConvertSQL2C_Date : public ::testing::Test, public ConnectedDBC { - - protected: - DATE_STRUCT ds; - - void prepareAndBind(const char *json_answer) { - prepareStatement(json_answer); - - ret = SQLBindCol(stmt, /*col#*/1, SQL_C_TYPE_DATE, &ds, sizeof(ds), - &ind_len); - ASSERT_TRUE(SQL_SUCCEEDED(ret)); - } +class ConvertSQL2C_Date : public ::testing::Test, public ConnectedDBC +{ + protected: + DATE_STRUCT ds; + + void prepareAndBind(const char *json_answer) + { + prepareStatement(json_answer); + + ret = SQLBindCol(stmt, /*col#*/1, SQL_C_TYPE_DATE, &ds, sizeof(ds), + &ind_len); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + } }; /* ES/SQL 'date' is actually 'timestamp' */ -TEST_F(ConvertSQL2C_Date, Timestamp2Date) { - +TEST_F(ConvertSQL2C_Date, Timestamp2Date) +{ #undef SQL_VAL #undef SQL #define SQL_VAL "2345-01-23T00:00:00Z" #define SQL "CAST(" SQL_VAL "AS DATETIME)" - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"" SQL "\", \"type\": \"DATETIME\"}\ @@ -48,25 +49,83 @@ TEST_F(ConvertSQL2C_Date, Timestamp2Date) { ]\ }\ "; - prepareAndBind(json_answer); + prepareAndBind(json_answer); - ret = SQLFetch(stmt); - ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); - EXPECT_EQ(ind_len, sizeof(ds)); - EXPECT_EQ(ds.year, 2345); - EXPECT_EQ(ds.month, 1); - EXPECT_EQ(ds.day, 23); + EXPECT_EQ(ind_len, sizeof(ds)); + EXPECT_EQ(ds.year, 2345); + EXPECT_EQ(ds.month, 1); + EXPECT_EQ(ds.day, 23); } -TEST_F(ConvertSQL2C_Date, Timestamp2Date_truncate) { +TEST_F(ConvertSQL2C_Date, Date2Char) +{ +#undef SQL_VAL +#undef SQL +#define SQL_VAL "2345-01-23T00:00:00Z" +#define SQL "CAST(" SQL_VAL "AS DATE)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"DATE\"}\ + ],\ + \"rows\": [\ + [\"" SQL_VAL "\"]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLCHAR val[sizeof(SQL_VAL)]; + ret = SQLBindCol(stmt, /*col#*/1, SQL_C_CHAR, val, sizeof(val), &ind_len); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + EXPECT_EQ(ind_len, DATE_TEMPLATE_LEN); + EXPECT_EQ(strncmp((char *)val, SQL_VAL, DATE_TEMPLATE_LEN), 0); +} +TEST_F(ConvertSQL2C_Date, Timestamp_Str2Date) +{ +#undef SQL_VAL +#undef SQL +#define SQL_VAL "2345-01-23T00:00:00Z" +#define SQL "CAST(" SQL_VAL "AS KEYWORD)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"KEYWORD\"}\ + ],\ + \"rows\": [\ + [\"" SQL_VAL "\"]\ + ]\ +}\ +"; + prepareAndBind(json_answer); + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + EXPECT_EQ(ind_len, sizeof(ds)); + EXPECT_EQ(ds.year, 2345); + EXPECT_EQ(ds.month, 1); + EXPECT_EQ(ds.day, 23); +} + +TEST_F(ConvertSQL2C_Date, Timestamp2Date_truncate) +{ #undef SQL_VAL #undef SQL #define SQL_VAL " 2345-01-23T12:34:56.789Z " #define SQL "CAST(" SQL_VAL "AS DATETIME)" - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"CAST(" SQL ")\", \"type\": \"DATETIME\"}\ @@ -76,27 +135,27 @@ TEST_F(ConvertSQL2C_Date, Timestamp2Date_truncate) { ]\ }\ "; - prepareAndBind(json_answer); + prepareAndBind(json_answer); - ret = SQLFetch(stmt); - ASSERT_TRUE(SQL_SUCCEEDED(ret)); - assertState(L"01S07"); + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + assertState(L"01S07"); - EXPECT_EQ(ind_len, sizeof(ds)); - EXPECT_EQ(ds.year, 2345); - EXPECT_EQ(ds.month, 1); - EXPECT_EQ(ds.day, 23); + EXPECT_EQ(ind_len, sizeof(ds)); + EXPECT_EQ(ds.year, 2345); + EXPECT_EQ(ds.month, 1); + EXPECT_EQ(ds.day, 23); } -TEST_F(ConvertSQL2C_Date, Date2Date) { - +TEST_F(ConvertSQL2C_Date, Date2Date) +{ #undef SQL_VAL #undef SQL #define SQL_VAL "2345-01-23" #define SQL "CAST(" SQL_VAL "AS TEXT)" - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"" SQL "\", \"type\": \"text\"}\ @@ -106,26 +165,26 @@ TEST_F(ConvertSQL2C_Date, Date2Date) { ]\ }\ "; - prepareAndBind(json_answer); + prepareAndBind(json_answer); - ret = SQLFetch(stmt); - ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); - EXPECT_EQ(ind_len, sizeof(ds)); - EXPECT_EQ(ds.year, 2345); - EXPECT_EQ(ds.month, 1); - EXPECT_EQ(ds.day, 23); + EXPECT_EQ(ind_len, sizeof(ds)); + EXPECT_EQ(ds.year, 2345); + EXPECT_EQ(ds.month, 1); + EXPECT_EQ(ds.day, 23); } -TEST_F(ConvertSQL2C_Date, Time2Date_22018) { - +TEST_F(ConvertSQL2C_Date, Time2Date_22018) +{ #undef SQL_VAL #undef SQL #define SQL_VAL "10:10:10.1010" #define SQL "CAST(" SQL_VAL "AS TEXT)" - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"" SQL "\", \"type\": \"text\"}\ @@ -135,22 +194,22 @@ TEST_F(ConvertSQL2C_Date, Time2Date_22018) { ]\ }\ "; - prepareAndBind(json_answer); + prepareAndBind(json_answer); - ret = SQLFetch(stmt); - ASSERT_FALSE(SQL_SUCCEEDED(ret)); - assertState(L"22018"); + ret = SQLFetch(stmt); + ASSERT_FALSE(SQL_SUCCEEDED(ret)); + assertState(L"22018"); } -TEST_F(ConvertSQL2C_Date, Integer2Date_violation_07006) { - +TEST_F(ConvertSQL2C_Date, Integer2Date_violation_07006) +{ #undef SQL_VAL #undef SQL #define SQL_VAL "1" #define SQL "select " SQL_VAL - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"select " SQL "\", \"type\": \"integer\"}\ @@ -160,13 +219,14 @@ TEST_F(ConvertSQL2C_Date, Integer2Date_violation_07006) { ]\ }\ "; - prepareAndBind(json_answer); + prepareAndBind(json_answer); - ret = SQLFetch(stmt); - ASSERT_FALSE(SQL_SUCCEEDED(ret)); - assertState(L"07006"); + ret = SQLFetch(stmt); + ASSERT_FALSE(SQL_SUCCEEDED(ret)); + assertState(L"07006"); } } // test namespace +/* vim: set noet fenc=utf-8 ff=dos sts=0 sw=4 ts=4 tw=78 : */ diff --git a/test/test_conversion_sql2c_time.cc b/test/test_conversion_sql2c_time.cc index 47fc336f..141c7ec0 100644 --- a/test/test_conversion_sql2c_time.cc +++ b/test/test_conversion_sql2c_time.cc @@ -15,30 +15,32 @@ namespace test { -class ConvertSQL2C_Time : public ::testing::Test, public ConnectedDBC { +class ConvertSQL2C_Time : public ::testing::Test, public ConnectedDBC +{ - protected: - TIME_STRUCT ts; + protected: + TIME_STRUCT ts; - void prepareAndBind(const char *jsonAnswer) { - prepareStatement(jsonAnswer); + void prepareAndBind(const char *jsonAnswer) + { + prepareStatement(jsonAnswer); - ret = SQLBindCol(stmt, /*col#*/1, SQL_C_TYPE_TIME, &ts, sizeof(ts), - &ind_len); - ASSERT_TRUE(SQL_SUCCEEDED(ret)); - } + ret = SQLBindCol(stmt, /*col#*/1, SQL_C_TYPE_TIME, &ts, sizeof(ts), + &ind_len); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + } }; /* ES/SQL 'date' is actually 'timestamp' */ -TEST_F(ConvertSQL2C_Time, Timestamp2Time) { - +TEST_F(ConvertSQL2C_Time, Timestamp2Time) +{ #undef SQL_VAL #undef SQL #define SQL_VAL "2345-01-23T12:34:56.000Z" #define SQL "CAST(" SQL_VAL "AS DATETIME)" - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"" SQL "\", \"type\": \"DATETIME\"}\ @@ -48,25 +50,53 @@ TEST_F(ConvertSQL2C_Time, Timestamp2Time) { ]\ }\ "; - prepareAndBind(json_answer); + prepareAndBind(json_answer); - ret = SQLFetch(stmt); - ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); - EXPECT_EQ(ind_len, sizeof(ts)); - EXPECT_EQ(ts.hour, 12); - EXPECT_EQ(ts.minute, 34); - EXPECT_EQ(ts.second, 56); + EXPECT_EQ(ind_len, sizeof(ts)); + EXPECT_EQ(ts.hour, 12); + EXPECT_EQ(ts.minute, 34); + EXPECT_EQ(ts.second, 56); } -TEST_F(ConvertSQL2C_Time, Timestamp2Time_truncate) { +TEST_F(ConvertSQL2C_Time, Timestamp_Str2Time) +{ +#undef SQL_VAL +#undef SQL +#define SQL_VAL "2345-01-23T12:34:56.000Z" +#define SQL "CAST(" SQL_VAL "AS KEYWORD)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"KEYWORD\"}\ + ],\ + \"rows\": [\ + [\"" SQL_VAL "\"]\ + ]\ +}\ +"; + prepareAndBind(json_answer); + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + EXPECT_EQ(ind_len, sizeof(ts)); + EXPECT_EQ(ts.hour, 12); + EXPECT_EQ(ts.minute, 34); + EXPECT_EQ(ts.second, 56); +} +TEST_F(ConvertSQL2C_Time, Timestamp2Time_truncate) +{ #undef SQL_VAL #undef SQL #define SQL_VAL " 2345-01-23T12:34:56.789Z " #define SQL "CAST(" SQL_VAL "AS DATETIME)" - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"" SQL "\", \"type\": \"DATETIME\"}\ @@ -76,27 +106,27 @@ TEST_F(ConvertSQL2C_Time, Timestamp2Time_truncate) { ]\ }\ "; - prepareAndBind(json_answer); + prepareAndBind(json_answer); - ret = SQLFetch(stmt); - ASSERT_TRUE(SQL_SUCCEEDED(ret)); - assertState(L"01S07"); + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + assertState(L"01S07"); - EXPECT_EQ(ind_len, sizeof(ts)); - EXPECT_EQ(ts.hour, 12); - EXPECT_EQ(ts.minute, 34); - EXPECT_EQ(ts.second, 56); + EXPECT_EQ(ind_len, sizeof(ts)); + EXPECT_EQ(ts.hour, 12); + EXPECT_EQ(ts.minute, 34); + EXPECT_EQ(ts.second, 56); } -TEST_F(ConvertSQL2C_Time, Time2Time) { - +TEST_F(ConvertSQL2C_Time, Time2Time) +{ #undef SQL_VAL #undef SQL #define SQL_VAL "12:34:56.0" #define SQL "CAST(" SQL_VAL "AS TEXT)" - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"" SQL_VAL "\", \"type\": \"text\"}\ @@ -106,26 +136,26 @@ TEST_F(ConvertSQL2C_Time, Time2Time) { ]\ }\ "; - prepareAndBind(json_answer); + prepareAndBind(json_answer); - ret = SQLFetch(stmt); - ASSERT_TRUE(SQL_SUCCEEDED(ret)); + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); - EXPECT_EQ(ind_len, sizeof(ts)); - EXPECT_EQ(ts.hour, 12); - EXPECT_EQ(ts.minute, 34); - EXPECT_EQ(ts.second, 56); + EXPECT_EQ(ind_len, sizeof(ts)); + EXPECT_EQ(ts.hour, 12); + EXPECT_EQ(ts.minute, 34); + EXPECT_EQ(ts.second, 56); } -TEST_F(ConvertSQL2C_Time, Time2Time_truncate) { - +TEST_F(ConvertSQL2C_Time, Time2Time_truncate) +{ #undef SQL_VAL #undef SQL #define SQL_VAL "12:34:56.7777777" #define SQL "CAST(" SQL_VAL "AS TEXT)" - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"" SQL "\", \"type\": \"text\"}\ @@ -135,28 +165,28 @@ TEST_F(ConvertSQL2C_Time, Time2Time_truncate) { ]\ }\ "; - prepareAndBind(json_answer); + prepareAndBind(json_answer); - ret = SQLFetch(stmt); - ASSERT_TRUE(SQL_SUCCEEDED(ret)); - assertState(L"01S07"); + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + assertState(L"01S07"); - EXPECT_EQ(ind_len, sizeof(ts)); - EXPECT_EQ(ts.hour, 12); - EXPECT_EQ(ts.minute, 34); - EXPECT_EQ(ts.second, 56); + EXPECT_EQ(ind_len, sizeof(ts)); + EXPECT_EQ(ts.hour, 12); + EXPECT_EQ(ts.minute, 34); + EXPECT_EQ(ts.second, 56); } -TEST_F(ConvertSQL2C_Time, Date2Time_22018) { - +TEST_F(ConvertSQL2C_Time, Date2Time_22018) +{ #undef SQL_VAL #undef SQL #define SQL_VAL "2345-01-23" #define SQL "CAST(" SQL_VAL "AS TEXT)" - const SQLWCHAR *sql = MK_WPTR(SQL_VAL); - const char json_answer[] = "\ + const SQLWCHAR *sql = MK_WPTR(SQL_VAL); + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"" SQL_VAL "\", \"type\": \"text\"}\ @@ -166,22 +196,22 @@ TEST_F(ConvertSQL2C_Time, Date2Time_22018) { ]\ }\ "; - prepareAndBind(json_answer); + prepareAndBind(json_answer); - ret = SQLFetch(stmt); - ASSERT_FALSE(SQL_SUCCEEDED(ret)); - assertState(L"22018"); + ret = SQLFetch(stmt); + ASSERT_FALSE(SQL_SUCCEEDED(ret)); + assertState(L"22018"); } -TEST_F(ConvertSQL2C_Time, Integer2Date_violation_07006) { - +TEST_F(ConvertSQL2C_Time, Integer2Date_violation_07006) +{ #undef SQL_VAL #undef SQL #define SQL_VAL "1" #define SQL "select " SQL_VAL - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"select " SQL "\", \"type\": \"integer\"}\ @@ -191,13 +221,14 @@ TEST_F(ConvertSQL2C_Time, Integer2Date_violation_07006) { ]\ }\ "; - prepareAndBind(json_answer); + prepareAndBind(json_answer); - ret = SQLFetch(stmt); - ASSERT_FALSE(SQL_SUCCEEDED(ret)); - assertState(L"07006"); + ret = SQLFetch(stmt); + ASSERT_FALSE(SQL_SUCCEEDED(ret)); + assertState(L"07006"); } } // test namespace +/* vim: set noet fenc=utf-8 ff=dos sts=0 sw=4 ts=4 tw=78 : */ diff --git a/test/test_conversion_sql2c_timestamp.cc b/test/test_conversion_sql2c_timestamp.cc index d60c83c4..aba791c9 100644 --- a/test/test_conversion_sql2c_timestamp.cc +++ b/test/test_conversion_sql2c_timestamp.cc @@ -15,30 +15,32 @@ namespace test { -class ConvertSQL2C_Timestamp : public ::testing::Test, public ConnectedDBC { +class ConvertSQL2C_Timestamp : public ::testing::Test, public ConnectedDBC +{ - protected: - TIMESTAMP_STRUCT ts; + protected: + TIMESTAMP_STRUCT ts; - void prepareAndBind(const char *jsonAnswer) { - prepareStatement(jsonAnswer); + void prepareAndBind(const char *jsonAnswer) + { + prepareStatement(jsonAnswer); - ret = SQLBindCol(stmt, /*col#*/1, SQL_C_TYPE_TIMESTAMP, &ts, sizeof(ts), - &ind_len); - ASSERT_TRUE(SQL_SUCCEEDED(ret)); - } + ret = SQLBindCol(stmt, /*col#*/1, SQL_C_TYPE_TIMESTAMP, &ts, + sizeof(ts), &ind_len); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + } }; -/* ES/SQL 'date' is actually 'timestamp' */ -TEST_F(ConvertSQL2C_Timestamp, Timestamp2Timestamp_noTruncate) { - +/* ES/SQL 'DATETIME' is actually 'TIMESTAMP' */ +TEST_F(ConvertSQL2C_Timestamp, Datetime2Timestamp) +{ #undef SQL_VAL #undef SQL #define SQL_VAL "2345-01-23T12:34:56.789Z" #define SQL "CAST(" SQL_VAL " AS DATETIME)" - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"" SQL "\", \"type\": \"DATETIME\"}\ @@ -48,30 +50,31 @@ TEST_F(ConvertSQL2C_Timestamp, Timestamp2Timestamp_noTruncate) { ]\ }\ "; - prepareAndBind(json_answer); - - ret = SQLFetch(stmt); - ASSERT_TRUE(SQL_SUCCEEDED(ret)); - - EXPECT_EQ(ind_len, sizeof(ts)); - EXPECT_EQ(ts.year, 2345); - EXPECT_EQ(ts.month, 1); - EXPECT_EQ(ts.day, 23); - EXPECT_EQ(ts.hour, 12); - EXPECT_EQ(ts.minute, 34); - EXPECT_EQ(ts.second, 56); - EXPECT_EQ(ts.fraction, 789); + prepareAndBind(json_answer); + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + EXPECT_EQ(ind_len, sizeof(ts)); + EXPECT_EQ(ts.year, 2345); + EXPECT_EQ(ts.month, 1); + EXPECT_EQ(ts.day, 23); + EXPECT_EQ(ts.hour, 12); + EXPECT_EQ(ts.minute, 34); + EXPECT_EQ(ts.second, 56); + EXPECT_EQ(ts.fraction, 789000000); } -/* No second fraction truncation done by the driver -> no test for 01S07 */ -TEST_F(ConvertSQL2C_Timestamp, Timestamp2Timestamp_trimming) { +TEST_F(ConvertSQL2C_Timestamp, Datetime2Char) +{ #undef SQL_VAL #undef SQL -#define SQL_VAL " 2345-01-23T12:34:56.789Z " +#define SQL_VAL "2345-01-23T12:34:56.789Z" +#define SQL_VAL_TS "2345-01-23 12:34:56.789" #define SQL "CAST(" SQL_VAL " AS DATETIME)" - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"" SQL "\", \"type\": \"DATETIME\"}\ @@ -81,63 +84,98 @@ TEST_F(ConvertSQL2C_Timestamp, Timestamp2Timestamp_trimming) { ]\ }\ "; - prepareAndBind(json_answer); - - ret = SQLFetch(stmt); - ASSERT_TRUE(SQL_SUCCEEDED(ret)); - - EXPECT_EQ(ind_len, sizeof(ts)); - EXPECT_EQ(ts.year, 2345); - EXPECT_EQ(ts.month, 1); - EXPECT_EQ(ts.day, 23); - EXPECT_EQ(ts.hour, 12); - EXPECT_EQ(ts.minute, 34); - EXPECT_EQ(ts.second, 56); - EXPECT_EQ(ts.fraction, 789); + prepareStatement(json_answer); + + SQLCHAR val[sizeof(SQL_VAL)]; + ret = SQLBindCol(stmt, /*col#*/1, SQL_C_CHAR, val, sizeof(val), &ind_len); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + EXPECT_EQ(ind_len, ISO8601_TS_UTC_LEN(ESODBC_DEF_SEC_PRECISION) - /*Z*/1); + EXPECT_STREQ(SQL_VAL_TS, (char *)val); +#undef SQL_VAL_TS } -TEST_F(ConvertSQL2C_Timestamp, Date2Timestamp) { +TEST_F(ConvertSQL2C_Timestamp, String2Timestamp) +{ +#undef SQL_VAL +#undef SQL +#define SQL_VAL "2345-01-23T12:34:56.7890123Z" +#define SQL "CAST(" SQL_VAL " AS KEYWORD)" + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"KEYWORD\"}\ + ],\ + \"rows\": [\ + [\"" SQL_VAL "\"]\ + ]\ +}\ +"; + prepareAndBind(json_answer); + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + EXPECT_EQ(ind_len, sizeof(ts)); + EXPECT_EQ(ts.year, 2345); + EXPECT_EQ(ts.month, 1); + EXPECT_EQ(ts.day, 23); + EXPECT_EQ(ts.hour, 12); + EXPECT_EQ(ts.minute, 34); + EXPECT_EQ(ts.second, 56); + EXPECT_EQ(ts.fraction, 789012300); +} + +/* ES/SQL 'DATETIME' is actually 'TIMESTAMP' */ +TEST_F(ConvertSQL2C_Timestamp, Datetime_with_offset2Timestamp) +{ #undef SQL_VAL #undef SQL -#define SQL_VAL "2345-01-23T" -#define SQL "CAST(" SQL_VAL " AS TEXT)" +#define SQL_VAL "2345-01-23T12:34:56.789+01:30" +#define SQL "CAST(" SQL_VAL " AS DATETIME)" - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ - {\"name\": \"" SQL "\", \"type\": \"text\"}\ + {\"name\": \"" SQL "\", \"type\": \"DATETIME\"}\ ],\ \"rows\": [\ [\"" SQL_VAL "\"]\ ]\ }\ "; - prepareAndBind(json_answer); - - ret = SQLFetch(stmt); - ASSERT_TRUE(SQL_SUCCEEDED(ret)); - - EXPECT_EQ(ind_len, sizeof(ts)); - EXPECT_EQ(ts.year, 2345); - EXPECT_EQ(ts.month, 1); - EXPECT_EQ(ts.day, 23); - EXPECT_EQ(ts.hour, 0); - EXPECT_EQ(ts.minute, 0); - EXPECT_EQ(ts.second, 0); - EXPECT_EQ(ts.fraction, 0); + prepareAndBind(json_answer); + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + EXPECT_EQ(ind_len, sizeof(ts)); + EXPECT_EQ(ts.year, 2345); + EXPECT_EQ(ts.month, 1); + EXPECT_EQ(ts.day, 23); + EXPECT_EQ(ts.hour, 11); + EXPECT_EQ(ts.minute, 4); + EXPECT_EQ(ts.second, 56); + EXPECT_EQ(ts.fraction, 789000000); } +/* No second fraction truncation done by the driver -> no test for 01S07 */ +//TEST_F(ConvertSQL2C_Timestamp, Timestamp2Timestamp_trimming) {} -TEST_F(ConvertSQL2C_Timestamp, Time2Timestamp) { +TEST_F(ConvertSQL2C_Timestamp, Date_Str2Timestamp) +{ #undef SQL_VAL #undef SQL -#define SQL_VAL "10:10:10.1010" +#define SQL_VAL "2345-01-23T" #define SQL "CAST(" SQL_VAL " AS TEXT)" - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"" SQL "\", \"type\": \"text\"}\ @@ -147,30 +185,30 @@ TEST_F(ConvertSQL2C_Timestamp, Time2Timestamp) { ]\ }\ "; - prepareAndBind(json_answer); - - ret = SQLFetch(stmt); - ASSERT_TRUE(SQL_SUCCEEDED(ret)); - - EXPECT_EQ(ind_len, sizeof(ts)); - EXPECT_EQ(ts.year, 0); - EXPECT_EQ(ts.month, 0); - EXPECT_EQ(ts.day, 0); - EXPECT_EQ(ts.hour, 10); - EXPECT_EQ(ts.minute, 10); - EXPECT_EQ(ts.second, 10); - EXPECT_EQ(ts.fraction, 101); + prepareAndBind(json_answer); + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + EXPECT_EQ(ind_len, sizeof(ts)); + EXPECT_EQ(ts.year, 2345); + EXPECT_EQ(ts.month, 1); + EXPECT_EQ(ts.day, 23); + EXPECT_EQ(ts.hour, 0); + EXPECT_EQ(ts.minute, 0); + EXPECT_EQ(ts.second, 0); + EXPECT_EQ(ts.fraction, 0); } -TEST_F(ConvertSQL2C_Timestamp, Time2Timestamp_trimming) { - +TEST_F(ConvertSQL2C_Timestamp, Time_Str2Timestamp) +{ #undef SQL_VAL #undef SQL -#define SQL_VAL " 10:10:10.1010 " +#define SQL_VAL "10:10:10.1010" #define SQL "CAST(" SQL_VAL " AS TEXT)" - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"" SQL "\", \"type\": \"text\"}\ @@ -180,30 +218,30 @@ TEST_F(ConvertSQL2C_Timestamp, Time2Timestamp_trimming) { ]\ }\ "; - prepareAndBind(json_answer); - - ret = SQLFetch(stmt); - ASSERT_TRUE(SQL_SUCCEEDED(ret)); - - EXPECT_EQ(ind_len, sizeof(ts)); - EXPECT_EQ(ts.year, 0); - EXPECT_EQ(ts.month, 0); - EXPECT_EQ(ts.day, 0); - EXPECT_EQ(ts.hour, 10); - EXPECT_EQ(ts.minute, 10); - EXPECT_EQ(ts.second, 10); - EXPECT_EQ(ts.fraction, 101); + prepareAndBind(json_answer); + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + EXPECT_EQ(ind_len, sizeof(ts)); + EXPECT_EQ(ts.year, 0); + EXPECT_EQ(ts.month, 0); + EXPECT_EQ(ts.day, 0); + EXPECT_EQ(ts.hour, 10); + EXPECT_EQ(ts.minute, 10); + EXPECT_EQ(ts.second, 10); + EXPECT_EQ(ts.fraction, 101000000); } -TEST_F(ConvertSQL2C_Timestamp, String2Timestamp_invalidFormat_22018) { - +TEST_F(ConvertSQL2C_Timestamp, Datetime2Timestamp_invalidFormat_22018) +{ #undef SQL_VAL #undef SQL #define SQL_VAL "invalid 2345-01-23T12:34:56.789Z" #define SQL "CAST(" SQL_VAL " AS DATETIME)" - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"" SQL "\", \"type\": \"DATETIME\"}\ @@ -213,21 +251,21 @@ TEST_F(ConvertSQL2C_Timestamp, String2Timestamp_invalidFormat_22018) { ]\ }\ "; - prepareAndBind(json_answer); + prepareAndBind(json_answer); - ret = SQLFetch(stmt); - ASSERT_FALSE(SQL_SUCCEEDED(ret)); - assertState(L"22018"); + ret = SQLFetch(stmt); + ASSERT_FALSE(SQL_SUCCEEDED(ret)); + assertState(L"22018"); } -TEST_F(ConvertSQL2C_Timestamp, Integer2Timestamp_violation_07006) { - +TEST_F(ConvertSQL2C_Timestamp, Integer2Timestamp_violation_07006) +{ #undef SQL_VAL #undef SQL #define SQL_VAL "1" #define SQL "select " SQL_VAL - const char json_answer[] = "\ + const char json_answer[] = "\ {\ \"columns\": [\ {\"name\": \"" SQL "\", \"type\": \"integer\"}\ @@ -237,13 +275,137 @@ TEST_F(ConvertSQL2C_Timestamp, Integer2Timestamp_violation_07006) { ]\ }\ "; - prepareAndBind(json_answer); + prepareAndBind(json_answer); + + ret = SQLFetch(stmt); + ASSERT_FALSE(SQL_SUCCEEDED(ret)); + assertState(L"07006"); +} + + + +class ConvertSQL2C_Timestamp_TZ : public ConvertSQL2C_Timestamp +{ + protected: + void SetUp() override + { + ((esodbc_dbc_st *)dbc)->apply_tz = TRUE; + ASSERT_EQ(putenv("TZ=NPT-5:45NTP"), 0); + tzset(); + } + + void TearDown() override + { + ASSERT_EQ(putenv("TZ="), 0); + } +}; + + +TEST_F(ConvertSQL2C_Timestamp_TZ, Datetime2Char_local) +{ +#undef SQL_VAL +#undef SQL +#define SQL_VAL "2000-01-23T12:00:00.789Z" +#define SQL_VAL_TS "2000-01-23 17:45:00.789" +#define SQL "CAST(" SQL_VAL " AS DATETIME)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"DATETIME\"}\ + ],\ + \"rows\": [\ + [\"" SQL_VAL "\"]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLCHAR val[sizeof(SQL_VAL)]; + ret = SQLBindCol(stmt, /*col#*/1, SQL_C_CHAR, val, sizeof(val), &ind_len); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + EXPECT_EQ(ind_len, ISO8601_TS_UTC_LEN(ESODBC_DEF_SEC_PRECISION) - /*Z*/1); + EXPECT_STREQ(SQL_VAL_TS, (char *)val); + +#undef SQL_VAL_TS +} + + +TEST_F(ConvertSQL2C_Timestamp_TZ, Datetime_offset2Char_local) +{ +#undef SQL_VAL +#undef SQL +#define SQL_VAL "2000-01-23T13:00:00.789+01:00" +#define SQL_VAL_TS "2000-01-23 17:45:00.789" +#define SQL "CAST(" SQL_VAL " AS DATETIME)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"DATETIME\"}\ + ],\ + \"rows\": [\ + [\"" SQL_VAL "\"]\ + ]\ +}\ +"; + prepareStatement(json_answer); + + SQLCHAR val[sizeof(SQL_VAL)]; + ret = SQLBindCol(stmt, /*col#*/1, SQL_C_CHAR, val, sizeof(val), &ind_len); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); - ret = SQLFetch(stmt); - ASSERT_FALSE(SQL_SUCCEEDED(ret)); - assertState(L"07006"); + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + EXPECT_EQ(ind_len, ISO8601_TS_UTC_LEN(ESODBC_DEF_SEC_PRECISION) - /*Z*/1); + EXPECT_STREQ(SQL_VAL_TS, (char *)val); + +#undef SQL_VAL_TS } +TEST_F(ConvertSQL2C_Timestamp_TZ, Datetime_offset_Str2Timestamp) +{ +#undef SQL_VAL +#undef SQL +#define SQL_VAL "2000-01-23T13:00:01.789+01:00" +#define SQL_VAL_TS "2000-01-23 17:45:01.789" +#define SQL "CAST(" SQL_VAL " AS KEYWORD)" + + const char json_answer[] = "\ +{\ + \"columns\": [\ + {\"name\": \"" SQL "\", \"type\": \"KEYWORD\"}\ + ],\ + \"rows\": [\ + [\"" SQL_VAL "\"]\ + ]\ +}\ +"; + prepareAndBind(json_answer); + + ret = SQLFetch(stmt); + ASSERT_TRUE(SQL_SUCCEEDED(ret)); + + EXPECT_EQ(ind_len, sizeof(ts)); + EXPECT_EQ(ts.year, 2000); + EXPECT_EQ(ts.month, 1); + EXPECT_EQ(ts.day, 23); + EXPECT_EQ(ts.hour, 17); + EXPECT_EQ(ts.minute, 45); + EXPECT_EQ(ts.second, 1); + EXPECT_EQ(ts.fraction, 789000000); + +#undef SQL_VAL_TS +} + + + } // test namespace +/* vim: set noet fenc=utf-8 ff=dos sts=0 sw=4 ts=4 tw=78 : */ From efe351240db509a0037018a596d56a61591467b4 Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Fri, 1 Mar 2019 21:59:25 +0100 Subject: [PATCH 4/4] fix define conditionals for mktime error message - _WIN64 -> !_USE_32BIT_TIME_T --- driver/convert.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/driver/convert.c b/driver/convert.c index d6dacada..b1d15e95 100644 --- a/driver/convert.c +++ b/driver/convert.c @@ -29,11 +29,11 @@ } while (0) #ifdef _WIN32 -# ifdef _WIN64 +# ifndef _USE_32BIT_TIME_T # define MKTIME_YEAR_RANGE "1970-3000" -# else /* _WIN64 */ +# else /* !_USE_32BIT_TIME_T */ # define MKTIME_YEAR_RANGE "1970-2038" -# endif /* _WIN64 */ +# endif /* !_USE_32BIT_TIME_T */ #else /* _WIN32 */ # error "platform not supported" #endif /* _WIN32 */ @@ -1617,7 +1617,7 @@ static SQLRETURN parse_date_time_ts(esodbc_stmt_st *stmt, xstr_st *xstr, td.cnt ++; } DBGH(stmt, "SQL format translated to ISO: [%zu] `" LCPDL "`.", - td.cnt, LCSTR(&td)); + td.cnt, LCSTR(&td)); } /* else: already in ISO8601 format */ xtd.c = td; @@ -3986,14 +3986,14 @@ SQLRETURN c2sql_timestamp(esodbc_rec_st *arec, esodbc_rec_st *irec, if (colsize && (colsize < sizeof("yyyy-mm-dd hh:mm") - 1 || colsize == 17 || colsize == 18)) { ERRH(stmt, "invalid column size value: %llu; allowed: 16, 19, 20+f.", - colsize); + colsize); RET_HDIAGS(stmt, SQL_STATE_HY104); } decdigits = get_param_decdigits(irec); DBGH(stmt, "requested decimal digits: %llu.", 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); decdigits = ESODBC_MAX_SEC_PRECISION; }