Skip to content

Commit fe57f23

Browse files
committed
working xml
1 parent cc180ec commit fe57f23

File tree

3 files changed

+174
-13
lines changed

3 files changed

+174
-13
lines changed

main.py

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,80 @@
55

66
setup_logging('stdout')
77

8-
conn_str = os.getenv("DB_CONNECTION_STRING")
9-
conn = connect(conn_str)
8+
# conn_str = os.getenv("DB_CONNECTION_STRING")
9+
# conn = connect(conn_str)
10+
11+
# # conn.autocommit = True
12+
13+
# cursor = conn.cursor()
14+
# cursor.execute("SELECT database_id, name from sys.databases;")
15+
# rows = cursor.fetchall()
16+
17+
# for row in rows:
18+
# print(f"Database ID: {row[0]}, Name: {row[1]}")
19+
20+
# cursor.close()
21+
# conn.close()
1022

11-
# conn.autocommit = True
23+
# import pyodbc
1224

25+
# --- Connection ---
26+
conn_str = "DRIVER={ODBC Driver 18 for SQL Server};Server=Saumya;DATABASE=master;UID=sa;PWD=HappyPass1234;Trust_Connection=yes;TrustServerCertificate=yes;"
27+
conn = connect(conn_str)
1328
cursor = conn.cursor()
14-
cursor.execute("SELECT database_id, name from sys.databases;")
15-
rows = cursor.fetchall()
1629

17-
for row in rows:
18-
print(f"Database ID: {row[0]}, Name: {row[1]}")
30+
# --- Create a test table ---
31+
# cursor.execute("""
32+
# IF OBJECT_ID('dbo.TestXML', 'U') IS NOT NULL DROP TABLE dbo.TestXML;
33+
# CREATE TABLE dbo.TestXML (
34+
# id INT IDENTITY PRIMARY KEY,
35+
# xml_col XML
36+
# )
37+
# """)
38+
# conn.commit()
39+
40+
# # --- Test short XML insertion ---
41+
# short_xml = "<root><item>123</item></root>"
42+
# cursor.execute("INSERT INTO dbo.TestXML (xml_col) VALUES (?)", short_xml)
43+
# conn.commit()
44+
45+
# # --- Test long XML insertion (simulate DAE / large XML) ---
46+
# long_xml = "<root>" + "".join(f"<item>{i}</item>" for i in range(10000)) + "</root>"
47+
# cursor.execute("INSERT INTO dbo.TestXML (xml_col) VALUES (?)", long_xml)
48+
# conn.commit()
49+
50+
# # --- Fetch and verify ---
51+
# cursor.execute("SELECT id, xml_col FROM dbo.TestXML ORDER BY id")
52+
# rows = cursor.fetchall()
53+
54+
# for row in rows:
55+
# print(f"ID: {row.id}, XML Length: {len(str(row.xml_col))}")
56+
57+
# --- Clean up ---
58+
# cursor.execute("DROP TABLE dbo.TestXML")
59+
# conn.commit()
60+
# cursor.close()
61+
# conn.close()
62+
63+
64+
65+
SMALL_XML = "<root><item>1</item></root>"
66+
LARGE_XML = "<root>" + "".join(f"<item>{i}</item>" for i in range(10000)) + "</root>"
67+
EMPTY_XML = ""
68+
INVALID_XML = "<root><item></root>" # malformed
69+
70+
71+
cursor.execute("CREATE TABLE #pytest_xml_empty_null (id INT PRIMARY KEY IDENTITY(1,1), xml_col XML NULL);")
72+
conn.commit()
73+
74+
cursor.execute("INSERT INTO #pytest_xml_empty_null (xml_col) VALUES (?);", EMPTY_XML)
75+
cursor.execute("INSERT INTO #pytest_xml_empty_null (xml_col) VALUES (?);", None)
76+
conn.commit()
77+
78+
rows = [r[0] for r in cursor.execute("SELECT xml_col FROM #pytest_xml_empty_null ORDER BY id;").fetchall()]
79+
print(rows)
80+
assert rows[0] == EMPTY_XML
81+
assert rows[1] is None
1982

20-
cursor.close()
21-
conn.close()
83+
cursor.execute("DROP TABLE IF EXISTS #pytest_xml_empty_null;")
84+
conn.commit()

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#define SQL_SS_TIMESTAMPOFFSET (-155)
2222
#define SQL_C_SS_TIMESTAMPOFFSET (0x4001)
2323
#define MAX_DIGITS_IN_NUMERIC 64
24+
#define SQL_SS_XML (-152)
2425

