|
28 | 28 | #define SQL_MAX_NUMERIC_LEN 16 |
29 | 29 | #define SQL_SS_XML (-152) |
30 | 30 | #define SQL_SS_UDT (-151) |
| 31 | +#define SQL_SS_VARIANT (-150) |
| 32 | +#define SQL_CA_SS_BASE 1200 |
| 33 | +#define SQL_CA_SS_VARIANT_TYPE (SQL_CA_SS_BASE + 15) |
| 34 | +#define SQL_CA_SS_VARIANT_SQL_TYPE (SQL_CA_SS_BASE + 16) |
| 35 | +#define SQL_CA_SS_VARIANT_SERVER_TYPE (SQL_CA_SS_BASE + 17) |
31 | 36 |
|
32 | 37 | #define STRINGIFY_FOR_CASE(x) \ |
33 | 38 | case x: \ |
@@ -2907,6 +2912,55 @@ py::object FetchLobColumnData(SQLHSTMT hStmt, SQLUSMALLINT colIndex, SQLSMALLINT |
2907 | 2912 | } |
2908 | 2913 | } |
2909 | 2914 |
|
| 2915 | +// Helper function to map sql_variant's underlying C type to SQL data type |
| 2916 | +// This allows sql_variant to reuse existing fetch logic for each data type |
| 2917 | +SQLSMALLINT MapVariantCTypeToSQLType(SQLLEN variantCType) { |
| 2918 | + switch (variantCType) { |
| 2919 | + case SQL_C_SLONG: |
| 2920 | + case SQL_C_LONG: |
| 2921 | + return SQL_INTEGER; |
| 2922 | + case SQL_C_SSHORT: |
| 2923 | + case SQL_C_SHORT: |
| 2924 | + return SQL_SMALLINT; |
| 2925 | + case SQL_C_SBIGINT: |
| 2926 | + return SQL_BIGINT; |
| 2927 | + case SQL_C_FLOAT: |
| 2928 | + return SQL_REAL; |
| 2929 | + case SQL_C_DOUBLE: |
| 2930 | + return SQL_DOUBLE; |
| 2931 | + case SQL_C_BIT: |
| 2932 | + return SQL_BIT; |
| 2933 | + case SQL_C_CHAR: |
| 2934 | + return SQL_VARCHAR; |
| 2935 | + case SQL_C_WCHAR: |
| 2936 | + return SQL_WVARCHAR; |
| 2937 | + // Date/time types - handle both old-style (9, 10, 11) and new-style (91, 92, 93) codes |
| 2938 | + case 9: // SQL_C_DATE (old style) |
| 2939 | + case SQL_C_TYPE_DATE: // 91 (new style) |
| 2940 | + return SQL_TYPE_DATE; |
| 2941 | + case 10: // SQL_C_TIME (old style) |
| 2942 | + case SQL_C_TYPE_TIME: // 92 (new style) |
| 2943 | + case 16384: // SQL Server variant TIME type (observed value) |
| 2944 | + return SQL_TYPE_TIME; |
| 2945 | + case 11: // SQL_C_TIMESTAMP (old style) |
| 2946 | + case SQL_C_TYPE_TIMESTAMP: // 93 (new style) |
| 2947 | + return SQL_TYPE_TIMESTAMP; |
| 2948 | + case SQL_C_BINARY: |
| 2949 | + return SQL_VARBINARY; |
| 2950 | + case SQL_C_GUID: |
| 2951 | + return SQL_GUID; |
| 2952 | + case SQL_C_NUMERIC: |
| 2953 | + return SQL_NUMERIC; |
| 2954 | + case SQL_C_TINYINT: |
| 2955 | + case SQL_C_UTINYINT: |
| 2956 | + case SQL_C_STINYINT: |
| 2957 | + return SQL_TINYINT; |
| 2958 | + default: |
| 2959 | + // Unknown type, fallback to WVARCHAR for string conversion |
| 2960 | + return SQL_WVARCHAR; |
| 2961 | + } |
| 2962 | +} |
| 2963 | + |
2910 | 2964 | // Helper function to retrieve column data |
2911 | 2965 | SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, py::list& row, |
2912 | 2966 | const std::string& charEncoding = "utf-8", |
@@ -2945,7 +2999,40 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p |
2945 | 2999 | continue; |
2946 | 3000 | } |
2947 | 3001 |
|
2948 | | - switch (dataType) { |
| 3002 | + printf("[DEBUG] SQLGetData_wrap: Column %d - dataType=%d, columnSize=%lu\n", i, dataType, |
| 3003 | + (unsigned long)columnSize); |
| 3004 | + |
| 3005 | + // Preprocess sql_variant: detect underlying type and handle NULL |
| 3006 | + // This allows reuse of existing fetch logic instead of duplicating code |
| 3007 | + SQLSMALLINT effectiveDataType = dataType; |
| 3008 | + if (dataType == SQL_SS_VARIANT) { |
| 3009 | + // Step 1: Check for NULL using header read |
| 3010 | + SQLLEN indicator; |
| 3011 | + ret = SQLGetData_ptr(hStmt, i, SQL_C_BINARY, NULL, 0, &indicator); |
| 3012 | + if (indicator == SQL_NULL_DATA) { |
| 3013 | + row.append(py::none()); |
| 3014 | + continue; // Skip to next column |
| 3015 | + } |
| 3016 | + |
| 3017 | + // Step 2: Get the variant's underlying C data type |
| 3018 | + SQLLEN variantCType = 0; |
| 3019 | + ret = |
| 3020 | + SQLColAttribute_ptr(hStmt, i, SQL_CA_SS_VARIANT_TYPE, NULL, 0, NULL, &variantCType); |
| 3021 | + if (!SQL_SUCCEEDED(ret)) { |
| 3022 | + LOG("SQLGetData: Failed to get sql_variant underlying type for column %d", i); |
| 3023 | + row.append(py::none()); |
| 3024 | + continue; // Skip to next column |
| 3025 | + } |
| 3026 | + |
| 3027 | + printf("[DEBUG] SQLGetData_wrap: sql_variant column %d has variantCType=%ld\n", i, |
| 3028 | + (long)variantCType); |
| 3029 | + |
| 3030 | + // Step 3: Map C type to SQL type so existing code can handle it |
| 3031 | + effectiveDataType = MapVariantCTypeToSQLType(variantCType); |
| 3032 | + printf("[DEBUG] SQLGetData_wrap: Mapped to effectiveDataType=%d\n", effectiveDataType); |
| 3033 | + } |
| 3034 | + |
| 3035 | + switch (effectiveDataType) { |
2949 | 3036 | case SQL_CHAR: |
2950 | 3037 | case SQL_VARCHAR: |
2951 | 3038 | case SQL_LONGVARCHAR: { |
@@ -4048,7 +4135,8 @@ size_t calculateRowSize(py::list& columnNames, SQLUSMALLINT numCols) { |
4048 | 4135 | break; |
4049 | 4136 | case SQL_SS_UDT: |
4050 | 4137 | rowSize += (static_cast<SQLLEN>(columnSize) == SQL_NO_TOTAL || columnSize == 0) |
4051 | | - ? SQL_MAX_LOB_SIZE : columnSize; |
| 4138 | + ? SQL_MAX_LOB_SIZE |
| 4139 | + : columnSize; |
4052 | 4140 | break; |
4053 | 4141 | case SQL_BINARY: |
4054 | 4142 | case SQL_VARBINARY: |
@@ -4110,11 +4198,19 @@ SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetch |
4110 | 4198 | SQLSMALLINT dataType = colMeta["DataType"].cast<SQLSMALLINT>(); |
4111 | 4199 | SQLULEN columnSize = colMeta["ColumnSize"].cast<SQLULEN>(); |
4112 | 4200 |
|
4113 | | - if ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR || dataType == SQL_VARCHAR || |
4114 | | - dataType == SQL_LONGVARCHAR || dataType == SQL_VARBINARY || |
4115 | | - dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML || |
4116 | | - dataType == SQL_SS_UDT) && |
4117 | | - (columnSize == 0 || columnSize == SQL_NO_TOTAL || columnSize > SQL_MAX_LOB_SIZE)) { |
| 4201 | + printf("[DEBUG] FetchMany_wrap: Column %d - dataType=%d, columnSize=%lu\n", i + 1, dataType, |
| 4202 | + (unsigned long)columnSize); |
| 4203 | + |
| 4204 | + // Detect LOB columns that need SQLGetData streaming |
| 4205 | + // sql_variant always uses SQLGetData for native type preservation |
| 4206 | + if (dataType == SQL_SS_VARIANT) { |
| 4207 | + lobColumns.push_back(i + 1); // 1-based |
| 4208 | + } else if ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR || |
| 4209 | + dataType == SQL_VARCHAR || dataType == SQL_LONGVARCHAR || |
| 4210 | + dataType == SQL_VARBINARY || dataType == SQL_LONGVARBINARY || |
| 4211 | + dataType == SQL_SS_XML || dataType == SQL_SS_UDT) && |
| 4212 | + (columnSize == 0 || columnSize == SQL_NO_TOTAL || |
| 4213 | + columnSize > SQL_MAX_LOB_SIZE)) { |
4118 | 4214 | lobColumns.push_back(i + 1); // 1-based |
4119 | 4215 | } |
4120 | 4216 | } |
@@ -4204,6 +4300,52 @@ SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows, |
4204 | 4300 | return ret; |
4205 | 4301 | } |
4206 | 4302 |
|
| 4303 | + // Detect LOB columns FIRST (before calculateRowSize) |
| 4304 | + // This allows sql_variant to skip the binding path entirely |
| 4305 | + std::vector<SQLUSMALLINT> lobColumns; |
| 4306 | + for (SQLSMALLINT i = 0; i < numCols; i++) { |
| 4307 | + auto colMeta = columnNames[i].cast<py::dict>(); |
| 4308 | + SQLSMALLINT dataType = colMeta["DataType"].cast<SQLSMALLINT>(); |
| 4309 | + SQLULEN columnSize = colMeta["ColumnSize"].cast<SQLULEN>(); |
| 4310 | + |
| 4311 | + printf("[DEBUG] FetchAll_wrap: Column %d - dataType=%d, columnSize=%lu\n", i + 1, dataType, |
| 4312 | + (unsigned long)columnSize); |
| 4313 | + |
| 4314 | + // Detect LOB columns that need SQLGetData streaming |
| 4315 | + // sql_variant always uses SQLGetData for native type preservation |
| 4316 | + if (dataType == SQL_SS_VARIANT) { |
| 4317 | + lobColumns.push_back(i + 1); // 1-based |
| 4318 | + } else if ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR || |
| 4319 | + dataType == SQL_VARCHAR || dataType == SQL_LONGVARCHAR || |
| 4320 | + dataType == SQL_VARBINARY || dataType == SQL_LONGVARBINARY || |
| 4321 | + dataType == SQL_SS_XML) && |
| 4322 | + (columnSize == 0 || columnSize == SQL_NO_TOTAL || |
| 4323 | + columnSize > SQL_MAX_LOB_SIZE)) { |
| 4324 | + lobColumns.push_back(i + 1); // 1-based |
| 4325 | + } |
| 4326 | + } |
| 4327 | + |
| 4328 | + // If we have LOBs → fall back to row-by-row fetch + SQLGetData_wrap |
| 4329 | + if (!lobColumns.empty()) { |
| 4330 | + LOG("FetchAll_wrap: LOB columns detected (%zu columns), using per-row " |
| 4331 | + "SQLGetData path", |
| 4332 | + lobColumns.size()); |
| 4333 | + while (true) { |
| 4334 | + ret = SQLFetch_ptr(hStmt); |
| 4335 | + if (ret == SQL_NO_DATA) |
| 4336 | + break; |
| 4337 | + if (!SQL_SUCCEEDED(ret)) |
| 4338 | + return ret; |
| 4339 | + |
| 4340 | + py::list row; |
| 4341 | + SQLGetData_wrap(StatementHandle, numCols, row, charEncoding, |
| 4342 | + wcharEncoding); // <-- streams LOBs correctly |
| 4343 | + rows.append(row); |
| 4344 | + } |
| 4345 | + return SQL_SUCCESS; |
| 4346 | + } |
| 4347 | + |
| 4348 | + // No LOBs detected - use binding path with batch fetching |
4207 | 4349 | // Define a memory limit (1 GB) |
4208 | 4350 | const size_t memoryLimit = 1ULL * 1024 * 1024 * 1024; |
4209 | 4351 | size_t totalRowSize = calculateRowSize(columnNames, numCols); |
@@ -4252,8 +4394,7 @@ SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows, |
4252 | 4394 |
|
4253 | 4395 | if ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR || dataType == SQL_VARCHAR || |
4254 | 4396 | dataType == SQL_LONGVARCHAR || dataType == SQL_VARBINARY || |
4255 | | - dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML || |
4256 | | - dataType == SQL_SS_UDT) && |
| 4397 | + dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML || dataType == SQL_SS_UDT) && |
4257 | 4398 | (columnSize == 0 || columnSize == SQL_NO_TOTAL || columnSize > SQL_MAX_LOB_SIZE)) { |
4258 | 4399 | lobColumns.push_back(i + 1); // 1-based |
4259 | 4400 | } |
|
0 commit comments