You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
cursor.bulkcopy() cannot load a column whose type is a custom, assembly-registered CLR UDT (any UDT other than the built-in geography/geometry/hierarchyid). It raises a server-side syntax error instead of inserting the rows.
The cause is that bulkcopy() derives each column's type from the destination via SQLDescribeCol, gets SQL_SS_UDT (-151) for a custom UDT, and the native write_to_server_zerocopy then emits an INSERT BULK whose column list has an empty/invalid type token for that column (e.g. INSERT BULK dbo.udt_bcp_test (id int, v ) WITH (...)). There is no per-column type override to work around it (column_mappings maps names only).
A CLR UDT's wire form isvarbinary(max) (its IBinarySerialize payload). bulkcopy() into a varbinary(max) column works fine, and other clients (pyodbc, python-tds) load UDT columns today by declaring the bulk column as varbinary(max) and streaming the serialized bytes (the server materializes the UDT on insert). bulkcopy() offers no way to do that.
Exception message:
RuntimeError: Sql Error: 102: Class 15: State 1: Incorrect syntax near ')'. on <server> in at line 1
Sql Error: 319: Class 15: State 1: Incorrect syntax near the keyword 'with'. If this statement is a common table expression, an xmlnamespaces clause or a change tracking context clause, the previous statement must be terminated with a semicolon. on <server> in at line 1
Stack trace:
Traceback (most recent call last):
File "repro.py", line 21, in <module>
cur.bulkcopy("dbo.udt_bcp_test", rows, table_lock=True)
File ".../site-packages/mssql_python/cursor.py", line 3073, in bulkcopy
raise type(e)(str(e)) from None
RuntimeError: Sql Error: 102: Class 15: State 1: Incorrect syntax near ')'. on <server> in at line 1
Sql Error: 319: Class 15: State 1: Incorrect syntax near the keyword 'with'. ...
Both server errors ("near ')'" and "near the keyword 'with'") are consistent with a generated INSERT BULK (... v ) WITH (...) — an empty type before the ), then the WITH clause.
To reproduce
One-time prerequisite — a minimal custom CLR UDT (the bug needs a non-spatial assembly UDT; built-in spatial types do not reproduce it):
importstructimportmssql_pythonCONN="Server=<server>;Database=<db>;Trusted_Connection=yes;TrustServerCertificate=yes"conn=mssql_python.connect(CONN)
cur=conn.cursor()
cur.execute(
"IF OBJECT_ID('dbo.udt_bcp_test','U') IS NOT NULL DROP TABLE dbo.udt_bcp_test;""CREATE TABLE dbo.udt_bcp_test (id int NOT NULL, v dbo.SimpleBlob NULL)"
)
conn.commit()
data=b"\x01\x02\x03\x04"payload=struct.pack("<i", len(data)) +data# SimpleBlob's IBinarySerialize bytesrows= [(1, payload), (2, payload)]
cur.bulkcopy("dbo.udt_bcp_test", rows, table_lock=True) # <-- raises (see exception above)
Control that isolates the type (not the bytes, not table_lock): change the column to v varbinary(max) and run the identical bulkcopy — it succeeds, and SELECT CAST(v AS dbo.SimpleBlob) FROM dbo.udt_bcp_test round-trips the bytes. The failure happens only when the destination column is the UDT, in both table_lock=True and table_lock=False.
Expected behavior
bulkcopy() should load custom CLR UDT columns by sending the supplied bytes as varbinary(max) (the UDT's serialized form), letting SQL Server materialize the UDT on insert — the same way spatial UDTs already round-trip and the way pyodbc/python-tds handle it. At minimum it should raise a clear "unsupported/undescribable column type" error rather than a raw T-SQL syntax error.
Further technical details
Python version: 3.11 mssql-python version: 1.10.0
SQL Server version: SQL Server 2019
Operating system: macOS 14 (also reproduced on Ubuntu 24.04)
SQLDescribeCol returns SQL_SS_UDT (-151); constants.py only maps -151 for geography/geometry/hierarchyid, so a custom UDT falls through and write_to_server_zerocopy emits no valid type token for the column.
No Python-level override exists: cursor.bulkcopy's only column parameter is column_mappings (names only), and the native binding confirms it — PyCoreCursor.bulkcopy.__text_signature__:
What works, for contrast: bulkcopy() into varbinary(max)/int/char/tinyint columns; Arrow fetch (cursor.arrow()) of a UDT column via the LOB fallback; Kerberos/Trusted_Connection=yes connect.
Confirmed root cause: bulkcopy() derives each destination column's type via SQLDescribeCol, which returns SQL_SS_UDT (-151) for a custom CLR UDT. The bulk-insert path in the native core only emits a valid INSERT BULK type token for the built-in spatial UDTs (geography/geometry/hierarchyid), so for a generic UDT it writes an empty token — producing the malformed INSERT BULK (... v ) WITH (...) and the near ')' / near 'with' syntax errors you saw.
We'll be taking this up. Will follow up on this thread as the fix progresses.
Sorry for the misunderstanding, that was a hypothesis for why the SQL syntax error reported by JC (@jlchmura) might be occurring and it was something I wanted to investigate further.
After digging deeper, I found that the bulk copy operation is actually failing for the UDT type with the following error: Protocol Error: Unsupported TDS type for bulk copy: 0xF0
I saw this behavior on core 0.1.5, 0.1.6, and the exact PyPI release mssql-python==1.10.0, and consistently observed only the unsupported TDS type error.
This error is raised while writing COLMETADATA. A UDT column carries wire type 0xF0, which currently has no matching handler, so it hits the catch-all and errors before any row is sent.This isn't a missing feature per se - the statement-text side already treats UDT as varbinary (get_sql_type_definition() → varbinary(N)), but the wire-type side doesn't. I'll be sending a fix for this.
That said, I was not able to reproduce the SQL syntax error reported by JC (@jlchmura), so the protocol error and the syntax error appear to be separate issues.
While we can fix the bug with Protocol Error, I am not sure if that fix satisfies JC (@jlchmura)'s usecase. JC (@jlchmura) please help us with a repro which we can run in our environments.
Activity
github-actions commentedon Jul 8, 2026
Hi JC (@jlchmura), thank you for opening this issue!
Our team will review it shortly. We aim to triage all new issues within 24-48 hours and get back to you.
If you have additional information to share, please feel free to update the issue.
Thank you for your patience!
gargsaumya commentedon Jul 10, 2026
Thanks JC (@jlchmura) for the detailed report.
Confirmed root cause: bulkcopy() derives each destination column's type via SQLDescribeCol, which returns SQL_SS_UDT (-151) for a custom CLR UDT. The bulk-insert path in the native core only emits a valid INSERT BULK type token for the built-in spatial UDTs (geography/geometry/hierarchyid), so for a generic UDT it writes an empty token — producing the malformed INSERT BULK (... v ) WITH (...) and the near ')' / near 'with' syntax errors you saw.
We'll be taking this up. Will follow up on this thread as the fix progresses.
saurabh500 commentedon Jul 10, 2026
gargsaumya i am curious about your finding. Do you have the same repro as what is reported here?
gargsaumya commentedon Jul 10, 2026
Sorry for the misunderstanding, that was a hypothesis for why the SQL syntax error reported by JC (@jlchmura) might be occurring and it was something I wanted to investigate further.
After digging deeper, I found that the bulk copy operation is actually failing for the UDT type with the following error:
Protocol Error: Unsupported TDS type for bulk copy: 0xF0I saw this behavior on core 0.1.5, 0.1.6, and the exact PyPI release mssql-python==1.10.0, and consistently observed only the unsupported TDS type error.
This error is raised while writing COLMETADATA. A UDT column carries wire type 0xF0, which currently has no matching handler, so it hits the catch-all and errors before any row is sent.This isn't a missing feature per se - the statement-text side already treats UDT as varbinary (get_sql_type_definition() → varbinary(N)), but the wire-type side doesn't. I'll be sending a fix for this.
That said, I was not able to reproduce the SQL syntax error reported by JC (@jlchmura), so the protocol error and the syntax error appear to be separate issues.
saurabh500 commentedon Jul 10, 2026
Thanks for clarifying gargsaumya
I had similar findings :) JC (@jlchmura) had opened a similar issue in microsoft/mssql-rs#97 where I had provided my findings.
While we can fix the bug with Protocol Error, I am not sure if that fix satisfies JC (@jlchmura)'s usecase. JC (@jlchmura) please help us with a repro which we can run in our environments.
jlchmura commentedon Jul 10, 2026
Appreciate you both digging in to this. I'll work on setting up a shareable repo with a more reliable reproduction. Stay tuned.
test: e2e bulk copy into a CLR UDT column (GH-667)
CHORE: Bump mssql-py-core to 0.1.7 (#688)
RELEASE:1.12.0 (#689)
CHORE: e2e bulk copy into a CLR UDT column (GH-667) (#683)