mkleehammer / pyodbc

Python ODBC bridge
https://github.com/mkleehammer/pyodbc/wiki
MIT No Attribution
2.88k stars 561 forks source link

Implemented support for the SQL Server sql_variant data type #1354

Open will-hinson opened 1 month ago

will-hinson commented 1 month ago

This commit adds support for decoding the SQL Server sql_variant data type.

Attempting to read records containing sql_variant columns in the current version of pyodbc (5.1.0) results in the following exception:

pyodbc.ProgrammingError: ('ODBC SQL type -150 is not yet supported.  column-index=0  type=-150', 'HY106')

As mentioned in #307, it is not trivial to write a custom output converter for sql_variant as the bytes returned by SQL Server give no indication as to the underlying data type of each value.

This commit adds a GetData_SqlVariant() method which retrieves the underlying data type per the Microsoft documentation using SQLColAttribute():

// Call SQLGetData on the current column with a data length of 0. According to MS, this makes
// the ODBC driver read the sql_variant header which contains the underlying data type
pBuff = 0;
indicator = 0;
retcode = SQLGetData(cur->hstmt, static_cast<SQLSMALLINT>(iCol + 1), SQL_C_BINARY,   
                                &pBuff, 0, &indicator);
if (!SQL_SUCCEEDED(retcode))
    return RaiseErrorFromHandle(cur->cnxn, "SQLGetData", cur->cnxn->hdbc, cur->hstmt);

// Get the SQL_CA_SS_VARIANT_TYPE field for the column which will contain the underlying data type
variantType = 0;
retcode = SQLColAttribute(cur->hstmt, iCol + 1, SQL_CA_SS_VARIANT_TYPE, NULL, 0, NULL, &variantType);
if (!SQL_SUCCEEDED(retcode))
    return RaiseErrorFromHandle(cur->cnxn, "SQLColAttribute", cur->cnxn->hdbc, cur->hstmt);

This underlying data type is then patched into the ColumnInfo struct for the current column and GetData() is invoked using this new type:

// Replace the original SQL_VARIANT data type with the underlying data type then call GetData() again
cur->colinfos[iCol].sql_type = static_cast<SQLSMALLINT>(variantType);
return GetData(cur, iCol);

The sql_variant type is used by a number of system functions and views and this commit adds support for reading them:

import pyodbc

connection = pyodbc.connect(...)
print(
    connection.execute(
        """
        SELECT
            SERVERPROPERTY('MachineName'),
            SERVERPROPERTY('InstanceName'),
            SERVERPROPERTY('Edition'),
            SERVERPROPERTY('ProductVersion'),
            SERVERPROPERTY('ProductLevel');
        """
    ).fetchone()
)

# ('acfd83beaa19', '', 'Developer Edition (64-bit)', '16.0.4003.1', 'RTM')
gordthompson commented 3 weeks ago

Hey, Will. This looks promising. Thanks for contributing!

Can we add a test to tests/sqlserver_test.py that will exercise your code a bit?

will-hinson commented 3 weeks ago

Hello @gordthompson,

Of course! I have pushed another commit with an added test for the sql_variant type.

I also made the following related changes:

Please let me know if further action is required on my part. Thanks!