2526
#define STRINGIFY_FOR_CASE(x) \
2627
case x: \
@@ -2525,6 +2526,12 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
25252526
}
25262527
break;
25272528
}
2529+
case SQL_SS_XML:
2530+
{
2531+
LOG("Streaming XML for column {}", i);
2532+
row.append(FetchLobColumnData(hStmt, i, SQL_C_WCHAR, true, false));
2533+
break;
2534+
}
25282535
case SQL_WCHAR:
25292536
case SQL_WVARCHAR:
25302537
case SQL_WLONGVARCHAR: {
@@ -2982,6 +2989,7 @@ SQLRETURN SQLBindColums(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& column
29822989
buffers.indicators[col - 1].data());
29832990
break;
29842991
}
2992+
case SQL_SS_XML:
29852993
case SQL_WCHAR:
29862994
case SQL_WVARCHAR:
29872995
case SQL_WLONGVARCHAR: {
@@ -3184,6 +3192,10 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
31843192
}
31853193
break;
31863194
}
3195+
case SQL_SS_XML: {
3196+
row.append(FetchLobColumnData(hStmt, col, SQL_C_WCHAR, true, false));
3197+
break;
3198+
}
31873199
case SQL_WCHAR:
31883200
case SQL_WVARCHAR:
31893201
case SQL_WLONGVARCHAR: {
@@ -3397,6 +3409,7 @@ size_t calculateRowSize(py::list& columnNames, SQLUSMALLINT numCols) {
33973409
case SQL_LONGVARCHAR:
33983410
rowSize += columnSize;
33993411
break;
3412+
case SQL_SS_XML:
34003413
case SQL_WCHAR:
34013414
case SQL_WVARCHAR:
34023415
case SQL_WLONGVARCHAR:
@@ -3501,12 +3514,13 @@ SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetch
35013514

35023515
if ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR ||
35033516
dataType == SQL_VARCHAR || dataType == SQL_LONGVARCHAR ||
3504-
dataType == SQL_VARBINARY || dataType == SQL_LONGVARBINARY) &&
3517+
dataType == SQL_VARBINARY || dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML) &&
35053518
(columnSize == 0 || columnSize == SQL_NO_TOTAL || columnSize > SQL_MAX_LOB_SIZE)) {
3519+
std::cout<<"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"<<std::endl;
35063520
lobColumns.push_back(i + 1); // 1-based
35073521
}
35083522
}
3509-
3523+
std::cout<<"lobColumns.size()="<<lobColumns.size()<<std::endl;
35103524
// If we have LOBs → fall back to row-by-row fetch + SQLGetData_wrap
35113525
if (!lobColumns.empty()) {
35123526
LOG("LOB columns detected, using per-row SQLGetData path");
@@ -3521,7 +3535,7 @@ SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetch
35213535
}
35223536
return SQL_SUCCESS;
35233537
}
3524-
3538+
std::cout<<"No LOB columns, using batch fetch path"<<std::endl;
35253539
// Initialize column buffers
35263540
ColumnBuffers buffers(numCols, fetchSize);
35273541

@@ -3623,7 +3637,7 @@ SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows) {
36233637

36243638
if ((dataType == SQL_WVARCHAR || dataType == SQL_WLONGVARCHAR ||
36253639
dataType == SQL_VARCHAR || dataType == SQL_LONGVARCHAR ||
3626-
dataType == SQL_VARBINARY || dataType == SQL_LONGVARBINARY) &&
3640+
dataType == SQL_VARBINARY || dataType == SQL_LONGVARBINARY || dataType == SQL_SS_XML) &&
36273641
(columnSize == 0 || columnSize == SQL_NO_TOTAL || columnSize > SQL_MAX_LOB_SIZE)) {
36283642
lobColumns.push_back(i + 1); // 1-based
36293643
}

tests/test_004_cursor.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11403,6 +11403,90 @@ def test_datetime_string_parameter_binding(cursor, db_connection):
1140311403
drop_table_if_exists(cursor, table_name)
1140411404
db_connection.commit()
1140511405

