1919// This constant is not exposed via sql.h, hence define it here
2020#define SQL_SS_TIME2 (-154 )
2121#define SQL_SS_TIMESTAMPOFFSET (-155 )
22- #define SQL_C_SS_TIMESTAMPOFFSET (16385 )
23-
22+ #define SQL_C_SS_TIMESTAMPOFFSET (0x4001 )
2423#define MAX_DIGITS_IN_NUMERIC 64
2524
2625#define STRINGIFY_FOR_CASE (x ) \
@@ -95,7 +94,8 @@ struct ColumnBuffers {
9594 indicators(numCols, std::vector<SQLLEN>(fetchSize)) {}
9695};
9796
98- struct SQL_SS_TIMESTAMPOFFSET_STRUCT
97+ // Struct to hold the DateTimeOffset structure
98+ struct DateTimeOffset
9999{
100100 SQLSMALLINT year;
101101 SQLUSMALLINT month;
@@ -469,56 +469,56 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params,
469469 dataPtr = static_cast <void *>(sqlTimePtr);
470470 break ;
471471 }
472- case SQL_C_TYPE_TIMESTAMP : {
472+ case SQL_C_SS_TIMESTAMPOFFSET : {
473473 py::object datetimeType = py::module_::import (" datetime" ).attr (" datetime" );
474474 if (!py::isinstance (param, datetimeType)) {
475475 ThrowStdException (MakeParamMismatchErrorStr (paramInfo.paramCType , paramIndex));
476476 }
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));
477+ // Checking if the object has a timezone
478+ py::object tzinfo = param.attr (" tzinfo" );
479+ if (tzinfo.is_none ()) {
480+ ThrowStdException (" Datetime object must have tzinfo for SQL_C_SS_TIMESTAMPOFFSET at paramIndex " + std::to_string (paramIndex));
481+ }
482+
483+ DateTimeOffset* dtoPtr = AllocateParamBuffer<DateTimeOffset>(paramBuffers);
484+
485+ dtoPtr->year = static_cast <SQLSMALLINT>(param.attr (" year" ).cast <int >());
486+ dtoPtr->month = static_cast <SQLUSMALLINT>(param.attr (" month" ).cast <int >());
487+ dtoPtr->day = static_cast <SQLUSMALLINT>(param.attr (" day" ).cast <int >());
488+ dtoPtr->hour = static_cast <SQLUSMALLINT>(param.attr (" hour" ).cast <int >());
489+ dtoPtr->minute = static_cast <SQLUSMALLINT>(param.attr (" minute" ).cast <int >());
490+ dtoPtr->second = static_cast <SQLUSMALLINT>(param.attr (" second" ).cast <int >());
491+ dtoPtr->fraction = static_cast <SQLUINTEGER>(param.attr (" microsecond" ).cast <int >() * 1000 );
492+
493+ py::object utcoffset = tzinfo.attr (" utcoffset" )(param);
494+ int total_seconds = static_cast <int >(utcoffset.attr (" total_seconds" )().cast <double >());
495+ std::div_t div_result = std::div (total_seconds, 3600 );
496+ dtoPtr->timezone_hour = static_cast <SQLSMALLINT>(div_result.quot );
497+ dtoPtr->timezone_minute = static_cast <SQLSMALLINT>(div (div_result.rem , 60 ).quot );
498+
499+ dataPtr = static_cast <void *>(dtoPtr);
500+ bufferLength = sizeof (DateTimeOffset);
501+ strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);
502+ *strLenOrIndPtr = bufferLength;
503+ break ;
504+ }
505+ case SQL_C_TYPE_TIMESTAMP: {
506+ py::object datetimeType = py::module_::import (" datetime" ).attr (" datetime" );
507+ if (!py::isinstance (param, datetimeType)) {
508+ ThrowStdException (MakeParamMismatchErrorStr (paramInfo.paramCType , paramIndex));
521509 }
510+ SQL_TIMESTAMP_STRUCT* sqlTimestampPtr =
511+ AllocateParamBuffer<SQL_TIMESTAMP_STRUCT>(paramBuffers);
512+ sqlTimestampPtr->year = static_cast <SQLSMALLINT>(param.attr (" year" ).cast <int >());
513+ sqlTimestampPtr->month = static_cast <SQLUSMALLINT>(param.attr (" month" ).cast <int >());
514+ sqlTimestampPtr->day = static_cast <SQLUSMALLINT>(param.attr (" day" ).cast <int >());
515+ sqlTimestampPtr->hour = static_cast <SQLUSMALLINT>(param.attr (" hour" ).cast <int >());
516+ sqlTimestampPtr->minute = static_cast <SQLUSMALLINT>(param.attr (" minute" ).cast <int >());
517+ sqlTimestampPtr->second = static_cast <SQLUSMALLINT>(param.attr (" second" ).cast <int >());
518+ // SQL server supports in ns, but python datetime supports in µs
519+ sqlTimestampPtr->fraction = static_cast <SQLUINTEGER>(
520+ param.attr (" microsecond" ).cast <int >() * 1000 ); // Convert µs to ns
521+ dataPtr = static_cast <void *>(sqlTimestampPtr);
522522 break ;
523523 }
524524 case SQL_C_NUMERIC: {
@@ -2227,16 +2227,16 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
22272227 break ;
22282228 }
22292229 case SQL_SS_TIMESTAMPOFFSET: {
2230- SQL_SS_TIMESTAMPOFFSET_STRUCT dtoValue;
2230+ DateTimeOffset dtoValue;
2231+ SQLLEN indicator;
22312232 ret = SQLGetData_ptr (
2232- hStmt,
2233- i,
2234- SQL_C_SS_TIMESTAMPOFFSET,
2235- &dtoValue,
2236- sizeof (dtoValue),
2237- NULL
2233+ hStmt,
2234+ i, SQL_C_SS_TIMESTAMPOFFSET,
2235+ &dtoValue,
2236+ sizeof (dtoValue),
2237+ &indicator
22382238 );
2239- if (SQL_SUCCEEDED (ret)) {
2239+ if (SQL_SUCCEEDED (ret) && indicator != SQL_NULL_DATA ) {
22402240 LOG (" [Fetch] Retrieved DTO: {}-{}-{} {}:{}:{}, fraction(ns)={}, tz_hour={}, tz_minute={}" ,
22412241 dtoValue.year , dtoValue.month , dtoValue.day ,
22422242 dtoValue.hour , dtoValue.minute , dtoValue.second ,
@@ -2245,7 +2245,7 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
22452245 );
22462246
22472247 int totalMinutes = dtoValue.timezone_hour * 60 + dtoValue.timezone_minute ;
2248- // Validate offset
2248+ // Validating offset
22492249 if (totalMinutes < -24 * 60 || totalMinutes > 24 * 60 ) {
22502250 std::ostringstream oss;
22512251 oss << " Invalid timezone offset from SQL_SS_TIMESTAMPOFFSET_STRUCT: "
0 commit comments