From 7eb3c8cc587f6ddc74ad10a9016f576ddf39b10f Mon Sep 17 00:00:00 2001 From: Matt McNabb Date: Mon, 23 Mar 2026 11:26:47 +1300 Subject: [PATCH 1/8] Read ODBC date-to-char conversion formats from the context Instead of duplicate instances of hardcoding. Signed-off-by: Matt McNabb --- src/odbc/convert_tds2sql.c | 7 ++++--- src/odbc/odbc.c | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/odbc/convert_tds2sql.c b/src/odbc/convert_tds2sql.c index 79be55888..5f4a54b0e 100644 --- a/src/odbc/convert_tds2sql.c +++ b/src/odbc/convert_tds2sql.c @@ -381,7 +381,8 @@ odbc_tds2sql(TDS_STMT * stmt, TDSCOLUMN *curcol, int srctype, TDS_CHAR * src, TD case SYBDATETIME4: prec = 0; datetime: - fmt = "%Y-%m-%d %H:%M:%S.%z"; + /* note: this string initialized in odbc_SQLAllocEnv() */ + fmt = context->locale->datetime_fmt; break; case SYBMSTIME: prec = dta->time_prec; @@ -392,12 +393,12 @@ odbc_tds2sql(TDS_STMT * stmt, TDSCOLUMN *curcol, int srctype, TDS_CHAR * src, TD case SYBTIME: prec = 3; time: - fmt = "%H:%M:%S.%z"; + fmt = context->locale->time_fmt; break; case SYBMSDATE: case SYBDATE: prec = 0; - fmt = "%Y-%m-%d"; + fmt = context->locale->date_fmt; break; } if (!fmt) goto normal_conversion; diff --git a/src/odbc/odbc.c b/src/odbc/odbc.c index 5d761d8cc..b5de4effb 100644 --- a/src/odbc/odbc.c +++ b/src/odbc/odbc.c @@ -1810,7 +1810,7 @@ odbc_SQLAllocEnv(SQLHENV FAR * phenv, SQLINTEGER odbc_version) ctx->msg_handler = odbc_errmsg_handler; ctx->err_handler = odbc_errmsg_handler; - /* ODBC has its own format */ + /* ODBC driver date-to-char conversion: mimic MS client */ free(ctx->locale->datetime_fmt); ctx->locale->datetime_fmt = strdup("%Y-%m-%d %H:%M:%S.%z"); free(ctx->locale->date_fmt); From 16e171f27e5bfd84988a181e67d986771ab19868 Mon Sep 17 00:00:00 2001 From: Matt McNabb Date: Mon, 23 Mar 2026 12:10:52 +1300 Subject: [PATCH 2/8] ODBC date/time format - configuration attribute Signed-off-by: Matt McNabb --- include/freetds/odbc.h | 8 ++++++++ include/odbcss.h | 6 +++++- src/odbc/convert_tds2sql.c | 7 +++---- src/odbc/odbc.c | 26 ++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/include/freetds/odbc.h b/include/freetds/odbc.h index 83783dcd5..36837581e 100644 --- a/include/freetds/odbc.h +++ b/include/freetds/odbc.h @@ -311,6 +311,14 @@ struct _hdbc TDS_INT default_query_timeout; TDSBCPINFO *bcpinfo; + + /* ODBC extra options. Format strings for tds_strftime() + * to be applied when a client application fetches a date/time field + * as a character type. (Not used for the reverse direction) + */ + DSTR datetime_fmt; + DSTR date_fmt; + DSTR time_fmt; }; struct _hsattr diff --git a/include/odbcss.h b/include/odbcss.h index 85d6347dc..b966af80e 100644 --- a/include/odbcss.h +++ b/include/odbcss.h @@ -153,6 +153,11 @@ typedef struct tagSS_TIMESTAMPOFFSET_STRUCT { SQLSMALLINT timezone_minute; } SQL_SS_TIMESTAMPOFFSET_STRUCT; +#define SQL_COPT_TDSODBC_IMPL_BASE 1500 +/* note: values +0 to +8 allocated to deprecated TDSODBC_BCP options below */ +#define SQL_COPT_TDSODBC_DATETIME_FORMAT (SQL_COPT_TDSODBC_IMPL_BASE+9) +#define SQL_COPT_TDSODBC_DATE_FORMAT (SQL_COPT_TDSODBC_IMPL_BASE+10) +#define SQL_COPT_TDSODBC_TIME_FORMAT (SQL_COPT_TDSODBC_IMPL_BASE+11) #ifdef TDSODBC_BCP @@ -179,7 +184,6 @@ typedef struct tagSS_TIMESTAMPOFFSET_STRUCT { #define SQL_BCP_OFF 0 #define SQL_BCP_ON 1 -#define SQL_COPT_TDSODBC_IMPL_BASE 1500 #define SQL_COPT_TDSODBC_IMPL_BCP_INITA (SQL_COPT_TDSODBC_IMPL_BASE) /* deprecated SQL_COPT_TDSODBC_IMPL_BCP_CONTROL */ #define SQL_COPT_TDSODBC_IMPL_BCP_COLPTR (SQL_COPT_TDSODBC_IMPL_BASE+2) diff --git a/src/odbc/convert_tds2sql.c b/src/odbc/convert_tds2sql.c index 5f4a54b0e..ba1695f06 100644 --- a/src/odbc/convert_tds2sql.c +++ b/src/odbc/convert_tds2sql.c @@ -381,8 +381,7 @@ odbc_tds2sql(TDS_STMT * stmt, TDSCOLUMN *curcol, int srctype, TDS_CHAR * src, TD case SYBDATETIME4: prec = 0; datetime: - /* note: this string initialized in odbc_SQLAllocEnv() */ - fmt = context->locale->datetime_fmt; + fmt = tds_dstr_cstr(&stmt->dbc->datetime_fmt); break; case SYBMSTIME: prec = dta->time_prec; @@ -393,12 +392,12 @@ odbc_tds2sql(TDS_STMT * stmt, TDSCOLUMN *curcol, int srctype, TDS_CHAR * src, TD case SYBTIME: prec = 3; time: - fmt = context->locale->time_fmt; + fmt = tds_dstr_cstr(&stmt->dbc->time_fmt); break; case SYBMSDATE: case SYBDATE: prec = 0; - fmt = context->locale->date_fmt; + fmt = tds_dstr_cstr(&stmt->dbc->date_fmt); break; } if (!fmt) goto normal_conversion; diff --git a/src/odbc/odbc.c b/src/odbc/odbc.c index b5de4effb..947297ca6 100644 --- a/src/odbc/odbc.c +++ b/src/odbc/odbc.c @@ -1766,6 +1766,15 @@ odbc_SQLAllocConnect(SQLHENV henv, SQLHDBC FAR * phdbc) dbc->attr.mars_enabled = SQL_MARS_ENABLED_NO; dbc->attr.bulk_enabled = SQL_BCP_OFF; + /* Initial values of DBC date formats taken from environment context. + * odbc_SQLAllocEnv() always initializes that. */ + tds_dstr_init(&dbc->datetime_fmt); + tds_dstr_copy(&dbc->datetime_fmt, env->tds_ctx->locale->datetime_fmt); + tds_dstr_init(&dbc->date_fmt); + tds_dstr_copy(&dbc->date_fmt, env->tds_ctx->locale->date_fmt); + tds_dstr_init(&dbc->time_fmt); + tds_dstr_copy(&dbc->time_fmt, env->tds_ctx->locale->time_fmt); + tds_mutex_init(&dbc->mtx); *phdbc = (SQLHDBC) dbc; @@ -4349,6 +4358,11 @@ odbc_SQLFreeConnect(SQLHDBC hdbc) tds_free_socket(dbc->tds_socket); odbc_bcp_free_storage(dbc); + + tds_dstr_free(&dbc->datetime_fmt); + tds_dstr_free(&dbc->date_fmt); + tds_dstr_free(&dbc->time_fmt); + /* free attributes */ #ifdef TDS_NO_DM tds_dstr_free(&dbc->attr.tracefile); @@ -6607,6 +6621,18 @@ ODBC_FUNC(SQLSetConnectAttr, (P(SQLHDBC,hdbc), P(SQLINTEGER,Attribute), P(SQLPOI (const ODBC_CHAR *) params->errfile, params->direction _wide0); } break; + case SQL_COPT_TDSODBC_DATETIME_FORMAT: + if (!odbc_dstr_copy(dbc, &dbc->datetime_fmt, StringLength, (ODBC_CHAR*)ValuePtr)) + odbc_errs_add(&dbc->errs, "HY001", NULL); + break; + case SQL_COPT_TDSODBC_DATE_FORMAT: + if (!odbc_dstr_copy(dbc, &dbc->date_fmt, StringLength, (ODBC_CHAR*)ValuePtr)) + odbc_errs_add(&dbc->errs, "HY001", NULL); + break; + case SQL_COPT_TDSODBC_TIME_FORMAT: + if (!odbc_dstr_copy(dbc, &dbc->time_fmt, StringLength, (ODBC_CHAR*)ValuePtr)) + odbc_errs_add(&dbc->errs, "HY001", NULL); + break; #ifdef ENABLE_ODBC_WIDE case SQL_COPT_TDSODBC_IMPL_BCP_INITW: if (!ValuePtr) From 45fedb0a40067445cff2d706098ffbe749f5e55d Mon Sep 17 00:00:00 2001 From: Matt McNabb Date: Mon, 23 Mar 2026 12:10:07 +1300 Subject: [PATCH 3/8] Expand o_date test - Test MSDATETIME2 full precision display. - Test customized format strings. - Check display size is not exceeded. - Exit cleanly on test failure. Signed-off-by: Matt McNabb --- src/odbc/unittests/date.c | 66 ++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/src/odbc/unittests/date.c b/src/odbc/unittests/date.c index acd1e81d1..db3f44213 100644 --- a/src/odbc/unittests/date.c +++ b/src/odbc/unittests/date.c @@ -1,6 +1,7 @@ #include "common.h" +#include -static void +static int DoTest(int n) { SQLCHAR output[256]; @@ -9,14 +10,51 @@ DoTest(int n) SQLULEN colSize; SQLSMALLINT colScale, colNullable; SQLLEN dataSize; + char const* expect; TIMESTAMP_STRUCT ts; - odbc_command("select convert(datetime, '2002-12-27 18:43:21')"); + switch (n) + { + case 0: + case 1: + odbc_command("select convert(datetime, '2002-12-27 18:43:21')"); + expect = "2002-12-27 18:43:21.000"; + break; + + case 2: + /* Recent feature added to trunk - Full precision for datetime2 */ + odbc_command("select convert(datetime2, '2002-12-27 18:43:21')"); + expect = "2002-12-27 18:43:21.0000000"; + break; + + case 3: + SQLSetConnectAttr(odbc_conn, SQL_COPT_TDSODBC_DATETIME_FORMAT, T("%d-%m-%Y %H:%M:%S"), SQL_NTS); + odbc_command("select convert(datetime, '2002-12-27 18:43:21')"); + expect = "27-12-2002 18:43:21"; + break; + + case 4: + SQLSetConnectAttr(odbc_conn, SQL_COPT_TDSODBC_DATE_FORMAT, T("**%d-%m-%Y**"), SQL_NTS); + odbc_command("select convert(date, '2002-12-27 18:43:21')"); + expect = "**27-12-2002**"; + break; + + case 5: + SQLSetConnectAttr(odbc_conn, SQL_COPT_TDSODBC_TIME_FORMAT, T("**%H:%M:%S**"), SQL_NTS); + odbc_command("select convert(time, '2002-12-27 18:43:21')"); + expect = "**18:43:21**"; + break; + + default: + printf("Done.\n"); + return 0; + } CHKFetch("SI"); CHKDescribeCol(1, (SQLTCHAR*)output, sizeof(output)/sizeof(SQLWCHAR), NULL, &colType, &colSize, &colScale, &colNullable, "S"); + /* Case 0 tests binding SQL_C_TIMESTAMP to result; other cases are testing binding to char. */ if (n == 0) { memset(&ts, 0, sizeof(ts)); CHKGetData(1, SQL_C_TIMESTAMP, &ts, sizeof(ts), &dataSize, "S"); @@ -25,25 +63,29 @@ DoTest(int n) CHKGetData(1, SQL_C_CHAR, output, sizeof(output), &dataSize, "S"); } - printf("Date returned: %s\n", output); - if (strcmp((char *) output, "2002-12-27 18:43:21.000") != 0) { - fprintf(stderr, "Invalid returned date\n"); - exit(1); + printf("Date returned: \"%s\"\n", output); + if (strcmp((char *) output, expect) != 0) { + fprintf(stderr, "Invalid returned date; expected \"%s\".\n", expect); + return 1; } + if ((int)colSize < dataSize) + { + fprintf(stderr, "Column described as size %d, but data length was %d\n", (int)colSize, (int)dataSize); + return 1; + } CHKFetch("No"); CHKCloseCursor("SI"); + return -1; } TEST_MAIN() { + int i = 0; + int exit_status; odbc_connect(); - - DoTest(0); - DoTest(1); - + while ((exit_status = DoTest(i++)) == -1) {} odbc_disconnect(); - printf("Done.\n"); - return 0; + return exit_status; } From 2d5c9af794fc177a0347746e2ebc4161a8b1b203 Mon Sep 17 00:00:00 2001 From: Matt McNabb Date: Tue, 31 Mar 2026 16:46:35 +1300 Subject: [PATCH 4/8] ODBC date/time format - connection string option Signed-off-by: Matt McNabb --- include/freetds/odbc.h | 3 +++ src/odbc/connectparams.c | 3 +++ src/odbc/odbc.c | 17 +++++++++++++++++ src/odbc/unittests/connection_string_parse.c | 20 +++++++++++++++++++- 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/include/freetds/odbc.h b/include/freetds/odbc.h index 36837581e..48512ff1a 100644 --- a/include/freetds/odbc.h +++ b/include/freetds/odbc.h @@ -515,6 +515,8 @@ bool get_login_info(HWND hwndParent, TDSLOGIN * login); ODBC_PARAM(ClientCharset) \ ODBC_PARAM(ConnectionTimeout) \ ODBC_PARAM(Database) \ + ODBC_PARAM(DateFmt) \ + ODBC_PARAM(DateTimeFmt) \ ODBC_PARAM(DebugFlags) \ ODBC_PARAM(DSN) \ ODBC_PARAM(DumpFile) \ @@ -534,6 +536,7 @@ bool get_login_info(HWND hwndParent, TDSLOGIN * login); ODBC_PARAM(ServerSPN) \ ODBC_PARAM(TDS_Version) \ ODBC_PARAM(TextSize) \ + ODBC_PARAM(TimeFmt) \ ODBC_PARAM(Timeout) \ ODBC_PARAM(Trusted_Connection) \ ODBC_PARAM(UID) \ diff --git a/src/odbc/connectparams.c b/src/odbc/connectparams.c index bdeca7ba8..d8cb8066d 100644 --- a/src/odbc/connectparams.c +++ b/src/odbc/connectparams.c @@ -492,6 +492,9 @@ odbc_parse_connect_string(TDS_ERRS *errs, const char *connect_string, const char || strcasecmp(option, "client_charset") == 0) { num_param = ODBC_PARAM_ClientCharset; tds_parse_conf_section(TDS_STR_CLCHARSET, tds_dstr_cstr(&value), login); + } else if (CHK_PARAM(DateTimeFmt)) { + } else if (CHK_PARAM(DateFmt)) { + } else if (CHK_PARAM(TimeFmt)) { } else if (CHK_PARAM(DumpFile)) { tds_parse_conf_section(TDS_STR_DUMPFILE, tds_dstr_cstr(&value), login); } else if (CHK_PARAM(DumpFileAppend)) { diff --git a/src/odbc/odbc.c b/src/odbc/odbc.c index 947297ca6..a2dd5a4c0 100644 --- a/src/odbc/odbc.c +++ b/src/odbc/odbc.c @@ -572,6 +572,19 @@ odbc_prepare(TDS_STMT *stmt) ODBC_RETURN_(stmt); } +/** Helper for SQLDriverConnect to apply DBC parameters */ +static void dbc_apply_param(TDS_DBC* dbc, DSTR* dst, const TDS_PARSED_PARAM* param) +{ + if (param->len > 0) + { + /* TDS_PARSED_PARAM still contains the optional {} delimiters */ + int offset = (param->p[0] == '{' && param->p[param->len - 1] == '}'); + + if (!tds_dstr_copyn(dst, param->p + offset, param->len - 2 * offset)) + odbc_errs_add(&dbc->errs, "HY001", NULL); + } +} + ODBC_FUNC(SQLDriverConnect, (P(SQLHDBC,hdbc), P(SQLHWND,hwnd), PCHARIN(ConnStrIn,SQLSMALLINT), PCHAROUT(ConnStrOut,SQLSMALLINT), P(SQLUSMALLINT,fDriverCompletion) WIDE)) { @@ -633,6 +646,10 @@ ODBC_FUNC(SQLDriverConnect, (P(SQLHDBC,hdbc), P(SQLHWND,hwnd), PCHARIN(ConnStrIn odbc_set_dstr(dbc, szConnStrOut, cbConnStrOutMax, pcbConnStrOut, &conn_str); tds_dstr_free(&conn_str); + dbc_apply_param(dbc, &dbc->datetime_fmt, ¶ms[ODBC_PARAM_DateTimeFmt]); + dbc_apply_param(dbc, &dbc->date_fmt, ¶ms[ODBC_PARAM_DateFmt]); + dbc_apply_param(dbc, &dbc->time_fmt, ¶ms[ODBC_PARAM_TimeFmt]); + /* add login info */ if (hwnd && fDriverCompletion != SQL_DRIVER_NOPROMPT && (fDriverCompletion == SQL_DRIVER_PROMPT || (!params[ODBC_PARAM_UID].p && !params[ODBC_PARAM_Trusted_Connection].p) diff --git a/src/odbc/unittests/connection_string_parse.c b/src/odbc/unittests/connection_string_parse.c index ac195e4ca..dd5a01d22 100644 --- a/src/odbc/unittests/connection_string_parse.c +++ b/src/odbc/unittests/connection_string_parse.c @@ -13,7 +13,15 @@ static void assert_equal_str(TDS_PARSED_PARAM param, const char *b) { /* printf("param %.*s b %s\n", (int) param.len, param.p, b); */ - assert(b && strlen(b) == param.len && strncmp(param.p, b, param.len)==0); + if (b && strlen(b) == param.len && strncmp(param.p, b, param.len)==0) + return; + + if (!b) + b = "(null)"; + + fprintf(stderr, "(%d) \"%s\"\n", (int)strlen(b), b); + fprintf(stderr, "(%d) \"%.*s\"\n", (int)param.len, (int)param.len, param.p); + assert(!"Strings differ."); } typedef void check_func_t(TDSLOGIN *login, TDS_PARSED_PARAM *parsed_params); @@ -137,6 +145,14 @@ CHECK(password_bug_report, CHECK_ERROR(unfinished, "Driver=FreeTDS;Server=1.2.3.4;Port=1433;pwd={p@ssw0rd") +CHECK(datetime_formats, + "Driver=FreeTDS;Server=1.2.3.4;Port=1433;Database=test;uid=test_user;pwd=xxxx;TIMEFMT=%H:%M:%S;DATEFMT=%Y-%m-%d;DATETIMEFMT={==%Y%m%d;%H%M%S==}") +{ + assert_equal_str(parsed_params[ODBC_PARAM_DateFmt], "%Y-%m-%d"); + assert_equal_str(parsed_params[ODBC_PARAM_TimeFmt], "%H:%M:%S"); + assert_equal_str(parsed_params[ODBC_PARAM_DateTimeFmt], "{==%Y%m%d;%H%M%S==}"); +} + TEST_MAIN() { simple_string(); @@ -153,5 +169,7 @@ TEST_MAIN() unfinished(); + datetime_formats(); + return 0; } From 32564e71a9ed23c05566477003b37f9d872cceb1 Mon Sep 17 00:00:00 2001 From: Matt McNabb Date: Tue, 31 Mar 2026 17:15:00 +1300 Subject: [PATCH 5/8] ODBC - make more DBC info available to set_type_info() This will be used by the date/time format length computation Signed-off-by: Matt McNabb --- include/freetds/odbc.h | 4 ++-- src/odbc/odbc.c | 4 ++-- src/odbc/odbc_data.c | 24 ++++++++++++------------ src/odbc/unittests/all_types.c | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/include/freetds/odbc.h b/include/freetds/odbc.h index 48512ff1a..3d76f0821 100644 --- a/include/freetds/odbc.h +++ b/include/freetds/odbc.h @@ -460,7 +460,7 @@ typedef struct _hchk TDS_CHK; typedef struct { /* this must be the first member */ TDSCOLUMNFUNCS common; - void (*set_type_info)(TDSCOLUMN *col, struct _drecord *drec, SQLINTEGER odbc_ver); + void (*set_type_info)(TDSCOLUMN *col, struct _drecord *drec, const TDS_DBC *dbc); } TDS_FUNCS; #define IS_HENV(x) (((TDS_CHK *)x)->htype == SQL_HANDLE_ENV) @@ -657,7 +657,7 @@ SQLRETURN odbc_set_stmt_query(struct _hstmt *stmt, const ODBC_CHAR *sql, ptrdiff void odbc_set_return_status(struct _hstmt *stmt, unsigned int n_row); void odbc_set_return_params(struct _hstmt *stmt, unsigned int n_row); -void odbc_set_sql_type_info(TDSCOLUMN * col, struct _drecord *drec, SQLINTEGER odbc_ver); +void odbc_set_sql_type_info(TDSCOLUMN* col, struct _drecord* drec, const TDS_DBC* dbc); int odbc_sql_to_c_type_default(int sql_type); TDS_SERVER_TYPE odbc_sql_to_server_type(TDSCONNECTION * conn, int sql_type, int sql_unsigned); diff --git a/src/odbc/odbc.c b/src/odbc/odbc.c index a2dd5a4c0..ee9542ee5 100644 --- a/src/odbc/odbc.c +++ b/src/odbc/odbc.c @@ -3325,7 +3325,7 @@ odbc_populate_ird(TDS_STMT * stmt) * is formatting function correct ?? * we should not convert to string with invalid precision! */ - odbc_set_sql_type_info(col, drec, stmt->dbc->env->attr.odbc_version); + odbc_set_sql_type_info(col, drec, stmt->dbc); drec->sql_desc_fixed_prec_scale = (col->column_prec && col->column_scale) ? SQL_TRUE : SQL_FALSE; if (!tds_dstr_dup(&drec->sql_desc_label, &col->column_name)) @@ -8143,7 +8143,7 @@ read_params(TDS_STMT *stmt) col->column_prec = precision; col->column_scale = scale; drec->sql_desc_nullable = SQL_NULLABLE; - odbc_set_sql_type_info(col, drec, stmt->dbc->env->attr.odbc_version); + odbc_set_sql_type_info(col, drec, stmt->dbc); break; } continue; diff --git a/src/odbc/odbc_data.c b/src/odbc/odbc_data.c index 34325f7a1..903e26590 100644 --- a/src/odbc/odbc_data.c +++ b/src/odbc/odbc_data.c @@ -49,7 +49,7 @@ } while(0) static void -data_msdatetime_set_type_info(TDSCOLUMN * col, struct _drecord *drec, SQLINTEGER odbc_ver TDS_UNUSED) +data_msdatetime_set_type_info(TDSCOLUMN * col, struct _drecord *drec, const TDS_DBC *dbc) { int decimals = col->column_prec ? col->column_prec + 1: 0; @@ -85,7 +85,7 @@ data_msdatetime_set_type_info(TDSCOLUMN * col, struct _drecord *drec, SQLINTEGER } static void -data_variant_set_type_info(TDSCOLUMN * col TDS_UNUSED, struct _drecord *drec, SQLINTEGER odbc_ver TDS_UNUSED) +data_variant_set_type_info(TDSCOLUMN * col TDS_UNUSED, struct _drecord *drec, const TDS_DBC *dbc TDS_UNUSED) { drec->sql_desc_concise_type = SQL_SS_VARIANT; drec->sql_desc_display_size = 8000; @@ -94,7 +94,7 @@ data_variant_set_type_info(TDSCOLUMN * col TDS_UNUSED, struct _drecord *drec, SQ } static void -data_numeric_set_type_info(TDSCOLUMN * col, struct _drecord *drec, SQLINTEGER odbc_ver TDS_UNUSED) +data_numeric_set_type_info(TDSCOLUMN * col, struct _drecord *drec, const TDS_DBC* dbc TDS_UNUSED) { const char *type_name = col->on_server.column_type == SYBNUMERIC ? "numeric" : "decimal"; @@ -107,7 +107,7 @@ data_numeric_set_type_info(TDSCOLUMN * col, struct _drecord *drec, SQLINTEGER od } static void -data_clrudt_set_type_info(TDSCOLUMN * col, struct _drecord *drec, SQLINTEGER odbc_ver TDS_UNUSED) +data_clrudt_set_type_info(TDSCOLUMN * col, struct _drecord *drec, const TDS_DBC* dbc TDS_UNUSED) { drec->sql_desc_concise_type = SQL_LONGVARBINARY; /* TODO ??? */ @@ -115,7 +115,7 @@ data_clrudt_set_type_info(TDSCOLUMN * col, struct _drecord *drec, SQLINTEGER odb } static void -data_sybbigtime_set_type_info(TDSCOLUMN * col, struct _drecord *drec, SQLINTEGER odbc_ver TDS_UNUSED) +data_sybbigtime_set_type_info(TDSCOLUMN * col, struct _drecord *drec, const TDS_DBC* dbc TDS_UNUSED) { if (col->on_server.column_type == SYB5BIGTIME) { drec->sql_desc_concise_type = SQL_SS_TIME2; @@ -140,14 +140,14 @@ data_sybbigtime_set_type_info(TDSCOLUMN * col, struct _drecord *drec, SQLINTEGER } static void -data_mstabletype_set_type_info(TDSCOLUMN *col TDS_UNUSED, struct _drecord *drec, SQLINTEGER odbc_ver TDS_UNUSED) +data_mstabletype_set_type_info(TDSCOLUMN *col TDS_UNUSED, struct _drecord *drec, const TDS_DBC* dbc TDS_UNUSED) { drec->sql_desc_concise_type = SQL_SS_TABLE; drec->sql_desc_octet_length = 0; } static void -data_generic_set_type_info(TDSCOLUMN * col, struct _drecord *drec, SQLINTEGER odbc_ver) +data_generic_set_type_info(TDSCOLUMN * col, struct _drecord *drec, const TDS_DBC* dbc) { TDS_SERVER_TYPE col_type = col->on_server.column_type; int col_size = col->on_server.column_size; @@ -260,12 +260,12 @@ data_generic_set_type_info(TDSCOLUMN * col, struct _drecord *drec, SQLINTEGER od case SYBREAL: drec->sql_desc_concise_type = SQL_REAL; drec->sql_desc_display_size = 14; - SET_INFO2("real", "", "", odbc_ver == SQL_OV_ODBC3 ? 24 : 7); + SET_INFO2("real", "", "", dbc->env->attr.odbc_version == SQL_OV_ODBC3 ? 24 : 7); case SYBFLT8: drec->sql_desc_concise_type = SQL_DOUBLE; drec->sql_desc_display_size = 24; /* FIXME -- what should the correct size be? */ - SET_INFO2("float", "", "", odbc_ver == SQL_OV_ODBC3 ? 53 : 15); + SET_INFO2("float", "", "", dbc->env->attr.odbc_version == SQL_OV_ODBC3 ? 53 : 15); case SYBMONEY: /* TODO check money format returned by proprietary ODBC, scale == 4 but we use 2 digits */ @@ -413,12 +413,12 @@ data_generic_set_type_info(TDSCOLUMN * col, struct _drecord *drec, SQLINTEGER od } static void -data_invalid_set_type_info(TDSCOLUMN * col TDS_UNUSED, struct _drecord *drec TDS_UNUSED, SQLINTEGER odbc_ver TDS_UNUSED) +data_invalid_set_type_info(TDSCOLUMN * col TDS_UNUSED, struct _drecord *drec TDS_UNUSED, const TDS_DBC* dbc TDS_UNUSED) { } void -odbc_set_sql_type_info(TDSCOLUMN * col, struct _drecord *drec, SQLINTEGER odbc_ver) +odbc_set_sql_type_info(TDSCOLUMN * col, struct _drecord *drec, const TDS_DBC *dbc) { drec->sql_desc_precision = col->column_prec; drec->sql_desc_scale = col->column_scale; @@ -427,7 +427,7 @@ odbc_set_sql_type_info(TDSCOLUMN * col, struct _drecord *drec, SQLINTEGER odbc_v drec->sql_desc_num_prec_radix = 0; drec->sql_desc_datetime_interval_code = 0; - ((TDS_FUNCS *) col->funcs)->set_type_info(col, drec, odbc_ver); + ((TDS_FUNCS *) col->funcs)->set_type_info(col, drec, dbc); drec->sql_desc_type = drec->sql_desc_concise_type; if (drec->sql_desc_concise_type == SQL_TYPE_TIMESTAMP) diff --git a/src/odbc/unittests/all_types.c b/src/odbc/unittests/all_types.c index f715c8b72..0611b90ea 100644 --- a/src/odbc/unittests/all_types.c +++ b/src/odbc/unittests/all_types.c @@ -19,7 +19,7 @@ test_type(TDSSOCKET *tds TDS_UNUSED, TDSCOLUMN *col) /* check that we can get type information from column */ struct _drecord drec; memset(&drec, 0, sizeof(drec)); - odbc_set_sql_type_info(col, &drec, SQL_OV_ODBC3); + odbc_set_sql_type_info(col, &drec, stmt->dbc); assert(drec.sql_desc_literal_prefix); assert(drec.sql_desc_literal_suffix); From 43c99d62a259d1d9b8a2629d26746f7b75d2a40c Mon Sep 17 00:00:00 2001 From: Matt McNabb Date: Wed, 1 Apr 2026 16:03:52 +1300 Subject: [PATCH 6/8] ODBC date/time format - compute display size descriptors Signed-off-by: Matt McNabb --- include/freetds/tds/convert.h | 3 + src/odbc/convert_tds2sql.c | 2 + src/odbc/odbc_data.c | 120 ++++++++++++++++++++++++++-------- src/tds/convert.c | 88 +++++++++++++++++-------- 4 files changed, 157 insertions(+), 56 deletions(-) diff --git a/include/freetds/tds/convert.h b/include/freetds/tds/convert.h index ed61f2f11..d80c3462a 100644 --- a/include/freetds/tds/convert.h +++ b/include/freetds/tds/convert.h @@ -92,6 +92,9 @@ TDS_INT tds_convert(const TDSCONTEXT * context, int srctype, const void *src, TD size_t tds_strftime(char *buf, size_t maxsize, const char *format, const TDSDATEREC * timeptr, int prec); +/** Maximum expected size of strftime() output for a given string and precision (excluding null terminator) */ +size_t tds_strftime_maxsize(const char* format, int prec); + /* Fast int to string (massively outperforms sprintf in hot loop). * No null termination; returns number of characters read. */ diff --git a/src/odbc/convert_tds2sql.c b/src/odbc/convert_tds2sql.c index ba1695f06..270bd9588 100644 --- a/src/odbc/convert_tds2sql.c +++ b/src/odbc/convert_tds2sql.c @@ -367,6 +367,8 @@ odbc_tds2sql(TDS_STMT * stmt, TDSCOLUMN *curcol, int srctype, TDS_CHAR * src, TD const char *fmt = NULL; const TDS_DATETIMEALL *dta = (const TDS_DATETIMEALL *) src; + /* This logic should match odbc_datetime_display_size() in odbc_data.c */ + /* TODO: so, instead of dta->time_prec this should be using curcol->column_prec? */ switch (srctype) { case SYBMSDATETIMEOFFSET: case SYBMSDATETIME2: diff --git a/src/odbc/odbc_data.c b/src/odbc/odbc_data.c index 903e26590..61fdce0f6 100644 --- a/src/odbc/odbc_data.c +++ b/src/odbc/odbc_data.c @@ -35,6 +35,7 @@ #include #include +#include #include #define SET_INFO(type, prefix, suffix) do { \ @@ -48,37 +49,96 @@ SET_INFO(type, prefix, suffix); \ } while(0) +/* For date/time types - ODBC specification says that SQL_DESC_LENGTH + * should be the character string representation size. + * https://learn.microsoft.com/en-us/sql/odbc/reference/syntax/sqlsetdescfield-function + */ +#define SET_INFO_DT(type, prefix, suffix) SET_INFO2(type, prefix, suffix, drec->sql_desc_display_size) + +static int odbc_datetime_display_size(const TDSCOLUMN * col, const TDS_DBC * dbc) +{ + /* This logic should match that of odbc_tds2sql() code block which + * selects the format string and precision. + * + * "precision" here refers to the number of digits to be generated by the + * TDS-specific %z format specifier, which represents fractions of a second. + * + * This value SHOULD be set in col->column_prec. It is so, for MS types. + * However, the non-MS types sometimes do not actually set column_prec, + * so odbc_tds2sql() hardcodes precisions for those types. + */ + + int prec; + const char* fmt; + int extra = 0; + + switch (tds_get_conversion_type(col->on_server.column_type, col->on_server.column_size)) + { + case SYBMSDATETIMEOFFSET: + /* Offset formatting hardcodes a " +HH:MM" suffix */ + extra = 7; + prec = col->column_prec; + goto datetime; + case SYBMSDATETIME2: + prec = col->column_prec; + goto datetime; + case SYB5BIGDATETIME: + prec = 6; + goto datetime; + case SYBDATETIME: + prec = 3; + goto datetime; + case SYBDATETIME4: + prec = 0; + datetime: + fmt = tds_dstr_cstr(&dbc->datetime_fmt); + break; + case SYBMSTIME: + prec = col->column_prec; + goto time; + case SYB5BIGTIME: + prec = 6; + goto time; + case SYBTIME: + prec = 3; + time: + fmt = tds_dstr_cstr(&dbc->time_fmt); + break; + case SYBMSDATE: + case SYBDATE: + prec = 0; + fmt = tds_dstr_cstr(&dbc->date_fmt); + break; + default: + return 0; + } + + return tds_strftime_maxsize(fmt, prec) + extra; +} + static void data_msdatetime_set_type_info(TDSCOLUMN * col, struct _drecord *drec, const TDS_DBC *dbc) { - int decimals = col->column_prec ? col->column_prec + 1: 0; + drec->sql_desc_display_size = odbc_datetime_display_size(col, dbc); switch (col->on_server.column_type) { case SYBMSTIME: drec->sql_desc_octet_length = sizeof(SQL_SS_TIME2_STRUCT); drec->sql_desc_concise_type = SQL_SS_TIME2; - /* always hh:mm:ss[.fff] */ - drec->sql_desc_display_size = 8 + decimals; - SET_INFO2("time", "'", "'", 8 + decimals); + SET_INFO_DT("time", "'", "'"); case SYBMSDATE: drec->sql_desc_octet_length = sizeof(DATE_STRUCT); drec->sql_desc_concise_type = SQL_TYPE_DATE; - /* always yyyy-mm-dd ?? */ - drec->sql_desc_display_size = 10; - SET_INFO2("date", "'", "'", 10); + SET_INFO_DT("date", "'", "'"); case SYBMSDATETIMEOFFSET: drec->sql_desc_octet_length = sizeof(SQL_SS_TIMESTAMPOFFSET_STRUCT); drec->sql_desc_concise_type = SQL_SS_TIMESTAMPOFFSET; - /* we always format using yyyy-mm-dd hh:mm:ss[.fff] +HH:MM, see convert_tds2sql.c */ - drec->sql_desc_display_size = 26 + decimals; - SET_INFO2("datetimeoffset", "'", "'", 26 + decimals); + SET_INFO_DT("datetimeoffset", "'", "'"); case SYBMSDATETIME2: drec->sql_desc_octet_length = sizeof(TIMESTAMP_STRUCT); drec->sql_desc_concise_type = SQL_TYPE_TIMESTAMP; drec->sql_desc_datetime_interval_code = SQL_CODE_TIMESTAMP; - /* we always format using yyyy-mm-dd hh:mm:ss[.fff], see convert_tds2sql.c */ - drec->sql_desc_display_size = 19 + decimals; - SET_INFO2("datetime2", "'", "'", 19 + decimals); + SET_INFO_DT("datetime2", "'", "'"); default: break; } @@ -117,26 +177,25 @@ data_clrudt_set_type_info(TDSCOLUMN * col, struct _drecord *drec, const TDS_DBC* static void data_sybbigtime_set_type_info(TDSCOLUMN * col, struct _drecord *drec, const TDS_DBC* dbc TDS_UNUSED) { + drec->sql_desc_display_size = odbc_datetime_display_size(col, dbc); + if (col->on_server.column_type == SYB5BIGTIME) { drec->sql_desc_concise_type = SQL_SS_TIME2; - /* we always format using hh:mm:ss[.ffffff], see convert_tds2sql.c */ - drec->sql_desc_display_size = 15; drec->sql_desc_octet_length = sizeof(SQL_SS_TIME2_STRUCT); drec->sql_desc_precision = 6; drec->sql_desc_scale = 6; drec->sql_desc_datetime_interval_code = SQL_CODE_TIMESTAMP; - SET_INFO2("bigtime", "'", "'", 15); + SET_INFO_DT("bigtime", "'", "'"); } assert(col->on_server.column_type == SYB5BIGDATETIME); drec->sql_desc_concise_type = SQL_TYPE_TIMESTAMP; - drec->sql_desc_display_size = 26; drec->sql_desc_octet_length = sizeof(TIMESTAMP_STRUCT); drec->sql_desc_precision = 6; drec->sql_desc_scale = 6; drec->sql_desc_datetime_interval_code = SQL_CODE_TIMESTAMP; - SET_INFO2("bigdatetime", "'", "'", 26); + SET_INFO_DT("bigdatetime", "'", "'"); } static void @@ -151,6 +210,8 @@ data_generic_set_type_info(TDSCOLUMN * col, struct _drecord *drec, const TDS_DBC { TDS_SERVER_TYPE col_type = col->on_server.column_type; int col_size = col->on_server.column_size; + drec->sql_desc_display_size = 26; + int datetime_display_size = odbc_datetime_display_size(col, dbc); switch (tds_get_conversion_type(col_type, col_size)) { case XSYBNCHAR: @@ -286,21 +347,23 @@ data_generic_set_type_info(TDSCOLUMN * col, struct _drecord *drec, const TDS_DBC case SYBDATETIME: drec->sql_desc_concise_type = SQL_TYPE_TIMESTAMP; - drec->sql_desc_display_size = 23; + drec->sql_desc_display_size = datetime_display_size; drec->sql_desc_octet_length = sizeof(TIMESTAMP_STRUCT); drec->sql_desc_precision = 3; drec->sql_desc_scale = 3; drec->sql_desc_datetime_interval_code = SQL_CODE_TIMESTAMP; - SET_INFO2("datetime", "'", "'", 23); + SET_INFO_DT("datetime", "'", "'"); case SYBDATETIME4: + /* MS ODBC driver gives 19 for display_size and 16 for length, + * the default display string has length 19. + * This appears to be a MS bug that we don't need to replicate, + * the two length values should match. */ drec->sql_desc_concise_type = SQL_TYPE_TIMESTAMP; - /* TODO dependent on precision (decimal second digits) */ - /* we always format using yyyy-mm-dd hh:mm:ss[.fff], see convert_tds2sql.c */ - drec->sql_desc_display_size = 19; + drec->sql_desc_display_size = datetime_display_size; drec->sql_desc_octet_length = sizeof(TIMESTAMP_STRUCT); drec->sql_desc_datetime_interval_code = SQL_CODE_TIMESTAMP; - SET_INFO2("datetime", "'", "'", 16); + SET_INFO_DT("datetime", "'", "'"); /* The following two types are just Sybase types but as mainly our ODBC * driver is much more compatible with Windows use attributes similar @@ -313,18 +376,17 @@ data_generic_set_type_info(TDSCOLUMN * col, struct _drecord *drec, const TDS_DBC case SYBTIME: drec->sql_desc_concise_type = SQL_SS_TIME2; drec->sql_desc_octet_length = sizeof(SQL_SS_TIME2_STRUCT); - /* we always format using hh:mm:ss[.fff], see convert_tds2sql.c */ - drec->sql_desc_display_size = 12; + drec->sql_desc_display_size = datetime_display_size; drec->sql_desc_precision = 3; drec->sql_desc_scale = 3; - SET_INFO2("time", "'", "'", 12); + SET_INFO_DT("time", "'", "'"); case SYBDATE: drec->sql_desc_octet_length = sizeof(DATE_STRUCT); drec->sql_desc_concise_type = SQL_TYPE_DATE; /* we always format using yyyy-mm-dd, see convert_tds2sql.c */ - drec->sql_desc_display_size = 10; - SET_INFO2("date", "'", "'", 10); + drec->sql_desc_display_size = datetime_display_size; + SET_INFO_DT("date", "'", "'"); case XSYBBINARY: case SYBBINARY: diff --git a/src/tds/convert.c b/src/tds/convert.c index e78fadc6b..b22d31ccb 100644 --- a/src/tds/convert.c +++ b/src/tds/convert.c @@ -3167,17 +3167,8 @@ two_digit(char *out, int num) out[0] = ' '; } -/** - * format a date string according to an "extended" strftime(3) formatting definition. - * @param buf output buffer - * @param maxsize size of buffer in bytes (space include terminator) - * @param format format string passed to strftime(3), except that %z represents fraction of seconds. - * @param dr date to convert - * @param prec second fraction precision (0-7). - * @return length of string returned, 0 for error - */ -size_t -tds_strftime(char *buf, size_t maxsize, const char *format, const TDSDATEREC * dr, int prec) +static size_t tds_strftime_tm +(char* buf, size_t maxsize, const char* format, const struct tm* ptm, TDS_INT decimicrosecond, int prec) { struct tm tm; @@ -3189,25 +3180,19 @@ tds_strftime(char *buf, size_t maxsize, const char *format, const TDSDATEREC * d assert(buf); assert(format); - assert(dr); - if (prec < 0 || prec > 7) - prec = 3; + assert(ptm); + tm = *ptm; - tm.tm_sec = dr->second; - tm.tm_min = dr->minute; - tm.tm_hour = dr->hour; - tm.tm_mday = dr->day; - tm.tm_mon = dr->month; - tm.tm_year = dr->year - 1900; - tm.tm_wday = dr->weekday; - tm.tm_yday = dr->dayofyear; - tm.tm_isdst = 0; + /* TDS date formatting does not support time zones */ #ifdef HAVE_STRUCT_TM_TM_ZONE tm.tm_zone = NULL; #elif defined(HAVE_STRUCT_TM___TM_ZONE) tm.__tm_zone = NULL; #endif + if (prec < 0 || prec > 7) + prec = 3; + /* more characters are required because we replace %z with up to 7 digits */ our_format = tds_new(char, strlen(format) + (1 + 5 + 1)); if (!our_format) @@ -3230,13 +3215,13 @@ tds_strftime(char *buf, size_t maxsize, const char *format, const TDSDATEREC * d case 'e': /* not portable: day of month, single digit preceded by a blank */ /* not supported on old Windows versions */ - two_digit(pz-1, dr->day); + two_digit(pz-1, tm.tm_mday); break; case 'l': /* not portable: 12-hour, single digit preceded by a blank */ /* not supported on: SCO Unix, AIX, HP-UX, Windows * supported on: *BSD, MacOS, Linux, Solaris */ - two_digit(pz-1, (dr->hour + 11u) % 12u + 1); + two_digit(pz-1, (tm.tm_hour + 11u) % 12u + 1); break; case '1': case '2': @@ -3253,7 +3238,7 @@ tds_strftime(char *buf, size_t maxsize, const char *format, const TDSDATEREC * d z_opt_len = 3; case 'z': /* - * Look for "%z" in the format string. If found, replace it with dr->milliseconds. + * Look for "%z" in the format string. If found, replace it with decimicrosecond. * For example, if milliseconds is 124, the format string * "%b %d %Y %H:%M:%S.%z" would become * "%b %d %Y %H:%M:%S.124". @@ -3272,7 +3257,7 @@ tds_strftime(char *buf, size_t maxsize, const char *format, const TDSDATEREC * d * too big, discard leading digits. */ memset(buf, '0', sizeof(buf)); - tds_u32toa_fast_right(buf, dr->decimicrosecond & 0x7fffffff); + tds_u32toa_fast_right(buf, decimicrosecond & 0x7fffffff); memcpy(pz, buf + 3, prec); strcpy(pz + prec, format + (pz - our_format) + z_opt_len); pz += prec; @@ -3292,6 +3277,55 @@ tds_strftime(char *buf, size_t maxsize, const char *format, const TDSDATEREC * d return length; } +/** + * format a date string according to an "extended" strftime(3) formatting definition. + * @param buf output buffer + * @param maxsize size of buffer in bytes (space include terminator) + * @param format format string passed to strftime(3), except that %z represents fraction of seconds. + * @param dr date to convert + * @param prec second fraction precision (0-7). + * @return length of string returned, 0 for error + */ +size_t +tds_strftime(char* buf, size_t maxsize, const char* format, const TDSDATEREC* dr, int prec) +{ + struct tm tm = { 0 }; + + tm.tm_sec = dr->second; + tm.tm_min = dr->minute; + tm.tm_hour = dr->hour; + tm.tm_mday = dr->day; + tm.tm_mon = dr->month; + tm.tm_year = dr->year - 1900; + tm.tm_wday = dr->weekday; + tm.tm_yday = dr->dayofyear; + return tds_strftime_tm(buf, maxsize, format, &tm, dr->decimicrosecond, prec); +} + +size_t tds_strftime_maxsize(const char* format, int prec) +{ + char buf[1000]; + struct tm tm = { 0 }; + + /* The longest possible time. + * "Wednesday" for %A, "September" for %B. + * If they use other locale specific fields... Might get wrong answer. + * The consequences of an error here could be display truncation in + * ODBC clients which choose to depend on this; should not lead to + * buffer overflows. + */ + tm.tm_sec = 59; + tm.tm_min = 59; + tm.tm_hour = 19; + tm.tm_mday = 24; + tm.tm_mon = 9 - 1; + tm.tm_year = 2025 - 1900; + tm.tm_wday = 3; + tm.tm_yday = 266; + + return tds_strftime_tm(buf, sizeof buf, format, &tm, 1234567, prec); +} + #if 0 static TDS_UINT utf16len(const utf16_t * s) From 8c0257b92f2a8d199b47ebabb60e7b3f8ad90c30 Mon Sep 17 00:00:00 2001 From: Matt McNabb Date: Tue, 7 Apr 2026 11:34:13 +1200 Subject: [PATCH 7/8] o_describecol: Test correct behaviour, not MS bug replication Signed-off-by: Matt McNabb --- src/odbc/unittests/describecol.in | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/odbc/unittests/describecol.in b/src/odbc/unittests/describecol.in index 510683a7c..c3d2ff3c8 100644 --- a/src/odbc/unittests/describecol.in +++ b/src/odbc/unittests/describecol.in @@ -175,7 +175,7 @@ select smalldatetime '2006-04-14' attr SQL_COLUMN_LENGTH 16 attr SQL_COLUMN_PRECISION 16 attr SQL_COLUMN_SCALE 0 -attr SQL_DESC_LENGTH 16 +attr SQL_DESC_LENGTH 19 attr SQL_DESC_OCTET_LENGTH 16 attr SQL_DESC_PRECISION 0 attr SQL_DESC_SCALE 0 @@ -421,7 +421,7 @@ select smalldatetime '2006-04-14' attr SQL_COLUMN_LENGTH 16 attr SQL_COLUMN_PRECISION 16 attr SQL_COLUMN_SCALE 0 -attr SQL_DESC_LENGTH 16 +attr SQL_DESC_LENGTH 19 attr SQL_DESC_OCTET_LENGTH 16 attr SQL_DESC_PRECISION 0 attr SQL_DESC_SCALE 0 From 442a5c552a249c18f9dddf3e86c49a1493dfac32 Mon Sep 17 00:00:00 2001 From: Matt McNabb Date: Tue, 7 Apr 2026 15:40:30 +1200 Subject: [PATCH 8/8] o_date: Some features only available on TDS 7.3+ Signed-off-by: Matt McNabb --- src/odbc/unittests/date.c | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/odbc/unittests/date.c b/src/odbc/unittests/date.c index db3f44213..549adfefc 100644 --- a/src/odbc/unittests/date.c +++ b/src/odbc/unittests/date.c @@ -29,18 +29,33 @@ DoTest(int n) break; case 3: - SQLSetConnectAttr(odbc_conn, SQL_COPT_TDSODBC_DATETIME_FORMAT, T("%d-%m-%Y %H:%M:%S"), SQL_NTS); + SQLSetConnectAttr(odbc_conn, SQL_COPT_TDSODBC_DATETIME_FORMAT, T("**%d-%m-%Y %H:%M:%S**"), SQL_NTS); odbc_command("select convert(datetime, '2002-12-27 18:43:21')"); - expect = "27-12-2002 18:43:21"; + expect = "**27-12-2002 18:43:21**"; break; case 4: + /* MS datetime2 wire format introduced in TDS 7.3 + * (earlier TDS versions use NVARCHAR wire format here) */ + if (odbc_tds_version() < 0x703) + return 0; + SQLSetConnectAttr(odbc_conn, SQL_COPT_TDSODBC_DATETIME_FORMAT, T("**%d-%m-%Y %H:%M:%S.%z**"), SQL_NTS); + odbc_command("select convert(datetime2(7), '2002-12-27 18:43:21')"); + expect = "**27-12-2002 18:43:21.0000000**"; + break; + + case 5: + /* MS date-only and time-only wire formats introduced in TDS 7.3 */ + if (odbc_tds_version() < 0x703) + return 0; SQLSetConnectAttr(odbc_conn, SQL_COPT_TDSODBC_DATE_FORMAT, T("**%d-%m-%Y**"), SQL_NTS); odbc_command("select convert(date, '2002-12-27 18:43:21')"); expect = "**27-12-2002**"; break; - case 5: + case 6: + if (odbc_tds_version() < 0x703) + return 0; SQLSetConnectAttr(odbc_conn, SQL_COPT_TDSODBC_TIME_FORMAT, T("**%H:%M:%S**"), SQL_NTS); odbc_command("select convert(time, '2002-12-27 18:43:21')"); expect = "**18:43:21**";