|
27 | 27 | #define MAX_DIGITS_IN_NUMERIC 64 |
28 | 28 | #define SQL_MAX_NUMERIC_LEN 16 |
29 | 29 | #define SQL_SS_XML (-152) |
| 30 | +#define SQL_SS_VARIANT (-150) |
| 31 | +#define SQL_CA_SS_BASE 1200 |
| 32 | +#define SQL_CA_SS_VARIANT_TYPE (SQL_CA_SS_BASE + 15) |
| 33 | +#define SQL_CA_SS_VARIANT_SQL_TYPE (SQL_CA_SS_BASE + 16) |
| 34 | +#define SQL_CA_SS_VARIANT_SERVER_TYPE (SQL_CA_SS_BASE + 17) |
30 | 35 |
|
31 | 36 | #define STRINGIFY_FOR_CASE(x) \ |
32 | 37 | case x: \ |
@@ -1153,7 +1158,8 @@ void SqlHandle::markImplicitlyFreed() { |
1153 | 1158 | // Log error but don't throw - we're likely in cleanup/destructor path |
1154 | 1159 | LOG_ERROR("SAFETY VIOLATION: Attempted to mark non-STMT handle as implicitly freed. " |
1155 | 1160 | "Handle type=%d. This will cause handle leak. Only STMT handles are " |
1156 | | - "automatically freed by parent DBC handles.", _type); |
| 1161 | + "automatically freed by parent DBC handles.", |
| 1162 | + _type); |
1157 | 1163 | return; // Refuse to mark - let normal free() handle it |
1158 | 1164 | } |
1159 | 1165 | _implicitly_freed = true; |
@@ -2891,6 +2897,55 @@ py::object FetchLobColumnData(SQLHSTMT hStmt, SQLUSMALLINT colIndex, SQLSMALLINT |
2891 | 2897 | } |
2892 | 2898 | } |
2893 | 2899 |
|
| 2900 | +// Helper function to map sql_variant's underlying C type to SQL data type |
| 2901 | +// This allows sql_variant to reuse existing fetch logic for each data type |
| 2902 | +SQLSMALLINT MapVariantCTypeToSQLType(SQLLEN variantCType) { |
| 2903 | + switch (variantCType) { |
| 2904 | + case SQL_C_SLONG: |
| 2905 | + case SQL_C_LONG: |
| 2906 | + return SQL_INTEGER; |
| 2907 | + case SQL_C_SSHORT: |
| 2908 | + case SQL_C_SHORT: |
| 2909 | + return SQL_SMALLINT; |
| 2910 | + case SQL_C_SBIGINT: |
| 2911 | + return SQL_BIGINT; |
| 2912 | + case SQL_C_FLOAT: |
| 2913 | + return SQL_REAL; |
| 2914 | + case SQL_C_DOUBLE: |
| 2915 | + return SQL_DOUBLE; |
| 2916 | + case SQL_C_BIT: |
| 2917 | + return SQL_BIT; |
| 2918 | + case SQL_C_CHAR: |
| 2919 | + return SQL_VARCHAR; |
| 2920 | + case SQL_C_WCHAR: |
| 2921 | + return SQL_WVARCHAR; |
| 2922 | + // Date/time types - handle both old-style (9, 10, 11) and new-style (91, 92, 93) codes |
| 2923 | + case 9: // SQL_C_DATE (old style) |
| 2924 | + case SQL_C_TYPE_DATE: // 91 (new style) |
| 2925 | + return SQL_TYPE_DATE; |
| 2926 | + case 10: // SQL_C_TIME (old style) |
| 2927 | + case SQL_C_TYPE_TIME: // 92 (new style) |
| 2928 | + case 16384: // SQL Server variant TIME type (observed value) |
| 2929 | + return SQL_TYPE_TIME; |
| 2930 | + case 11: // SQL_C_TIMESTAMP (old style) |
| 2931 | + case SQL_C_TYPE_TIMESTAMP: // 93 (new style) |
| 2932 | + return SQL_TYPE_TIMESTAMP; |
| 2933 | + case SQL_C_BINARY: |
| 2934 | + return SQL_VARBINARY; |
| 2935 | + case SQL_C_GUID: |
| 2936 | + return SQL_GUID; |
| 2937 | + case SQL_C_NUMERIC: |
| 2938 | + return SQL_NUMERIC; |
| 2939 | + case SQL_C_TINYINT: |
| 2940 | + case SQL_C_UTINYINT: |
| 2941 | + case SQL_C_STINYINT: |
| 2942 | + return SQL_TINYINT; |
| 2943 | + default: |
| 2944 | + // Unknown type, fallback to WVARCHAR for string conversion |
| 2945 | + return SQL_WVARCHAR; |
| 2946 | + } |
| 2947 | +} |
| 2948 | + |
2894 | 2949 | // Helper function to retrieve column data |
2895 | 2950 | SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, py::list& row, |
2896 | 2951 | const std::string& charEncoding = "utf-8", |
@@ -2929,7 +2984,40 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p |
2929 | 2984 | continue; |
2930 | 2985 | } |
2931 | 2986 |
|
2932 | | - switch (dataType) { |
| 2987 | + printf("[DEBUG] SQLGetData_wrap: Column %d - dataType=%d, columnSize=%lu\n", i, dataType, |
| 2988 | + (unsigned long)columnSize); |
| 2989 | + |
| 2990 | + // Preprocess sql_variant: detect underlying type and handle NULL |
| 2991 | + // This allows reuse of existing fetch logic instead of duplicating code |
| 2992 | + SQLSMALLINT effectiveDataType = dataType; |
| 2993 | + if (dataType == SQL_SS_VARIANT) { |
| 2994 | + // Step 1: Check for NULL using header read |
| 2995 | + SQLLEN indicator; |
| 2996 | + ret = SQLGetData_ptr(hStmt, i, SQL_C_BINARY, NULL, 0, &indicator); |
| 2997 | + if (indicator == SQL_NULL_DATA) { |
| 2998 | + row.append(py::none()); |
| 2999 | + continue; // Skip to next column |
| 3000 | + } |
| 3001 | + |
| 3002 | + // Step 2: Get the variant's underlying C data type |
| 3003 | + SQLLEN variantCType = 0; |
| 3004 | + ret = |
| 3005 | + SQLColAttribute_ptr(hStmt, i, SQL_CA_SS_VARIANT_TYPE, NULL, 0, NULL, &variantCType); |
| 3006 | + if (!SQL_SUCCEEDED(ret)) { |
| 3007 | + LOG("SQLGetData: Failed to get sql_variant underlying type for column %d", i); |
| 3008 | + row.append(py::none()); |
| 3009 | + continue; // Skip to next column |
| 3010 | + } |
| 3011 | + |
| 3012 | + printf("[DEBUG] SQLGetData_wrap: sql_variant column %d has variantCType=%ld\n", i, |
| 3013 | + (long)variantCType); |
| 3014 | + |
| 3015 | + // Step 3: Map C type to SQL type so existing code can handle it |
| 3016 | + effectiveDataType = MapVariantCTypeToSQLType(variantCType); |
| 3017 | + printf("[DEBUG] SQLGetData_wrap: Mapped to effectiveDataType=%d\n", effectiveDataType); |
| 3018 | + } |
| 3019 | + |
| 3020 | + switch (effectiveDataType) { |
2933 | 3021 | case SQL_CHAR: |
2934 | 3022 | case SQL_VARCHAR: |
2935 | 3023 | case SQL_LONGVARCHAR: { |
@@ -4041,10 +4129,19 @@ SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetch |
4041 | 4129 | SQLSMALLINT dataType = colMeta["DataType"].cast<SQLSMALLINT>(); |
4042 | 4130 | SQLULEN columnSize = colMeta["ColumnSize"].cast<SQLULEN>(); |
4043 | 4131 |
|
4044 | | - if ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR || dataType == SQL_VARCHAR || |
4045 | | - dataType == SQL_LONGVARCHAR || dataType == SQL_VARBINARY || |
4046 | | - dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML) && |
4047 | | - (columnSize == 0 || columnSize == SQL_NO_TOTAL || columnSize > SQL_MAX_LOB_SIZE)) { |
| 4132 | + printf("[DEBUG] FetchMany_wrap: Column %d - dataType=%d, columnSize=%lu\n", i + 1, dataType, |
| 4133 | + (unsigned long)columnSize); |
| 4134 | + |
| 4135 | + // Detect LOB columns that need SQLGetData streaming |
| 4136 | + // sql_variant always uses SQLGetData for native type preservation |
| 4137 | + if (dataType == SQL_SS_VARIANT) { |
| 4138 | + lobColumns.push_back(i + 1); // 1-based |
| 4139 | + } else if ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR || |
| 4140 | + dataType == SQL_VARCHAR || dataType == SQL_LONGVARCHAR || |
| 4141 | + dataType == SQL_VARBINARY || dataType == SQL_LONGVARBINARY || |
| 4142 | + dataType == SQL_SS_XML) && |
| 4143 | + (columnSize == 0 || columnSize == SQL_NO_TOTAL || |
| 4144 | + columnSize > SQL_MAX_LOB_SIZE)) { |
4048 | 4145 | lobColumns.push_back(i + 1); // 1-based |
4049 | 4146 | } |
4050 | 4147 | } |
@@ -4129,6 +4226,52 @@ SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows, |
4129 | 4226 | return ret; |
4130 | 4227 | } |
4131 | 4228 |
|
| 4229 | + // Detect LOB columns FIRST (before calculateRowSize) |
| 4230 | + // This allows sql_variant to skip the binding path entirely |
| 4231 | + std::vector<SQLUSMALLINT> lobColumns; |
| 4232 | + for (SQLSMALLINT i = 0; i < numCols; i++) { |
| 4233 | + auto colMeta = columnNames[i].cast<py::dict>(); |
| 4234 | + SQLSMALLINT dataType = colMeta["DataType"].cast<SQLSMALLINT>(); |
| 4235 | + SQLULEN columnSize = colMeta["ColumnSize"].cast<SQLULEN>(); |
| 4236 | + |
| 4237 | + printf("[DEBUG] FetchAll_wrap: Column %d - dataType=%d, columnSize=%lu\n", i + 1, dataType, |
| 4238 | + (unsigned long)columnSize); |
| 4239 | + |
| 4240 | + // Detect LOB columns that need SQLGetData streaming |
| 4241 | + // sql_variant always uses SQLGetData for native type preservation |
| 4242 | + if (dataType == SQL_SS_VARIANT) { |
| 4243 | + lobColumns.push_back(i + 1); // 1-based |
| 4244 | + } else if ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR || |
| 4245 | + dataType == SQL_VARCHAR || dataType == SQL_LONGVARCHAR || |
| 4246 | + dataType == SQL_VARBINARY || dataType == SQL_LONGVARBINARY || |
| 4247 | + dataType == SQL_SS_XML) && |
| 4248 | + (columnSize == 0 || columnSize == SQL_NO_TOTAL || |
| 4249 | + columnSize > SQL_MAX_LOB_SIZE)) { |
| 4250 | + lobColumns.push_back(i + 1); // 1-based |
| 4251 | + } |
| 4252 | + } |
| 4253 | + |
| 4254 | + // If we have LOBs → fall back to row-by-row fetch + SQLGetData_wrap |
| 4255 | + if (!lobColumns.empty()) { |
| 4256 | + LOG("FetchAll_wrap: LOB columns detected (%zu columns), using per-row " |
| 4257 | + "SQLGetData path", |
| 4258 | + lobColumns.size()); |
| 4259 | + while (true) { |
| 4260 | + ret = SQLFetch_ptr(hStmt); |
| 4261 | + if (ret == SQL_NO_DATA) |
| 4262 | + break; |
| 4263 | + if (!SQL_SUCCEEDED(ret)) |
| 4264 | + return ret; |
| 4265 | + |
| 4266 | + py::list row; |
| 4267 | + SQLGetData_wrap(StatementHandle, numCols, row, charEncoding, |
| 4268 | + wcharEncoding); // <-- streams LOBs correctly |
| 4269 | + rows.append(row); |
| 4270 | + } |
| 4271 | + return SQL_SUCCESS; |
| 4272 | + } |
| 4273 | + |
| 4274 | + // No LOBs detected - use binding path with batch fetching |
4132 | 4275 | // Define a memory limit (1 GB) |
4133 | 4276 | const size_t memoryLimit = 1ULL * 1024 * 1024 * 1024; |
4134 | 4277 | size_t totalRowSize = calculateRowSize(columnNames, numCols); |
@@ -4169,40 +4312,6 @@ SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows, |
4169 | 4312 | } |
4170 | 4313 | LOG("FetchAll_wrap: Fetching data in batch sizes of %d", fetchSize); |
4171 | 4314 |
|
4172 | | - std::vector<SQLUSMALLINT> lobColumns; |
4173 | | - for (SQLSMALLINT i = 0; i < numCols; i++) { |
4174 | | - auto colMeta = columnNames[i].cast<py::dict>(); |
4175 | | - SQLSMALLINT dataType = colMeta["DataType"].cast<SQLSMALLINT>(); |
4176 | | - SQLULEN columnSize = colMeta["ColumnSize"].cast<SQLULEN>(); |
4177 | | - |
4178 | | - if ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR || dataType == SQL_VARCHAR || |
4179 | | - dataType == SQL_LONGVARCHAR || dataType == SQL_VARBINARY || |
4180 | | - dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML) && |
4181 | | - (columnSize == 0 || columnSize == SQL_NO_TOTAL || columnSize > SQL_MAX_LOB_SIZE)) { |
4182 | | - lobColumns.push_back(i + 1); // 1-based |
4183 | | - } |
4184 | | - } |
4185 | | - |
4186 | | - // If we have LOBs → fall back to row-by-row fetch + SQLGetData_wrap |
4187 | | - if (!lobColumns.empty()) { |
4188 | | - LOG("FetchAll_wrap: LOB columns detected (%zu columns), using per-row " |
4189 | | - "SQLGetData path", |
4190 | | - lobColumns.size()); |
4191 | | - while (true) { |
4192 | | - ret = SQLFetch_ptr(hStmt); |
4193 | | - if (ret == SQL_NO_DATA) |
4194 | | - break; |
4195 | | - if (!SQL_SUCCEEDED(ret)) |
4196 | | - return ret; |
4197 | | - |
4198 | | - py::list row; |
4199 | | - SQLGetData_wrap(StatementHandle, numCols, row, charEncoding, |
4200 | | - wcharEncoding); // <-- streams LOBs correctly |
4201 | | - rows.append(row); |
4202 | | - } |
4203 | | - return SQL_SUCCESS; |
4204 | | - } |
4205 | | - |
4206 | 4315 | ColumnBuffers buffers(numCols, fetchSize); |
4207 | 4316 |
|
4208 | 4317 | // Bind columns |
|
0 commit comments