Repository navigation
cursor.bulkcopy() cannot load custom (non-spatial) CLR UDT columns #667
Description
Activity
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!
- addedtriage neededFor new issues, not triaged yet.For new issues, not triaged yet.
on Jul 8, 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.
- addedtriage doneIssues that are triaged by dev team and are in investigation.Issues that are triaged by dev team and are in investigation.area: bulk-copyIssues in cursor.bulkcopy()Issues in cursor.bulkcopy()area: data-typesType conversion and encoding: VARCHAR/NVARCHAR, UTF-8, decimal, datetime, UUID, binary, JSON.Type conversion and encoding: VARCHAR/NVARCHAR, UTF-8, decimal, datetime, UUID, binary, JSON.bugSomething isn't workingSomething isn't workingand removedtriage neededFor new issues, not triaged yet.For new issues, not triaged yet.
on Jul 10, 2026 saurabh500 commented
on Jul 10, 2026 ContributorMore actionsgargsaumya i am curious about your finding. Do you have the same repro as what is reported here?
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 commented
on Jul 10, 2026 ContributorMore actionsThanks 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.
Reacted by gargsaumyaAppreciate you both digging in to this. I'll work on setting up a shareable repo with a more reliable reproduction. Stay tuned.
- added a commit that references this issue
on Jul 16, 2026 - added a commit that references this issue
on Jul 24, 2026 - added a commit that references this issue
on Jul 24, 2026 - added a commit that references this issue
on Jul 28, 2026
Describe the bug
cursor.bulkcopy()cannot load a column whose type is a custom, assembly-registered CLR UDT (any UDT other than the built-ingeography/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 viaSQLDescribeCol, getsSQL_SS_UDT(-151) for a custom UDT, and the nativewrite_to_server_zerocopythen emits anINSERT BULKwhose 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_mappingsmaps names only).A CLR UDT's wire form is
varbinary(max)(itsIBinarySerializepayload).bulkcopy()into avarbinary(max)column works fine, and other clients (pyodbc,python-tds) load UDT columns today by declaring the bulk column asvarbinary(max)and streaming the serialized bytes (the server materializes the UDT on insert).bulkcopy()offers no way to do that.Both server errors ("near ')'" and "near the keyword 'with'") are consistent with a generated
INSERT BULK (... v ) WITH (...)— an empty type before the), then theWITHclause.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):
SimpleBlob.cs:Build and register it:
Repro (
repro.py):Control that isolates the type (not the bytes, not
table_lock): change the column tov varbinary(max)and run the identicalbulkcopy— it succeeds, andSELECT CAST(v AS dbo.SimpleBlob) FROM dbo.udt_bcp_testround-trips the bytes. The failure happens only when the destination column is the UDT, in bothtable_lock=Trueandtable_lock=False.Expected behavior
bulkcopy()should load custom CLR UDT columns by sending the supplied bytes asvarbinary(max)(the UDT's serialized form), letting SQL Server materialize the UDT on insert — the same way spatial UDTs already round-trip and the waypyodbc/python-tdshandle 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-pythonversion: 1.10.0SQL Server version: SQL Server 2019
Operating system: macOS 14 (also reproduced on Ubuntu 24.04)
Additional context
Diagnosis (debug logs +
cursor.py/binding inspection):SQLDescribeColreturnsSQL_SS_UDT(-151);constants.pyonly maps -151 forgeography/geometry/hierarchyid, so a custom UDT falls through andwrite_to_server_zerocopyemits no valid type token for the column.cursor.bulkcopy's only column parameter iscolumn_mappings(names only), and the native binding confirms it —PyCoreCursor.bulkcopy.__text_signature__:What works, for contrast:
bulkcopy()intovarbinary(max)/int/char/tinyintcolumns; Arrow fetch (cursor.arrow()) of a UDT column via the LOB fallback; Kerberos/Trusted_Connection=yesconnect.