Skip to content

Commit f389d0c

Browse files
committed
test: e2e bulk copy into a CLR UDT column (GH-667)
Adds an end-to-end bulkcopy test using the built-in geometry CLR UDT (same 0xF0 wire path as custom UDTs, which need a deployed assembly). Seeds a geometry column, reads the serialized UDT bytes, bulk copies them into a second geometry column, and asserts a byte-exact round-trip including a NULL. Requires the GH-667 fix in mssql_py_core (microsoft/mssql-rs#111): the varbinary(max) wire mapping plus bytes->UDT value coercion. Lands with the mssql-py-core version bump.
1 parent 0966b05 commit f389d0c

1 file changed

Lines changed: 61 additions & 0 deletions

File tree

tests/test_019_bulkcopy.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,3 +454,64 @@ def test_bulkcopy_empty_iterator(cursor):
454454
finally:
455455
cursor.execute(f"IF OBJECT_ID('{table_name}', 'U') IS NOT NULL DROP TABLE {table_name}")
456456
cursor.connection.commit()
457+
458+
459+
def test_bulkcopy_udt_geometry(cursor):
460+
"""GH-667: bulk copy into a CLR UDT column.
461+
462+
Custom (user-defined) CLR UDTs require a deployed assembly, so this test uses
463+
the built-in ``geometry`` CLR UDT, which travels the identical TDS ``0xF0``
464+
wire path. Before the fix, bulk copy of any UDT column failed with
465+
``Unsupported TDS type for bulk copy: 0xF0``; the fix streams UDTs as
466+
``varbinary(max)`` (their ``IBinarySerialize`` form).
467+
468+
The test seeds a ``geometry`` column, reads the serialized UDT bytes back,
469+
bulk copies them into a second ``geometry`` column, and verifies a byte-exact
470+
round-trip (including a NULL).
471+
"""
472+
src = "mssql_python_bcp_udt_src"
473+
dst = "mssql_python_bcp_udt_dst"
474+
475+
def _read(table):
476+
cursor.execute(f"SELECT id, g FROM {table} ORDER BY id")
477+
return [(r[0], bytes(r[1]) if r[1] is not None else None) for r in cursor.fetchall()]
478+
479+
try:
480+
# Seed a geometry (built-in CLR UDT) source table via T-SQL.
481+
cursor.execute(f"IF OBJECT_ID('{src}', 'U') IS NOT NULL DROP TABLE {src}")
482+
cursor.execute(f"CREATE TABLE {src} (id INT NOT NULL, g geometry NULL)")
483+
cursor.execute(
484+
f"INSERT INTO {src} (id, g) VALUES "
485+
"(1, geometry::STGeomFromText('POINT(1 2)', 0)), "
486+
"(2, geometry::STGeomFromText('LINESTRING(0 0, 10 10, 20 25)', 0)), "
487+
"(3, NULL), "
488+
"(4, geometry::STGeomFromText('POLYGON((0 0, 0 5, 5 5, 5 0, 0 0))', 0))"
489+
)
490+
cursor.connection.commit()
491+
492+
# Read the serialized UDT bytes back (unaffected by the fix).
493+
source_rows = _read(src)
494+
assert len(source_rows) == 4
495+
assert sum(1 for _, g in source_rows if g is not None) == 3
496+
assert any(g is None for _, g in source_rows)
497+
498+
# Destination geometry table.
499+
cursor.execute(f"IF OBJECT_ID('{dst}', 'U') IS NOT NULL DROP TABLE {dst}")
500+
cursor.execute(f"CREATE TABLE {dst} (id INT NOT NULL, g geometry NULL)")
501+
cursor.connection.commit()
502+
503+
# Bulk copy the UDT bytes into the geometry column. Requires the GH-667
504+
# fix in the bundled mssql_py_core (both the varbinary(max) wire mapping
505+
# and the bytes->UDT value-coercion). Pre-fix this failed with
506+
# "Unsupported TDS type for bulk copy: 0xF0" / a bad varbinary(-1) type /
507+
# a "target SQL type is Udt" coercion error.
508+
result = cursor.bulkcopy(dst, source_rows, timeout=60)
509+
assert result["rows_copied"] == 4
510+
511+
# Verify byte-exact round-trip, including the NULL row.
512+
assert _read(dst) == source_rows
513+
514+
finally:
515+
cursor.execute(f"IF OBJECT_ID('{src}', 'U') IS NOT NULL DROP TABLE {src}")
516+
cursor.execute(f"IF OBJECT_ID('{dst}', 'U') IS NOT NULL DROP TABLE {dst}")
517+
cursor.connection.commit()

0 commit comments

Comments
 (0)