Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions mssql_python/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@
MONEY_MAX: decimal.Decimal = decimal.Decimal("922337203685477.5807")


def _normalize_time_param(value, c_type):
"""Convert a datetime.time to its isoformat string when bound via text C-types.

Returns the isoformat string if conversion applies, otherwise *None*.
"""
if isinstance(value, datetime.time) and c_type in (
ddbc_sql_const.SQL_C_CHAR.value,
ddbc_sql_const.SQL_C_WCHAR.value,
):
return value.isoformat(timespec="microseconds")
return None


class Cursor: # pylint: disable=too-many-instance-attributes,too-many-public-methods
"""
Represents a database cursor, which is used to manage the context of a fetch operation.
Expand Down Expand Up @@ -676,10 +689,10 @@ def _map_sql_type( # pylint: disable=too-many-arguments,too-many-positional-arg

if isinstance(param, datetime.time):
return (
ddbc_sql_const.SQL_TIME.value,
ddbc_sql_const.SQL_C_TYPE_TIME.value,
8,
0,
ddbc_sql_const.SQL_TYPE_TIME.value,
ddbc_sql_const.SQL_C_CHAR.value,
16,
6,
False,
)

Expand Down Expand Up @@ -958,6 +971,13 @@ def _create_parameter_types_list( # pylint: disable=too-many-arguments,too-many
parameter, parameters_list, i, min_val=min_val, max_val=max_val
)

# If TIME values are being bound via text C-types, normalize them to a
# textual representation expected by SQL_C_CHAR/SQL_C_WCHAR binding.
time_text = _normalize_time_param(parameter, c_type)
if time_text is not None:
parameters_list[i] = time_text
column_size = max(column_size, len(time_text))

paraminfo.paramCType = c_type
paraminfo.paramSQLType = sql_type
paraminfo.inputOutputType = ddbc_sql_const.SQL_PARAM_INPUT.value
Expand Down Expand Up @@ -2277,6 +2297,10 @@ def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-s
for i, val in enumerate(processed_row):
if val is None:
continue
time_text = _normalize_time_param(val, parameters_type[i].paramCType)
if time_text is not None:
processed_row[i] = time_text
continue
if (
isinstance(val, decimal.Decimal)
and parameters_type[i].paramSQLType == ddbc_sql_const.SQL_VARCHAR.value
Expand Down
51 changes: 27 additions & 24 deletions mssql_python/pybind/ddbc_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "connection/connection_pool.h"
#include "logger_bridge.hpp"

#include <cctype>
#include <cstdint>
#include <cstring> // For std::memcpy
#include <filesystem>
Expand All @@ -23,6 +24,7 @@
// These constants are not exposed via sql.h, hence define them here
#define SQL_SS_TIME2 (-154)
#define SQL_SS_TIMESTAMPOFFSET (-155)
#define SQL_C_SS_TIME2 (0x4000)
#define SQL_C_SS_TIMESTAMPOFFSET (0x4001)
#define MAX_DIGITS_IN_NUMERIC 64
#define SQL_MAX_NUMERIC_LEN 16
Expand Down Expand Up @@ -66,6 +68,10 @@ inline std::string GetEffectiveCharDecoding(const std::string& userEncoding) {
#endif
}

namespace PythonObjectCache {
py::object get_time_class();
}

//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
// Logging Infrastructure:
Expand Down Expand Up @@ -3076,7 +3082,7 @@ SQLSMALLINT MapVariantCTypeToSQLType(SQLLEN variantCType) {
case SQL_C_TIME:
case SQL_C_TYPE_TIME:
case SQL_SS_VARIANT_TIME:
return SQL_TYPE_TIME;
return SQL_SS_TIME2;
case SQL_C_TIMESTAMP:
case SQL_C_TYPE_TIMESTAMP:
return SQL_TYPE_TIMESTAMP;
Expand Down Expand Up @@ -3481,19 +3487,20 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
}
break;
}
case SQL_TIME:
case SQL_TYPE_TIME:
case SQL_SS_TIME2: {
SQL_TIME_STRUCT timeValue;
ret =
SQLGetData_ptr(hStmt, i, SQL_C_TYPE_TIME, &timeValue, sizeof(timeValue), NULL);
if (SQL_SUCCEEDED(ret)) {
row.append(PythonObjectCache::get_time_class()(timeValue.hour, timeValue.minute,
timeValue.second));
SQL_SS_TIME2_STRUCT t2 = {};
SQLLEN indicator = 0;
ret = SQLGetData_ptr(hStmt, i, SQL_C_SS_TIME2, &t2, sizeof(t2), &indicator);
if (SQL_SUCCEEDED(ret) && indicator != SQL_NULL_DATA) {
row.append(PythonObjectCache::get_time_class()(
t2.hour, t2.minute, t2.second, t2.fraction / 1000)); // ns to µs
} else {
LOG("SQLGetData: Error retrieving SQL_TYPE_TIME for column "
"%d - SQLRETURN=%d",
i, ret);
if (!SQL_SUCCEEDED(ret)) {
LOG("SQLGetData: Error retrieving SQL_SS_TIME2 for column "
"%d - SQLRETURN=%d",
i, ret);
}
row.append(py::none());
}
break;
Expand Down Expand Up @@ -3668,7 +3675,7 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
default:
std::ostringstream errorString;
errorString << "Unsupported data type for column - " << columnName << ", Type - "
<< dataType << ", column ID - " << i;
<< effectiveDataType << ", column ID - " << i;
LOG("SQLGetData: %s", errorString.str().c_str());
ThrowStdException(errorString.str());
break;
Expand Down Expand Up @@ -3822,13 +3829,11 @@ SQLRETURN SQLBindColums(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& column
SQLBindCol_ptr(hStmt, col, SQL_C_TYPE_DATE, buffers.dateBuffers[col - 1].data(),
sizeof(SQL_DATE_STRUCT), buffers.indicators[col - 1].data());
break;
case SQL_TIME:
case SQL_TYPE_TIME:
case SQL_SS_TIME2:
buffers.timeBuffers[col - 1].resize(fetchSize);
ret =
SQLBindCol_ptr(hStmt, col, SQL_C_TYPE_TIME, buffers.timeBuffers[col - 1].data(),
sizeof(SQL_TIME_STRUCT), buffers.indicators[col - 1].data());
SQLBindCol_ptr(hStmt, col, SQL_C_SS_TIME2, buffers.timeBuffers[col - 1].data(),
sizeof(SQL_SS_TIME2_STRUCT), buffers.indicators[col - 1].data());
break;
case SQL_GUID:
buffers.guidBuffers[col - 1].resize(fetchSize);
Expand Down Expand Up @@ -4132,13 +4137,11 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
PyList_SET_ITEM(row, col - 1, dateObj);
break;
}
case SQL_TIME:
case SQL_TYPE_TIME:
case SQL_SS_TIME2: {
const SQL_SS_TIME2_STRUCT& t2 = buffers.timeBuffers[col - 1][i];
PyObject* timeObj =
PythonObjectCache::get_time_class()(buffers.timeBuffers[col - 1][i].hour,
buffers.timeBuffers[col - 1][i].minute,
buffers.timeBuffers[col - 1][i].second)
PythonObjectCache::get_time_class()(t2.hour, t2.minute, t2.second,
t2.fraction / 1000) // ns to µs
.release()
.ptr();
PyList_SET_ITEM(row, col - 1, timeObj);
Expand Down Expand Up @@ -4271,10 +4274,8 @@ size_t calculateRowSize(py::list& columnNames, SQLUSMALLINT numCols) {
case SQL_TYPE_DATE:
rowSize += sizeof(SQL_DATE_STRUCT);
break;
case SQL_TIME:
case SQL_TYPE_TIME:
case SQL_SS_TIME2:
rowSize += sizeof(SQL_TIME_STRUCT);
rowSize += sizeof(SQL_SS_TIME2_STRUCT);
break;
case SQL_GUID:
rowSize += sizeof(SQLGUID);
Expand Down Expand Up @@ -5713,6 +5714,8 @@ PYBIND11_MODULE(ddbc_bindings, m) {
// Expose architecture-specific constants
m.attr("ARCHITECTURE") = ARCHITECTURE;

m.attr("SQL_NO_TOTAL") = static_cast<int>(SQL_NO_TOTAL);

// Expose the C++ functions to Python
m.def("ThrowStdException", &ThrowStdException);
m.def("GetDriverPathCpp", &GetDriverPathCpp, "Get the path to the ODBC driver");
Expand Down
10 changes: 9 additions & 1 deletion mssql_python/pybind/ddbc_bindings.h
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,14 @@ void DDBCSetDecimalSeparator(const std::string& separator);
// (Used internally by ddbc_bindings.cpp - not part of public API)
//-------------------------------------------------------------------------------------------------

// Struct to hold the SQL Server TIME2 structure (SQL_C_SS_TIME2)
struct SQL_SS_TIME2_STRUCT {
SQLUSMALLINT hour;
SQLUSMALLINT minute;
SQLUSMALLINT second;
SQLUINTEGER fraction; // Nanoseconds
};

// Struct to hold the DateTimeOffset structure
struct DateTimeOffset {
SQLSMALLINT year;
Expand All @@ -642,7 +650,7 @@ struct ColumnBuffers {
std::vector<std::vector<SQL_TIMESTAMP_STRUCT>> timestampBuffers;
std::vector<std::vector<SQLBIGINT>> bigIntBuffers;
std::vector<std::vector<SQL_DATE_STRUCT>> dateBuffers;
std::vector<std::vector<SQL_TIME_STRUCT>> timeBuffers;
std::vector<std::vector<SQL_SS_TIME2_STRUCT>> timeBuffers;
std::vector<std::vector<SQLGUID>> guidBuffers;
std::vector<std::vector<SQLLEN>> indicators;
std::vector<std::vector<DateTimeOffset>> datetimeoffsetBuffers;
Expand Down
Loading
Loading