|
12 | 12 | #include <iostream> |
13 | 13 | #include <utility> // std::forward |
14 | 14 | #include <filesystem> |
15 | | - |
16 | 15 | //------------------------------------------------------------------------------------------------- |
17 | 16 | // Macro definitions |
18 | 17 | //------------------------------------------------------------------------------------------------- |
19 | 18 |
|
20 | 19 | // This constant is not exposed via sql.h, hence define it here |
21 | 20 | #define SQL_SS_TIME2 (-154) |
| 21 | +#define SQL_SS_TIMESTAMPOFFSET (-155) |
| 22 | +#define SQL_C_SS_TIMESTAMPOFFSET (16385) |
22 | 23 |
|
23 | 24 | #define MAX_DIGITS_IN_NUMERIC 64 |
24 | 25 |
|
@@ -94,6 +95,19 @@ struct ColumnBuffers { |
94 | 95 | indicators(numCols, std::vector<SQLLEN>(fetchSize)) {} |
95 | 96 | }; |
96 | 97 |
|
| 98 | +struct SQL_SS_TIMESTAMPOFFSET_STRUCT |
| 99 | +{ |
| 100 | + SQLSMALLINT year; |
| 101 | + SQLUSMALLINT month; |
| 102 | + SQLUSMALLINT day; |
| 103 | + SQLUSMALLINT hour; |
| 104 | + SQLUSMALLINT minute; |
| 105 | + SQLUSMALLINT second; |
| 106 | + SQLUINTEGER fraction; // Nanoseconds |
| 107 | + SQLSMALLINT timezone_hour; // Offset hours from UTC |
| 108 | + SQLSMALLINT timezone_minute; // Offset minutes from UTC |
| 109 | +}; |
| 110 | + |
97 | 111 | //------------------------------------------------------------------------------------------------- |
98 | 112 | // Function pointer initialization |
99 | 113 | //------------------------------------------------------------------------------------------------- |
@@ -460,18 +474,51 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, |
460 | 474 | if (!py::isinstance(param, datetimeType)) { |
461 | 475 | ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex)); |
462 | 476 | } |
463 | | - SQL_TIMESTAMP_STRUCT* sqlTimestampPtr = |
464 | | - AllocateParamBuffer<SQL_TIMESTAMP_STRUCT>(paramBuffers); |
465 | | - sqlTimestampPtr->year = static_cast<SQLSMALLINT>(param.attr("year").cast<int>()); |
466 | | - sqlTimestampPtr->month = static_cast<SQLUSMALLINT>(param.attr("month").cast<int>()); |
467 | | - sqlTimestampPtr->day = static_cast<SQLUSMALLINT>(param.attr("day").cast<int>()); |
468 | | - sqlTimestampPtr->hour = static_cast<SQLUSMALLINT>(param.attr("hour").cast<int>()); |
469 | | - sqlTimestampPtr->minute = static_cast<SQLUSMALLINT>(param.attr("minute").cast<int>()); |
470 | | - sqlTimestampPtr->second = static_cast<SQLUSMALLINT>(param.attr("second").cast<int>()); |
471 | | - // SQL server supports in ns, but python datetime supports in µs |
472 | | - sqlTimestampPtr->fraction = static_cast<SQLUINTEGER>( |
473 | | - param.attr("microsecond").cast<int>() * 1000); // Convert µs to ns |
474 | | - dataPtr = static_cast<void*>(sqlTimestampPtr); |
| 477 | + if (paramInfo.paramSQLType == SQL_TIMESTAMP) { |
| 478 | + // Handle naive datetime |
| 479 | + SQL_TIMESTAMP_STRUCT* tsPtr = AllocateParamBuffer<SQL_TIMESTAMP_STRUCT>(paramBuffers); |
| 480 | + tsPtr->year = static_cast<SQLSMALLINT>(param.attr("year").cast<int>()); |
| 481 | + tsPtr->month = static_cast<SQLUSMALLINT>(param.attr("month").cast<int>()); |
| 482 | + tsPtr->day = static_cast<SQLUSMALLINT>(param.attr("day").cast<int>()); |
| 483 | + tsPtr->hour = static_cast<SQLUSMALLINT>(param.attr("hour").cast<int>()); |
| 484 | + tsPtr->minute = static_cast<SQLUSMALLINT>(param.attr("minute").cast<int>()); |
| 485 | + tsPtr->second = static_cast<SQLUSMALLINT>(param.attr("second").cast<int>()); |
| 486 | + tsPtr->fraction = static_cast<SQLUINTEGER>(param.attr("microsecond").cast<int>() * 1000); |
| 487 | + dataPtr = static_cast<void*>(tsPtr); |
| 488 | + } |
| 489 | + else if (paramInfo.paramSQLType == SQL_SS_TIMESTAMPOFFSET) { |
| 490 | + // Handle tz-aware datetime → SQL_DATETIMEOFFSET |
| 491 | + SQL_SS_TIMESTAMPOFFSET_STRUCT* dtoPtr = AllocateParamBuffer<SQL_SS_TIMESTAMPOFFSET_STRUCT>(paramBuffers); |
| 492 | + int year = param.attr("year").cast<int>(); |
| 493 | + if (year < 1753 || year > 9999) { |
| 494 | + ThrowStdException("Date out of range for SQL Server (1753-9999) at paramIndex " + std::to_string(paramIndex)); |
| 495 | + } |
| 496 | + dtoPtr->year = static_cast<SQLSMALLINT>(year); |
| 497 | + dtoPtr->month = static_cast<SQLUSMALLINT>(param.attr("month").cast<int>()); |
| 498 | + dtoPtr->day = static_cast<SQLUSMALLINT>(param.attr("day").cast<int>()); |
| 499 | + dtoPtr->hour = static_cast<SQLUSMALLINT>(param.attr("hour").cast<int>()); |
| 500 | + dtoPtr->minute = static_cast<SQLUSMALLINT>(param.attr("minute").cast<int>()); |
| 501 | + dtoPtr->second = static_cast<SQLUSMALLINT>(param.attr("second").cast<int>()); |
| 502 | + dtoPtr->fraction = static_cast<SQLUINTEGER>(param.attr("microsecond").cast<int>() * 1000); |
| 503 | + |
| 504 | + py::object tzinfo = param.attr("tzinfo"); |
| 505 | + if (tzinfo.is_none()) { |
| 506 | + ThrowStdException("Datetime object must have tzinfo for DATETIMEOFFSET at paramIndex " + std::to_string(paramIndex)); |
| 507 | + } |
| 508 | + |
| 509 | + py::object utcoffset = tzinfo.attr("utcoffset")(param); |
| 510 | + if (utcoffset.is_none()) { |
| 511 | + ThrowStdException("utcoffset is None for DATETIMEOFFSET at paramIndex " + std::to_string(paramIndex)); |
| 512 | + } |
| 513 | + |
| 514 | + int total_seconds = static_cast<int>(utcoffset.attr("total_seconds")().cast<double>()); |
| 515 | + dtoPtr->timezone_hour = static_cast<SQLSMALLINT>(total_seconds / 3600); |
| 516 | + dtoPtr->timezone_minute = static_cast<SQLSMALLINT>((abs(total_seconds) % 3600) / 60); |
| 517 | + dataPtr = static_cast<void*>(dtoPtr); |
| 518 | + } |
| 519 | + else { |
| 520 | + ThrowStdException("Unsupported SQL type for timestamp at paramIndex " + std::to_string(paramIndex)); |
| 521 | + } |
475 | 522 | break; |
476 | 523 | } |
477 | 524 | case SQL_C_NUMERIC: { |
@@ -506,7 +553,6 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, |
506 | 553 | } |
507 | 554 | } |
508 | 555 | assert(SQLBindParameter_ptr && SQLGetStmtAttr_ptr && SQLSetDescField_ptr); |
509 | | - |
510 | 556 | RETCODE rc = SQLBindParameter_ptr( |
511 | 557 | hStmt, |
512 | 558 | static_cast<SQLUSMALLINT>(paramIndex + 1), /* 1-based indexing */ |
@@ -2180,6 +2226,55 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p |
2180 | 2226 | } |
2181 | 2227 | break; |
2182 | 2228 | } |
| 2229 | + case SQL_SS_TIMESTAMPOFFSET: { |
| 2230 | + SQL_SS_TIMESTAMPOFFSET_STRUCT dtoValue; |
| 2231 | + ret = SQLGetData_ptr( |
| 2232 | + hStmt, |
| 2233 | + i, |
| 2234 | + SQL_C_SS_TIMESTAMPOFFSET, |
| 2235 | + &dtoValue, |
| 2236 | + sizeof(dtoValue), |
| 2237 | + NULL |
| 2238 | + ); |
| 2239 | + if (SQL_SUCCEEDED(ret)) { |
| 2240 | + LOG("[Fetch] Retrieved DTO: {}-{}-{} {}:{}:{}, fraction(ns)={}, tz_hour={}, tz_minute={}", |
| 2241 | + dtoValue.year, dtoValue.month, dtoValue.day, |
| 2242 | + dtoValue.hour, dtoValue.minute, dtoValue.second, |
| 2243 | + dtoValue.fraction, |
| 2244 | + dtoValue.timezone_hour, dtoValue.timezone_minute |
| 2245 | + ); |
| 2246 | + |
| 2247 | + int totalMinutes = dtoValue.timezone_hour * 60 + dtoValue.timezone_minute; |
| 2248 | + // Validate offset |
| 2249 | + if (totalMinutes < -24 * 60 || totalMinutes > 24 * 60) { |
| 2250 | + std::ostringstream oss; |
| 2251 | + oss << "Invalid timezone offset from SQL_SS_TIMESTAMPOFFSET_STRUCT: " |
| 2252 | + << totalMinutes << " minutes for column " << i; |
| 2253 | + ThrowStdException(oss.str()); |
| 2254 | + } |
| 2255 | + // Convert fraction from ns to µs |
| 2256 | + int microseconds = dtoValue.fraction / 1000; |
| 2257 | + py::object datetime = py::module_::import("datetime"); |
| 2258 | + py::object tzinfo = datetime.attr("timezone")( |
| 2259 | + datetime.attr("timedelta")(py::arg("minutes") = totalMinutes) |
| 2260 | + ); |
| 2261 | + py::object py_dt = datetime.attr("datetime")( |
| 2262 | + dtoValue.year, |
| 2263 | + dtoValue.month, |
| 2264 | + dtoValue.day, |
| 2265 | + dtoValue.hour, |
| 2266 | + dtoValue.minute, |
| 2267 | + dtoValue.second, |
| 2268 | + microseconds, |
| 2269 | + tzinfo |
| 2270 | + ); |
| 2271 | + row.append(py_dt); |
| 2272 | + } else { |
| 2273 | + LOG("Error fetching DATETIMEOFFSET for column {}, ret={}", i, ret); |
| 2274 | + row.append(py::none()); |
| 2275 | + } |
| 2276 | + break; |
| 2277 | + } |
2183 | 2278 | case SQL_BINARY: |
2184 | 2279 | case SQL_VARBINARY: |
2185 | 2280 | case SQL_LONGVARBINARY: { |
|
0 commit comments