Description:
According to the ODBC documentation for the COLUMN_SIZE column returned by SQLGetTypeInfo: "For datetime data types, this is the length in characters of the string representation (assuming the maximum allowed precision of the fractional seconds component)." However, with the MySQL ODBC connector:
TYPE_NAME COLUMN_SIZE
-------------------------------- -----------
datetime 21
timestamp 14
21 would only allow for a single decimal digit for the fractional portion of a second, and 14 wouldn't even allow for the integer portion of the second, much less the microseconds or the time zone offset.
How to repeat:
Compile and run this program, providing the connection string as the command-line argument:
#include <stdio.h>
#include <stdlib.h>
#include <sql.h>
#include <sqlext.h>
static void check(SQLRETURN ret, SQLSMALLINT htype, SQLHANDLE h, const char* msg)
{
if (SQL_SUCCEEDED(ret))
return;
SQLCHAR state[6], text[256];
SQLINTEGER native;
SQLSMALLINT len;
SQLGetDiagRec(htype, h, 1, state, &native, text, sizeof(text), &len);
fprintf(stderr, "ERROR %s: %s (%s, native=%d)\n", msg, text, state, native);
exit(1);
}
int main(int argc, char* argv[])
{
if (argc < 2) {
fprintf(stderr, "Usage: %s <connection-string>\n", argv[0]);
return 1;
}
printf("sizeof(SQL_TIMESTAMP_STRUCT) = %zu\n", sizeof(SQL_TIMESTAMP_STRUCT));
SQLHENV henv = SQL_NULL_HANDLE;
SQLHDBC hdbc = SQL_NULL_HANDLE;
SQLHSTMT hstmt = SQL_NULL_HANDLE;
SQLRETURN ret;
ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv);
check(ret, SQL_HANDLE_ENV, henv, "SQLAllocHandle(ENV)");
ret = SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
check(ret, SQL_HANDLE_ENV, henv, "SQLSetEnvAttr");
ret = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc);
check(ret, SQL_HANDLE_ENV, henv, "SQLAllocHandle(DBC)");
ret = SQLDriverConnect(hdbc, NULL, (SQLCHAR*)argv[1], SQL_NTS,
NULL, 0, NULL, SQL_DRIVER_NOPROMPT);
check(ret, SQL_HANDLE_DBC, hdbc, "SQLDriverConnect");
ret = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt);
check(ret, SQL_HANDLE_DBC, hdbc, "SQLAllocHandle(STMT)");
ret = SQLGetTypeInfo(hstmt, SQL_TYPE_TIMESTAMP);
check(ret, SQL_HANDLE_STMT, hstmt, "SQLGetTypeInfo");
printf("%-32s %s\n", "TYPE_NAME", "COLUMN_SIZE");
printf("%-32s %s\n", "--------------------------------", "-----------");
while (SQL_SUCCEEDED(SQLFetch(hstmt)))
{
SQLCHAR type_name[128];
SQLLEN type_name_ind;
SQLINTEGER column_size;
SQLLEN column_size_ind;
ret = SQLGetData(hstmt, 1, SQL_C_CHAR, type_name, sizeof(type_name), &type_name_ind);
check(ret, SQL_HANDLE_STMT, hstmt, "SQLGetData(TYPE_NAME)");
ret = SQLGetData(hstmt, 3, SQL_C_SLONG, &column_size, sizeof(column_size), &column_size_ind);
check(ret, SQL_HANDLE_STMT, hstmt, "SQLGetData(COLUMN_SIZE)");
if (column_size_ind == SQL_NULL_DATA)
printf("%-32s NULL\n", type_name);
else
printf("%-32s %d\n", type_name, column_size);
}
SQLFreeHandle(SQL_HANDLE_STMT, hstmt);
SQLDisconnect(hdbc);
SQLFreeHandle(SQL_HANDLE_DBC, hdbc);
SQLFreeHandle(SQL_HANDLE_ENV, henv);
return 0;
}
You can also see the effect of the bug with this Python script:
import argparse
import datetime
import pyodbc
VALUE = datetime.datetime(2000, 1, 2, 3, 4, 5, 123456)
parser = argparse.ArgumentParser()
parser.add_argument("conn")
args = parser.parse_args()
conn = pyodbc.connect(args.conn)
cursor = conn.cursor()
cursor.execute("CREATE TEMPORARY TABLE truncated_datetime_repro (dt datetime(6))")
cursor.execute("INSERT INTO truncated_datetime_repro VALUES (?)", VALUE)
cursor.execute("SELECT dt FROM truncated_datetime_repro")
print("expected:", VALUE)
print(" got:", cursor.fetchval())
Suggested fix:
Return 32 for timestamp ("YYYY-MM-DD HH:MM:SS.999999+00:00") and 26 for datetime ("YYYY-MM-DD HH:MM:SS.999999").
Description: According to the ODBC documentation for the COLUMN_SIZE column returned by SQLGetTypeInfo: "For datetime data types, this is the length in characters of the string representation (assuming the maximum allowed precision of the fractional seconds component)." However, with the MySQL ODBC connector: TYPE_NAME COLUMN_SIZE -------------------------------- ----------- datetime 21 timestamp 14 21 would only allow for a single decimal digit for the fractional portion of a second, and 14 wouldn't even allow for the integer portion of the second, much less the microseconds or the time zone offset. How to repeat: Compile and run this program, providing the connection string as the command-line argument: #include <stdio.h> #include <stdlib.h> #include <sql.h> #include <sqlext.h> static void check(SQLRETURN ret, SQLSMALLINT htype, SQLHANDLE h, const char* msg) { if (SQL_SUCCEEDED(ret)) return; SQLCHAR state[6], text[256]; SQLINTEGER native; SQLSMALLINT len; SQLGetDiagRec(htype, h, 1, state, &native, text, sizeof(text), &len); fprintf(stderr, "ERROR %s: %s (%s, native=%d)\n", msg, text, state, native); exit(1); } int main(int argc, char* argv[]) { if (argc < 2) { fprintf(stderr, "Usage: %s <connection-string>\n", argv[0]); return 1; } printf("sizeof(SQL_TIMESTAMP_STRUCT) = %zu\n", sizeof(SQL_TIMESTAMP_STRUCT)); SQLHENV henv = SQL_NULL_HANDLE; SQLHDBC hdbc = SQL_NULL_HANDLE; SQLHSTMT hstmt = SQL_NULL_HANDLE; SQLRETURN ret; ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &henv); check(ret, SQL_HANDLE_ENV, henv, "SQLAllocHandle(ENV)"); ret = SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0); check(ret, SQL_HANDLE_ENV, henv, "SQLSetEnvAttr"); ret = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc); check(ret, SQL_HANDLE_ENV, henv, "SQLAllocHandle(DBC)"); ret = SQLDriverConnect(hdbc, NULL, (SQLCHAR*)argv[1], SQL_NTS, NULL, 0, NULL, SQL_DRIVER_NOPROMPT); check(ret, SQL_HANDLE_DBC, hdbc, "SQLDriverConnect"); ret = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt); check(ret, SQL_HANDLE_DBC, hdbc, "SQLAllocHandle(STMT)"); ret = SQLGetTypeInfo(hstmt, SQL_TYPE_TIMESTAMP); check(ret, SQL_HANDLE_STMT, hstmt, "SQLGetTypeInfo"); printf("%-32s %s\n", "TYPE_NAME", "COLUMN_SIZE"); printf("%-32s %s\n", "--------------------------------", "-----------"); while (SQL_SUCCEEDED(SQLFetch(hstmt))) { SQLCHAR type_name[128]; SQLLEN type_name_ind; SQLINTEGER column_size; SQLLEN column_size_ind; ret = SQLGetData(hstmt, 1, SQL_C_CHAR, type_name, sizeof(type_name), &type_name_ind); check(ret, SQL_HANDLE_STMT, hstmt, "SQLGetData(TYPE_NAME)"); ret = SQLGetData(hstmt, 3, SQL_C_SLONG, &column_size, sizeof(column_size), &column_size_ind); check(ret, SQL_HANDLE_STMT, hstmt, "SQLGetData(COLUMN_SIZE)"); if (column_size_ind == SQL_NULL_DATA) printf("%-32s NULL\n", type_name); else printf("%-32s %d\n", type_name, column_size); } SQLFreeHandle(SQL_HANDLE_STMT, hstmt); SQLDisconnect(hdbc); SQLFreeHandle(SQL_HANDLE_DBC, hdbc); SQLFreeHandle(SQL_HANDLE_ENV, henv); return 0; } You can also see the effect of the bug with this Python script: import argparse import datetime import pyodbc VALUE = datetime.datetime(2000, 1, 2, 3, 4, 5, 123456) parser = argparse.ArgumentParser() parser.add_argument("conn") args = parser.parse_args() conn = pyodbc.connect(args.conn) cursor = conn.cursor() cursor.execute("CREATE TEMPORARY TABLE truncated_datetime_repro (dt datetime(6))") cursor.execute("INSERT INTO truncated_datetime_repro VALUES (?)", VALUE) cursor.execute("SELECT dt FROM truncated_datetime_repro") print("expected:", VALUE) print(" got:", cursor.fetchval()) Suggested fix: Return 32 for timestamp ("YYYY-MM-DD HH:MM:SS.999999+00:00") and 26 for datetime ("YYYY-MM-DD HH:MM:SS.999999").