|
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", |
@@ -2949,7 +3030,42 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p |
2949 | 3030 | continue; |
2950 | 3031 | } |
2951 | 3032 |
|
2952 | | - switch (dataType) { |
| 3033 | + // Preprocess sql_variant: detect underlying type to route to correct conversion logic |
| 3034 | + SQLSMALLINT effectiveDataType = dataType; |
| 3035 | + if (dataType == SQL_SS_VARIANT) { |
| 3036 | + // For sql_variant, we MUST call SQLGetData with SQL_C_BINARY (NULL buffer, len=0) |
| 3037 | + // first. This serves two purposes: |
| 3038 | + // 1. Detects NULL values via the indicator parameter |
| 3039 | + // 2. Initializes the variant metadata in the ODBC driver, which is required for |
| 3040 | + // SQLColAttribute(SQL_CA_SS_VARIANT_TYPE) to return the correct underlying C type. |
| 3041 | + // Without this probe call, SQLColAttribute returns incorrect type codes. |
| 3042 | + SQLLEN indicator; |
| 3043 | + ret = SQLGetData_ptr(hStmt, i, SQL_C_BINARY, NULL, 0, &indicator); |
| 3044 | + if (!SQL_SUCCEEDED(ret)) { |
| 3045 | + LOG_ERROR("SQLGetData: Failed to probe sql_variant column %d - SQLRETURN=%d", i, |
| 3046 | + ret); |
| 3047 | + row.append(py::none()); |
| 3048 | + continue; |
| 3049 | + } |
| 3050 | + if (indicator == SQL_NULL_DATA) { |
| 3051 | + row.append(py::none()); |
| 3052 | + continue; |
| 3053 | + } |
| 3054 | + // Now retrieve the underlying C type |
| 3055 | + SQLLEN variantCType = 0; |
| 3056 | + ret = |
| 3057 | + SQLColAttribute_ptr(hStmt, i, SQL_CA_SS_VARIANT_TYPE, NULL, 0, NULL, &variantCType); |
| 3058 | + if (!SQL_SUCCEEDED(ret)) { |
| 3059 | + LOG_ERROR("SQLGetData: Failed to get sql_variant underlying type for column %d", i); |
| 3060 | + row.append(py::none()); |
| 3061 | + continue; |
| 3062 | + } |
| 3063 | + effectiveDataType = MapVariantCTypeToSQLType(variantCType); |
| 3064 | + LOG("SQLGetData: sql_variant column %d has variantCType=%ld, mapped to SQL type %d", i, |
| 3065 | + (long)variantCType, effectiveDataType); |
| 3066 | + } |
| 3067 | + |
| 3068 | + switch (effectiveDataType) { |
2953 | 3069 | case SQL_CHAR: |
2954 | 3070 | case SQL_VARCHAR: |
2955 | 3071 | case SQL_LONGVARCHAR: { |
@@ -4169,6 +4285,7 @@ SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetch |
4169 | 4285 | dataType == SQL_LONGVARCHAR || dataType == SQL_VARBINARY || |
4170 | 4286 | dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML || dataType == SQL_SS_UDT) && |
4171 | 4287 | (columnSize == 0 || columnSize == SQL_NO_TOTAL || columnSize > SQL_MAX_LOB_SIZE)) { |
| 4288 | + if (IsLobOrVariantColumn(dataType, columnSize)) { |
4172 | 4289 | lobColumns.push_back(i + 1); // 1-based |
4173 | 4290 | } |
4174 | 4291 | } |
@@ -4258,6 +4375,40 @@ SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows, |
4258 | 4375 | return ret; |
4259 | 4376 | } |
4260 | 4377 |
|
| 4378 | + std::vector<SQLUSMALLINT> lobColumns; |
| 4379 | + for (SQLSMALLINT i = 0; i < numCols; i++) { |
| 4380 | + auto colMeta = columnNames[i].cast<py::dict>(); |
| 4381 | + SQLSMALLINT dataType = colMeta["DataType"].cast<SQLSMALLINT>(); |
| 4382 | + SQLULEN columnSize = colMeta["ColumnSize"].cast<SQLULEN>(); |
| 4383 | + |
| 4384 | + // Detect LOB columns that need SQLGetData streaming |
| 4385 | + // sql_variant always uses SQLGetData for native type preservation |
| 4386 | + if (IsLobOrVariantColumn(dataType, columnSize)) { |
| 4387 | + lobColumns.push_back(i + 1); // 1-based |
| 4388 | + } |
| 4389 | + } |
| 4390 | + |
| 4391 | + // If we have LOBs → fall back to row-by-row fetch + SQLGetData_wrap |
| 4392 | + if (!lobColumns.empty()) { |
| 4393 | + LOG("FetchAll_wrap: LOB columns detected (%zu columns), using per-row " |
| 4394 | + "SQLGetData path", |
| 4395 | + lobColumns.size()); |
| 4396 | + while (true) { |
| 4397 | + ret = SQLFetch_ptr(hStmt); |
| 4398 | + if (ret == SQL_NO_DATA) |
| 4399 | + break; |
| 4400 | + if (!SQL_SUCCEEDED(ret)) |
| 4401 | + return ret; |
| 4402 | + |
| 4403 | + py::list row; |
| 4404 | + SQLGetData_wrap(StatementHandle, numCols, row, charEncoding, |
| 4405 | + wcharEncoding); // <-- streams LOBs correctly |
| 4406 | + rows.append(row); |
| 4407 | + } |
| 4408 | + return SQL_SUCCESS; |
| 4409 | + } |
| 4410 | + |
| 4411 | + // No LOBs detected - use binding path with batch fetching |
4261 | 4412 | // Define a memory limit (1 GB) |
4262 | 4413 | const size_t memoryLimit = 1ULL * 1024 * 1024 * 1024; |
4263 | 4414 | size_t totalRowSize = calculateRowSize(columnNames, numCols); |
|
0 commit comments