|
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_VARIANT_TYPE (1215) |
| 33 | +#ifndef SQL_C_DATE |
| 34 | +#define SQL_C_DATE (9) |
| 35 | +#endif |
| 36 | +#ifndef SQL_C_TIME |
| 37 | +#define SQL_C_TIME (10) |
| 38 | +#endif |
| 39 | +#ifndef SQL_C_TIMESTAMP |
| 40 | +#define SQL_C_TIMESTAMP (11) |
| 41 | +#endif |
| 42 | +// SQL Server-specific variant TIME type code |
| 43 | +#define SQL_SS_VARIANT_TIME (16384) |
31 | 44 |
|
32 | 45 | #define STRINGIFY_FOR_CASE(x) \ |
33 | 46 | case x: \ |
@@ -471,14 +484,21 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, |
471 | 484 | hStmt, static_cast<SQLUSMALLINT>(paramIndex + 1), &describedType, |
472 | 485 | &describedSize, &describedDigits, &nullable); |
473 | 486 | if (!SQL_SUCCEEDED(rc)) { |
474 | | - LOG("BindParameters: SQLDescribeParam failed for " |
475 | | - "param[%d] (NULL parameter) - SQLRETURN=%d", |
476 | | - paramIndex, rc); |
477 | | - return rc; |
| 487 | + // SQLDescribeParam can fail for generic SELECT statements where |
| 488 | + // no table column is referenced. Fall back to SQL_VARCHAR as a safe |
| 489 | + // default. |
| 490 | + LOG_WARNING("BindParameters: SQLDescribeParam failed for " |
| 491 | + "param[%d] (NULL parameter) - SQLRETURN=%d, falling back to " |
| 492 | + "SQL_VARCHAR", |
| 493 | + paramIndex, rc); |
| 494 | + sqlType = SQL_VARCHAR; |
| 495 | + columnSize = 1; |
| 496 | + decimalDigits = 0; |
| 497 | + } else { |
| 498 | + sqlType = describedType; |
| 499 | + columnSize = describedSize; |
| 500 | + decimalDigits = describedDigits; |
478 | 501 | } |
479 | | - sqlType = describedType; |
480 | | - columnSize = describedSize; |
481 | | - decimalDigits = describedDigits; |
482 | 502 | } |
483 | 503 | dataPtr = nullptr; |
484 | 504 | strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers); |
@@ -2907,6 +2927,67 @@ py::object FetchLobColumnData(SQLHSTMT hStmt, SQLUSMALLINT colIndex, SQLSMALLINT |
2907 | 2927 | } |
2908 | 2928 | } |
2909 | 2929 |
|
| 2930 | +// Helper function to map sql_variant's underlying C type to SQL data type |
| 2931 | +// This allows sql_variant to reuse existing fetch logic for each data type |
| 2932 | +SQLSMALLINT MapVariantCTypeToSQLType(SQLLEN variantCType) { |
| 2933 | + switch (variantCType) { |
| 2934 | + case SQL_C_SLONG: |
| 2935 | + case SQL_C_LONG: |
| 2936 | + return SQL_INTEGER; |
| 2937 | + case SQL_C_SSHORT: |
| 2938 | + case SQL_C_SHORT: |
| 2939 | + return SQL_SMALLINT; |
| 2940 | + case SQL_C_SBIGINT: |
| 2941 | + return SQL_BIGINT; |
| 2942 | + case SQL_C_FLOAT: |
| 2943 | + return SQL_REAL; |
| 2944 | + case SQL_C_DOUBLE: |
| 2945 | + return SQL_DOUBLE; |
| 2946 | + case SQL_C_BIT: |
| 2947 | + return SQL_BIT; |
| 2948 | + case SQL_C_CHAR: |
| 2949 | + return SQL_VARCHAR; |
| 2950 | + case SQL_C_WCHAR: |
| 2951 | + return SQL_WVARCHAR; |
| 2952 | + case SQL_C_DATE: |
| 2953 | + case SQL_C_TYPE_DATE: |
| 2954 | + return SQL_TYPE_DATE; |
| 2955 | + case SQL_C_TIME: |
| 2956 | + case SQL_C_TYPE_TIME: |
| 2957 | + case SQL_SS_VARIANT_TIME: |
| 2958 | + return SQL_TYPE_TIME; |
| 2959 | + case SQL_C_TIMESTAMP: |
| 2960 | + case SQL_C_TYPE_TIMESTAMP: |
| 2961 | + return SQL_TYPE_TIMESTAMP; |
| 2962 | + case SQL_C_BINARY: |
| 2963 | + return SQL_VARBINARY; |
| 2964 | + case SQL_C_GUID: |
| 2965 | + return SQL_GUID; |
| 2966 | + case SQL_C_NUMERIC: |
| 2967 | + return SQL_NUMERIC; |
| 2968 | + case SQL_C_TINYINT: |
| 2969 | + case SQL_C_UTINYINT: |
| 2970 | + case SQL_C_STINYINT: |
| 2971 | + return SQL_TINYINT; |
| 2972 | + default: |
| 2973 | + // Unknown C type code - fallback to WVARCHAR for string conversion |
| 2974 | + // Note: SQL Server enforces sql_variant restrictions at INSERT time, preventing |
| 2975 | + // invalid types (text, ntext, image, timestamp, xml, MAX types, nested variants, |
| 2976 | + // spatial types, hierarchyid, UDTs) from being stored. By the time we fetch data, |
| 2977 | + // only valid base types exist. This default handles unmapped/future type codes. |
| 2978 | + return SQL_WVARCHAR; |
| 2979 | + } |
| 2980 | +} |
| 2981 | + |
| 2982 | +// Helper function to check if a column requires SQLGetData streaming (LOB or sql_variant) |
| 2983 | +static inline bool IsLobOrVariantColumn(SQLSMALLINT dataType, SQLULEN columnSize) { |
| 2984 | + return dataType == SQL_SS_VARIANT || |
| 2985 | + ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR || dataType == SQL_VARCHAR || |
| 2986 | + dataType == SQL_LONGVARCHAR || dataType == SQL_VARBINARY || |
| 2987 | + dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML || dataType == SQL_SS_UDT) && |
| 2988 | + (columnSize == 0 || columnSize == SQL_NO_TOTAL || columnSize > SQL_MAX_LOB_SIZE)); |
| 2989 | +} |
| 2990 | + |
2910 | 2991 | // Helper function to retrieve column data |
2911 | 2992 | SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, py::list& row, |
2912 | 2993 | const std::string& charEncoding = "utf-8", |
@@ -2945,7 +3026,42 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p |
2945 | 3026 | continue; |
2946 | 3027 | } |
2947 | 3028 |
|
2948 | | - switch (dataType) { |
| 3029 | + // Preprocess sql_variant: detect underlying type to route to correct conversion logic |
| 3030 | + SQLSMALLINT effectiveDataType = dataType; |
| 3031 | + if (dataType == SQL_SS_VARIANT) { |
| 3032 | + // For sql_variant, we MUST call SQLGetData with SQL_C_BINARY (NULL buffer, len=0) |
| 3033 | + // first. This serves two purposes: |
| 3034 | + // 1. Detects NULL values via the indicator parameter |
| 3035 | + // 2. Initializes the variant metadata in the ODBC driver, which is required for |
| 3036 | + // SQLColAttribute(SQL_CA_SS_VARIANT_TYPE) to return the correct underlying C type. |
| 3037 | + // Without this probe call, SQLColAttribute returns incorrect type codes. |
| 3038 | + SQLLEN indicator; |
| 3039 | + ret = SQLGetData_ptr(hStmt, i, SQL_C_BINARY, NULL, 0, &indicator); |
| 3040 | + if (!SQL_SUCCEEDED(ret)) { |
| 3041 | + LOG_ERROR("SQLGetData: Failed to probe sql_variant column %d - SQLRETURN=%d", i, |
| 3042 | + ret); |
| 3043 | + row.append(py::none()); |
| 3044 | + continue; |
| 3045 | + } |
| 3046 | + if (indicator == SQL_NULL_DATA) { |
| 3047 | + row.append(py::none()); |
| 3048 | + continue; |
| 3049 | + } |
| 3050 | + // Now retrieve the underlying C type |
| 3051 | + SQLLEN variantCType = 0; |
| 3052 | + ret = |
| 3053 | + SQLColAttribute_ptr(hStmt, i, SQL_CA_SS_VARIANT_TYPE, NULL, 0, NULL, &variantCType); |
| 3054 | + if (!SQL_SUCCEEDED(ret)) { |
| 3055 | + LOG_ERROR("SQLGetData: Failed to get sql_variant underlying type for column %d", i); |
| 3056 | + row.append(py::none()); |
| 3057 | + continue; |
| 3058 | + } |
| 3059 | + effectiveDataType = MapVariantCTypeToSQLType(variantCType); |
| 3060 | + LOG("SQLGetData: sql_variant column %d has variantCType=%ld, mapped to SQL type %d", i, |
| 3061 | + (long)variantCType, effectiveDataType); |
| 3062 | + } |
| 3063 | + |
| 3064 | + switch (effectiveDataType) { |
2949 | 3065 | case SQL_CHAR: |
2950 | 3066 | case SQL_VARCHAR: |
2951 | 3067 | case SQL_LONGVARCHAR: { |
@@ -4048,7 +4164,8 @@ size_t calculateRowSize(py::list& columnNames, SQLUSMALLINT numCols) { |
4048 | 4164 | break; |
4049 | 4165 | case SQL_SS_UDT: |
4050 | 4166 | rowSize += (static_cast<SQLLEN>(columnSize) == SQL_NO_TOTAL || columnSize == 0) |
4051 | | - ? SQL_MAX_LOB_SIZE : columnSize; |
| 4167 | + ? SQL_MAX_LOB_SIZE |
| 4168 | + : columnSize; |
4052 | 4169 | break; |
4053 | 4170 | case SQL_BINARY: |
4054 | 4171 | case SQL_VARBINARY: |
@@ -4110,11 +4227,7 @@ SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetch |
4110 | 4227 | SQLSMALLINT dataType = colMeta["DataType"].cast<SQLSMALLINT>(); |
4111 | 4228 | SQLULEN columnSize = colMeta["ColumnSize"].cast<SQLULEN>(); |
4112 | 4229 |
|
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)) { |
| 4230 | + if (IsLobOrVariantColumn(dataType, columnSize)) { |
4118 | 4231 | lobColumns.push_back(i + 1); // 1-based |
4119 | 4232 | } |
4120 | 4233 | } |
@@ -4204,6 +4317,40 @@ SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows, |
4204 | 4317 | return ret; |
4205 | 4318 | } |
4206 | 4319 |
|
| 4320 | + std::vector<SQLUSMALLINT> lobColumns; |
| 4321 | + for (SQLSMALLINT i = 0; i < numCols; i++) { |
| 4322 | + auto colMeta = columnNames[i].cast<py::dict>(); |
| 4323 | + SQLSMALLINT dataType = colMeta["DataType"].cast<SQLSMALLINT>(); |
| 4324 | + SQLULEN columnSize = colMeta["ColumnSize"].cast<SQLULEN>(); |
| 4325 | + |
| 4326 | + // Detect LOB columns that need SQLGetData streaming |
| 4327 | + // sql_variant always uses SQLGetData for native type preservation |
| 4328 | + if (IsLobOrVariantColumn(dataType, columnSize)) { |
| 4329 | + lobColumns.push_back(i + 1); // 1-based |
| 4330 | + } |
| 4331 | + } |
| 4332 | + |
| 4333 | + // If we have LOBs → fall back to row-by-row fetch + SQLGetData_wrap |
| 4334 | + if (!lobColumns.empty()) { |
| 4335 | + LOG("FetchAll_wrap: LOB columns detected (%zu columns), using per-row " |
| 4336 | + "SQLGetData path", |
| 4337 | + lobColumns.size()); |
| 4338 | + while (true) { |
| 4339 | + ret = SQLFetch_ptr(hStmt); |
| 4340 | + if (ret == SQL_NO_DATA) |
| 4341 | + break; |
| 4342 | + if (!SQL_SUCCEEDED(ret)) |
| 4343 | + return ret; |
| 4344 | + |
| 4345 | + py::list row; |
| 4346 | + SQLGetData_wrap(StatementHandle, numCols, row, charEncoding, |
| 4347 | + wcharEncoding); // <-- streams LOBs correctly |
| 4348 | + rows.append(row); |
| 4349 | + } |
| 4350 | + return SQL_SUCCESS; |
| 4351 | + } |
| 4352 | + |
| 4353 | + // No LOBs detected - use binding path with batch fetching |
4207 | 4354 | // Define a memory limit (1 GB) |
4208 | 4355 | const size_t memoryLimit = 1ULL * 1024 * 1024 * 1024; |
4209 | 4356 | size_t totalRowSize = calculateRowSize(columnNames, numCols); |
@@ -4244,41 +4391,6 @@ SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows, |
4244 | 4391 | } |
4245 | 4392 | LOG("FetchAll_wrap: Fetching data in batch sizes of %d", fetchSize); |
4246 | 4393 |
|
4247 | | - std::vector<SQLUSMALLINT> lobColumns; |
4248 | | - for (SQLSMALLINT i = 0; i < numCols; i++) { |
4249 | | - auto colMeta = columnNames[i].cast<py::dict>(); |
4250 | | - SQLSMALLINT dataType = colMeta["DataType"].cast<SQLSMALLINT>(); |
4251 | | - SQLULEN columnSize = colMeta["ColumnSize"].cast<SQLULEN>(); |
4252 | | - |
4253 | | - if ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR || dataType == SQL_VARCHAR || |
4254 | | - dataType == SQL_LONGVARCHAR || dataType == SQL_VARBINARY || |
4255 | | - dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML || |
4256 | | - dataType == SQL_SS_UDT) && |
4257 | | - (columnSize == 0 || columnSize == SQL_NO_TOTAL || columnSize > SQL_MAX_LOB_SIZE)) { |
4258 | | - lobColumns.push_back(i + 1); // 1-based |
4259 | | - } |
4260 | | - } |
4261 | | - |
4262 | | - // If we have LOBs → fall back to row-by-row fetch + SQLGetData_wrap |
4263 | | - if (!lobColumns.empty()) { |
4264 | | - LOG("FetchAll_wrap: LOB columns detected (%zu columns), using per-row " |
4265 | | - "SQLGetData path", |
4266 | | - lobColumns.size()); |
4267 | | - while (true) { |
4268 | | - ret = SQLFetch_ptr(hStmt); |
4269 | | - if (ret == SQL_NO_DATA) |
4270 | | - break; |
4271 | | - if (!SQL_SUCCEEDED(ret)) |
4272 | | - return ret; |
4273 | | - |
4274 | | - py::list row; |
4275 | | - SQLGetData_wrap(StatementHandle, numCols, row, charEncoding, |
4276 | | - wcharEncoding); // <-- streams LOBs correctly |
4277 | | - rows.append(row); |
4278 | | - } |
4279 | | - return SQL_SUCCESS; |
4280 | | - } |
4281 | | - |
4282 | 4394 | ColumnBuffers buffers(numCols, fetchSize); |
4283 | 4395 |
|
4284 | 4396 | // Bind columns |
|
0 commit comments