Skip to content

Commit 7ebef4f

Browse files
FEAT: Add spatial type support (geography, geometry, hierarchyid) (#423)
### Work Item / Issue Reference > GitHub Issue #352 ------------------------------------------------------------------- ### Summary Adds `SQL_SS_UDT` (-151) handling so `geography`, `geometry`, and `hierarchyid` columns are fetched as `bytes` instead of raising errors. These SQL Server CLR User-Defined Types all report as `SQL_SS_UDT` via ODBC and were previously unhandled in both the C++ binding layer and Python type maps. ## Changes | File | Change | |------|--------| | `ddbc_bindings.cpp` | Define `SQL_SS_UDT` constant; add `case SQL_SS_UDT:` fallthroughs to `SQL_BINARY` in `SQLGetData_wrap`, `SQLBindColums`, and `FetchBatchData`; separate `calculateRowSize` case with LOB-size fallback; include in LOB detection for `FetchMany_wrap` and `FetchAll_wrap` | | `constants.py` | Add `SQL_SS_UDT = -151` to `ConstantsDDBC` enum and `get_valid_types()` | | `cursor.py` | Add `SQL_SS_UDT -> SQL_C_BINARY` in `sql_to_c_type` map; add `SQL_SS_UDT -> bytes` in `sql_to_python_type` map | | `test_017_spatial_types.py` | 37 tests covering geography, geometry, and hierarchyid: fetch paths (fetchone/fetchmany/fetchall/executemany), NULL handling, mixed columns, binary round-trips, output converters, cursor.description metadata, error cases | ## Testing - Built `ddbc_bindings.cp313-arm64.pyd` with 0 errors - Existing test suite: 639 passed, 1 pre-existing failure (unrelated `test_no_segfault_on_gc` WinError 50), 8 skipped - Spatial tests require live SQL Server connection with geography/geometry/hierarchyid support Co-authored-by: gargsaumya <saumyagarg.100@gmail.com>
1 parent 6a5491c commit 7ebef4f

File tree

4 files changed

+844
-3
lines changed

4 files changed

+844
-3
lines changed

mssql_python/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ class ConstantsDDBC(Enum):
114114
SQL_FETCH_ABSOLUTE = 5
115115
SQL_FETCH_RELATIVE = 6
116116
SQL_FETCH_BOOKMARK = 8
117+
SQL_SS_UDT = -151
117118
SQL_DATETIMEOFFSET = -155
118119
SQL_SS_TIME2 = -154
119120
SQL_SS_XML = -152
@@ -374,6 +375,7 @@ def get_valid_types(cls) -> set:
374375
ConstantsDDBC.SQL_DATETIMEOFFSET.value,
375376
ConstantsDDBC.SQL_SS_XML.value,
376377
ConstantsDDBC.SQL_GUID.value,
378+
ConstantsDDBC.SQL_SS_UDT.value,
377379
}
378380

379381
# Could also add category methods for convenience

mssql_python/cursor.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,7 @@ def _get_c_type_for_sql_type(self, sql_type: int) -> int:
865865
ddbc_sql_const.SQL_BINARY.value: ddbc_sql_const.SQL_C_BINARY.value,
866866
ddbc_sql_const.SQL_VARBINARY.value: ddbc_sql_const.SQL_C_BINARY.value,
867867
ddbc_sql_const.SQL_LONGVARBINARY.value: ddbc_sql_const.SQL_C_BINARY.value,
868+
ddbc_sql_const.SQL_SS_UDT.value: ddbc_sql_const.SQL_C_BINARY.value,
868869
# ODBC 3.x date/time types (reported by ODBC 18 driver)
869870
ddbc_sql_const.SQL_TYPE_DATE.value: ddbc_sql_const.SQL_C_TYPE_DATE.value,
870871
ddbc_sql_const.SQL_TYPE_TIME.value: ddbc_sql_const.SQL_C_TYPE_TIME.value,
@@ -1052,6 +1053,7 @@ def _map_data_type(self, sql_type):
10521053
SQL_SS_TIME2(-154) for time columns
10531054
SQL_DATETIMEOFFSET(-155) for datetimeoffset columns
10541055
SQL_SS_XML(-152) for xml columns
1056+
SQL_SS_UDT(-151) for geography/geometry/hierarchyid columns
10551057
10561058
ODBC 2.x aliases (9, 10, 11) are also accepted defensively.
10571059
@@ -1097,6 +1099,7 @@ def _map_data_type(self, sql_type):
10971099
ddbc_sql_const.SQL_BINARY.value: bytes,
10981100
ddbc_sql_const.SQL_VARBINARY.value: bytes,
10991101
ddbc_sql_const.SQL_LONGVARBINARY.value: bytes,
1102+
ddbc_sql_const.SQL_SS_UDT.value: bytes,
11001103
# UUID
11011104
ddbc_sql_const.SQL_GUID.value: uuid.UUID,
11021105
# XML — driver reports SQL_SS_XML (-152), fetched as str

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@
2020
// Macro definitions
2121
//-------------------------------------------------------------------------------------------------
2222