11406+
SMALL_XML = "<root><item>1</item></root>"
11407+
LARGE_XML = "<root>" + "".join(f"<item>{i}</item>" for i in range(10000)) + "</root>"
11408+
EMPTY_XML = ""
11409+
INVALID_XML = "<root><item></root>" # malformed
11410+
11411+
def test_xml_basic_insert_fetch(cursor, db_connection):
11412+
"""Test insert and fetch of a small XML value."""
11413+
try:
11414+
cursor.execute("CREATE TABLE #pytest_xml_basic (id INT PRIMARY KEY IDENTITY(1,1), xml_col XML NULL);")
11415+
db_connection.commit()
11416+
11417+
cursor.execute("INSERT INTO #pytest_xml_basic (xml_col) VALUES (?);", SMALL_XML)
11418+
db_connection.commit()
11419+
11420+
row = cursor.execute("SELECT xml_col FROM #pytest_xml_basic;").fetchone()
11421+
assert row[0] == SMALL_XML
11422+
finally:
11423+
cursor.execute("DROP TABLE IF EXISTS #pytest_xml_basic;")
11424+
db_connection.commit()
11425+
11426+
11427+
def test_xml_empty_and_null(cursor, db_connection):
11428+
"""Test insert and fetch of empty XML and NULL values."""
11429+
try:
11430+
cursor.execute("CREATE TABLE #pytest_xml_empty_null (id INT PRIMARY KEY IDENTITY(1,1), xml_col XML NULL);")
11431+
db_connection.commit()
11432+
11433+
cursor.execute("INSERT INTO #pytest_xml_empty_null (xml_col) VALUES (?);", EMPTY_XML)
11434+
cursor.execute("INSERT INTO #pytest_xml_empty_null (xml_col) VALUES (?);", None)
11435+
db_connection.commit()
11436+
11437+
rows = [r[0] for r in cursor.execute("SELECT xml_col FROM #pytest_xml_empty_null ORDER BY id;").fetchall()]
11438+
assert rows[0] == EMPTY_XML
11439+
assert rows[1] is None
11440+
finally:
11441+
cursor.execute("DROP TABLE IF EXISTS #pytest_xml_empty_null;")
11442+
db_connection.commit()
11443+
11444+
11445+
def test_xml_large_insert(cursor, db_connection):
11446+
"""Test insert and fetch of a large XML value to verify streaming/DAE."""
11447+
try:
11448+
cursor.execute("CREATE TABLE #pytest_xml_large (id INT PRIMARY KEY IDENTITY(1,1), xml_col XML NULL);")
11449+
db_connection.commit()
11450+
11451+
cursor.execute("INSERT INTO #pytest_xml_large (xml_col) VALUES (?);", LARGE_XML)
11452+
db_connection.commit()
11453+
11454+
row = cursor.execute("SELECT xml_col FROM #pytest_xml_large;").fetchone()
11455+
assert row[0] == LARGE_XML
11456+
finally:
11457+
cursor.execute("DROP TABLE IF EXISTS #pytest_xml_large;")
11458+
db_connection.commit()
11459+
11460+
11461+
def test_xml_batch_insert(cursor, db_connection):
11462+
"""Test batch insert (executemany) of multiple XML values."""
11463+
try:
11464+
cursor.execute("CREATE TABLE #pytest_xml_batch (id INT PRIMARY KEY IDENTITY(1,1), xml_col XML NULL);")
11465+
db_connection.commit()
11466+
11467+
xmls = [f"<root><item>{i}</item></root>" for i in range(5)]
11468+
cursor.executemany("INSERT INTO #pytest_xml_batch (xml_col) VALUES (?);", [(x,) for x in xmls])
11469+
db_connection.commit()
11470+
11471+
rows = [r[0] for r in cursor.execute("SELECT xml_col FROM #pytest_xml_batch ORDER BY id;").fetchall()]
11472+
assert rows == xmls
11473+
finally:
11474+
cursor.execute("DROP TABLE IF EXISTS #pytest_xml_batch;")
11475+
db_connection.commit()
11476+
11477+
11478+
def test_xml_malformed_input(cursor, db_connection):
11479+
"""Verify driver raises error for invalid XML input."""
11480+
try:
11481+
cursor.execute("CREATE TABLE #pytest_xml_invalid (id INT PRIMARY KEY IDENTITY(1,1), xml_col XML NULL);")
11482+
db_connection.commit()
11483+
11484+
with pytest.raises(Exception):
11485+
cursor.execute("INSERT INTO #pytest_xml_invalid (xml_col) VALUES (?);", INVALID_XML)
11486+
finally:
11487+
cursor.execute("DROP TABLE IF EXISTS #pytest_xml_invalid;")
11488+
db_connection.commit()
11489+
1140611490
def test_close(db_connection):
1140711491
"""Test closing the cursor"""
1140811492
try:

0 commit comments

Comments
 (0)