Bug #120402 Wrong COLUMN_SIZE reported for datetime columns
Submitted: 4 May 21:49
Reporter: Bob Kline Email Updates:
Status: Open Impact on me:
None 
Category:Connector / ODBC Severity:S3 (Non-critical)
Version:9.6.0-1ubuntu22.04 OS:Ubuntu (24.04)
Assigned to: CPU Architecture:x86 (64-bit)

[4 May 21:49] Bob Kline
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").