23-
// This constant is not exposed via sql.h, hence define it here
23+
// These constants are not exposed via sql.h, hence define them here
2424
#define SQL_SS_TIME2 (-154)
2525
#define SQL_SS_TIMESTAMPOFFSET (-155)
2626
#define SQL_C_SS_TIMESTAMPOFFSET (0x4001)
2727
#define MAX_DIGITS_IN_NUMERIC 64
2828
#define SQL_MAX_NUMERIC_LEN 16
2929
#define SQL_SS_XML (-152)
30+
#define SQL_SS_UDT (-151)
3031

3132
#define STRINGIFY_FOR_CASE(x) \
3233
case x: \
@@ -3285,6 +3286,7 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
32853286
}
32863287
break;
32873288
}
3289+
case SQL_SS_UDT:
32883290
case SQL_BINARY:
32893291
case SQL_VARBINARY:
32903292
case SQL_LONGVARBINARY: {
@@ -3555,6 +3557,7 @@ SQLRETURN SQLBindColums(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& column
35553557
ret = SQLBindCol_ptr(hStmt, col, SQL_C_GUID, buffers.guidBuffers[col - 1].data(),
35563558
sizeof(SQLGUID), buffers.indicators[col - 1].data());
35573559
break;
3560+
case SQL_SS_UDT:
35583561
case SQL_BINARY:
35593562
case SQL_VARBINARY:
35603563
case SQL_LONGVARBINARY:
@@ -3683,6 +3686,7 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
36833686
case SQL_WLONGVARCHAR:
36843687
columnProcessors[col] = ColumnProcessors::ProcessWChar;
36853688
break;
3689+
case SQL_SS_UDT:
36863690
case SQL_BINARY:
36873691
case SQL_VARBINARY:
36883692
case SQL_LONGVARBINARY:
@@ -3981,6 +3985,10 @@ size_t calculateRowSize(py::list& columnNames, SQLUSMALLINT numCols) {
39813985
case SQL_BIT:
39823986
rowSize += sizeof(SQLCHAR);
39833987
break;
3988+
case SQL_SS_UDT:
3989+
rowSize += (static_cast<SQLLEN>(columnSize) == SQL_NO_TOTAL || columnSize == 0)
3990+
? SQL_MAX_LOB_SIZE : columnSize;
3991+
break;
39843992
case SQL_BINARY:
39853993
case SQL_VARBINARY:
39863994
case SQL_LONGVARBINARY:
@@ -4043,7 +4051,8 @@ SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetch
40434051

40444052
if ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR || dataType == SQL_VARCHAR ||
40454053
dataType == SQL_LONGVARCHAR || dataType == SQL_VARBINARY ||
4046-
dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML) &&
4054+
dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML ||
4055+
dataType == SQL_SS_UDT) &&
40474056
(columnSize == 0 || columnSize == SQL_NO_TOTAL || columnSize > SQL_MAX_LOB_SIZE)) {
40484057
lobColumns.push_back(i + 1); // 1-based
40494058
}
@@ -4181,7 +4190,8 @@ SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows,
41814190

41824191
if ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR || dataType == SQL_VARCHAR ||
41834192
dataType == SQL_LONGVARCHAR || dataType == SQL_VARBINARY ||
4184-
dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML) &&
4193+
dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML ||
4194+
dataType == SQL_SS_UDT) &&
41854195
(columnSize == 0 || columnSize == SQL_NO_TOTAL || columnSize > SQL_MAX_LOB_SIZE)) {
41864196
lobColumns.push_back(i + 1); // 1-based
41874197
}

0 commit comments

Comments
 (0)