Skip to content

Commit 549e5d2

Browse files
committed
tests added
1 parent bbcd42b commit 549e5d2

File tree

5 files changed

+230
-110
lines changed

5 files changed

+230
-110
lines changed

main.py

Lines changed: 8 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,20 @@
22
from mssql_python import setup_logging
33
import os
44
import decimal
5-
# import pyodbc
65

76
setup_logging('stdout')
87

9-
# conn_str = os.getenv("DB_CONNECTION_STRING")
10-
conn_str = "DRIVER={ODBC Driver 18 for SQL Server};Server=Saumya;DATABASE=master;UID=sa;PWD=HappyPass1234;Trust_Connection=yes;TrustServerCertificate=yes;"
11-
8+
conn_str = os.getenv("DB_CONNECTION_STRING")
129
conn = connect(conn_str)
1310

14-
# # conn.autocommit = True
15-
16-
# cursor = conn.cursor()
17-
# cursor.execute("SELECT database_id, name from sys.databases;")
18-
# rows = cursor.fetchall()
19-
20-
# for row in rows:
21-
# print(f"Database ID: {row[0]}, Name: {row[1]}")
22-
23-
# cursor.close()
24-
# conn.close()
25-
26-
from datetime import datetime, timezone, timedelta
11+
# conn.autocommit = True
2712

28-
# Connect and get cursor
2913
cursor = conn.cursor()
14+
cursor.execute("SELECT database_id, name from sys.databases;")
15+
rows = cursor.fetchall()
3016

31-
# Create table (drop if exists)
32-
cursor.execute("""
33-
IF OBJECT_ID('dbo.test_datetimeoffset', 'U') IS NOT NULL
34-
DROP TABLE dbo.test_datetimeoffset;
35-
36-
CREATE TABLE dbo.test_datetimeoffset (
37-
id INT PRIMARY KEY,
38-
dt DATETIMEOFFSET
39-
)
40-
""")
41-
42-
# Insert a row
43-
dt_offset = datetime(2025, 9, 9, 15, 30, 45, 123456, tzinfo=timezone(timedelta(hours=5, minutes=30)))
44-
cursor.execute("INSERT INTO test_datetimeoffset (id, dt) VALUES (?, ?)", 1, dt_offset)
45-
conn.commit()
46-
print("Insertion done. Verify in SSMS.")
47-
48-
# --- Fetch the row ---
49-
cursor.execute("SELECT id, dt FROM dbo.test_datetimeoffset WHERE id = ?", 1)
50-
row = cursor.fetchone()
51-
52-
if row:
53-
fetched_id, fetched_dt = row
54-
print(f"Fetched ID: {fetched_id}")
55-
print(f"Fetched DATETIMEOFFSET: {fetched_dt} (tzinfo: {fetched_dt.tzinfo})")
17+
for row in rows:
18+
print(f"Database ID: {row[0]}, Name: {row[1]}")
5619

57-
# Optional check
58-
if fetched_dt == dt_offset:
59-
print("✅ Fetch successful: Datetime matches inserted value")
60-
else:
61-
print("⚠️ Fetch mismatch: Datetime does not match inserted value")
62-
else:
63-
print("No row fetched.")
20+
cursor.close()
21+
conn.close()

mssql_python/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ class ConstantsDDBC(Enum):
125125
SQL_FETCH_RELATIVE = 6
126126
SQL_FETCH_BOOKMARK = 8
127127
SQL_DATETIMEOFFSET = -155
128+
SQL_C_SS_TIMESTAMPOFFSET = 0x4001
128129

129130
class AuthType(Enum):
130131
"""Constants for authentication types"""

mssql_python/cursor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ def _map_sql_type(self, param, parameters_list, i, min_val=None, max_val=None):
435435
# Timezone-aware datetime -> DATETIMEOFFSET
436436
return (
437437
ddbc_sql_const.SQL_DATETIMEOFFSET.value,
438-
ddbc_sql_const.SQL_C_TYPE_TIMESTAMP.value,
438+
ddbc_sql_const.SQL_C_SS_TIMESTAMPOFFSET.value,
439439
34,
440440
7,
441441
False,

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 57 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
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

Comments
 (0)