Repository navigation
Feature | Send uniqueidentifier columns natively in bulk copy - #3041
Muskan Gupta (muskan124947) merged 16 commits into
Conversation
ab4aa95 to
37c5a3d
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
It changes low-level bulk-copy TDS type/value encoding for GUIDs, so it warrants final human review despite strong test coverage.
Pull request overview
This PR updates SQLServerBulkCopy to send uniqueidentifier values using the native TDS GUID type (16 bytes) when the source declares microsoft.sql.Types.GUID and the destination is uniqueidentifier, avoiding per-row server-side implicit conversions and their performance/cardinality-estimation impact.
Changes:
- Emit
TDSType.GUIDtype info and write GUID values as native 16-byte payloads for GUID→uniqueidentifierbulk copy. - Adjust
INSERT BULKcolumn type declaration touniqueidentifierfor GUID sources targeting GUID destinations. - Add comprehensive bulk-copy tests for plaintext and Always Encrypted GUID destinations, plus a CHANGELOG entry.
File summaries
| File | Description |
|---|---|
| src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBulkCopy.java | Switch GUID bulk-copy wire format to native GUID type/value encoding when destination is uniqueidentifier. |
| src/test/java/com/microsoft/sqlserver/jdbc/bulkCopy/BulkCopyGuidTest.java | Adds plaintext bulk-copy tests covering behavior parity, client-side parse failures, and XE conversion detection. |
| src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/BulkCopyGuidAETest.java | Adds AE-specific coverage to ensure encrypted uniqueidentifier remains ciphertext-based and GUID source rejection stays pinned. |
| CHANGELOG.md | Documents the behavior/performance change for GUID bulk copy into uniqueidentifier. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3041 +/- ##
============================================
+ Coverage 61.50% 61.63% +0.13%
- Complexity 5288 5359 +71
============================================
Files 154 154
Lines 36817 36869 +52
Branches 6770 6789 +19
============================================
+ Hits 22644 22725 +81
+ Misses 10310 10308 -2
+ Partials 3863 3836 -27 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Muskan Gupta (muskan124947)
left a comment
There was a problem hiding this comment.
Reviewed the native GUID bulk-copy change against merge-base 347d7755cdd46a1c01e3b23b02c707986264424d, including the parsing/precision and encrypted-source follow-up fixes. One new compatibility finding is attached: a reproduced change in the committed outcome of a caller-owned transaction after a malformed GUID, beyond the previously discussed parsing and exception changes.
Validation included the standalone parser suite, 311 non-Extended-Events integration cases, and identical baseline/head transaction probes on Windows/JDK 21 with SQL Server 16.0.1200.5. Always Encrypted execution, other runtime/platform combinations, and a quantitative performance comparison were not rerun; source review is not proof of compatibility or performance success.
SummaryFollow-up review of VerificationIndependently checked the REST commit/compare results: both revisions have tree Findings
No new high-confidence findings in the reviewed scope; this is not proof of compatibility. Performance / CI statusCurrent checks are successful. Windows build 177988 retains an initial test-error summary, but its actual JDK 17 AzureDB task log records |
d208914 to
b5c4345
Compare
Bulk copy declared a source column of type microsoft.sql.Types.GUID as CHAR(n) on the wire, so the server ran CONVERT_IMPLICIT(uniqueidentifier, ...) for every row inserted into a uniqueidentifier column. The native uniqueidentifier TDS type was only ever emitted for Always Encrypted base type metadata. Declare the column as uniqueidentifier and send the value in its native 16 byte representation. The new connection property sendGuidAsStringForBulkCopy restores the previous behavior.
Native uniqueidentifier is unconditional when the destination column is uniqueidentifier and the source column is declared as microsoft.sql.Types.GUID. Removes sendGuidAsStringForBulkCopy and the tests covering it.
Report an unparsable GUID as a SQLServerException instead of letting an IllegalArgumentException escape writeToServer in the middle of a batch, and accept the registry format in braces so that parsing on the client keeps the renderings the server accepted. Move the tests into BulkCopyGuidTest, where a CHAR source column carries the character wire format used before and lets every rendering be compared between both formats, add the CHAR control to the Extended Events test, and cover an encrypted uniqueidentifier column.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 47dd6a03-39b5-4d61-b7be-5ca5e885d9c5
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 47dd6a03-39b5-4d61-b7be-5ca5e885d9c5
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Validate canonical GUID groups, preserve suffix and declared-precision semantics, and cover custom and CSV sources with regression tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
An encrypted source column reports its base type in the column metadata, but the row data path replaces bulkJdbcType with the destination type. For an encrypted char/varchar source copied into a plaintext uniqueidentifier column, the metadata declared a CHAR column while the row was written as a native 16 byte GUID, corrupting the TDS stream. Decide on the native GUID row format from the source type, as writeTypeInfo does. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolve native source types per destination without changing public JDBC metadata or encrypted transfers. Refresh ResultSet source metadata when reusing bulk copy and retain SQL-compatible GUID parsing with shared precision checks. Add native-wire, conversion, mapping, source-reuse, and batch API regression coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Use a shared eligibility check for the declaration, type metadata and row payload. Inspect current SQLServerResultSet metadata before reading streams; preserve original JDBC type assignments, source caching and existing writers. Cover streamed VARCHAR sources and retain source-reuse regression tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
b5c4345 to
3e4019e
Compare
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
Review effort: Lite
Findings: 1
Open (3)
The returnedSQLServerBulkCSVFileRecordis constructed over anInputStreamthat is immediately… · New The GUID byte length is hard-coded as0x10in multiple places. Consider introducing a single… · New The GUID byte length is hard-coded as0x10in multiple places. Consider introducing a single… · New



Note
Community PR #3006 by Siarhei (@huzaus)
Source:
huzaus/mssql-jdbc:feature/native-uniqueidentifier-bulkcopyatec5724ddcb1902ca1f22f7c89e1fd08eedac8bc7Brief Description
Send eligible bulk-copy GUID columns using native TDS
uniqueidentifierencoding rather thanCHAR(n), avoiding server-side character-to-GUID conversion. GUID values were already supported; this changes their transfer representation while preserving the tested input acceptance and declared-precision constraints.In
SQLServerBulkCopy:isNativeGuid(source, destination)check selects native encoding consistently for the bulk SQL declaration, column metadata, and row payload.getSourceMetadata,writeTypeInfo, andwriteColumnToTdsWriterretain their original implementation. The native case is handled by narrow branches around the existing paths.ssTypestorage/copying or newsrcColumnMetadata = nullresets are needed. For SQL Server ResultSets the check inspects the current column's native type, without changing cached/public JDBC metadata.ISQLServerBulkDatasource, including custom records and CSV, eligibility reads the current source'sgetColumnType()rather than its cached type. This handles GUID-to-CHAR and CHAR-to-GUID source reuse without resetting the general metadata cache. An active ResultSet takes precedence over any retained bulk-data object.Util.asGuidByteArray; null uses a zero length byte.8-4-4-4-12layout before callingUUID.fromString, while preserving SQL Server's accepted braces/suffixes and legacy precision constraints.SQLServerExceptionthroughR_errorConvertingValue, retaining the parsing cause. Invalid metadata is rejected before row transmission.Path selection
microsoft.sql.Types.GUIDSQLServerResultSetexecuteBatch()/executeLargeBatch()withuseBulkCopyForBatchInsert=truesetString()inputGUID-to-character destinations and encrypted sources/destinations retain their existing paths. Encrypted-value-modification handling is not expanded. No connection property is introduced.
For SQL Server ResultSets, public
ResultSetMetaData.getColumnType()remains unchanged: GUID is still exposed as JDBC CHAR. Native selection is per destination, allowing the same source column to map to both GUID and character destinations. Current-source eligibility avoids retaining a stale native GUID decision when the bulk-copy object is reused. This is a narrow eligibility fix, not a general source-schema cache refresh: cached precision, scale, names, and other metadata retain their existing behavior.Fixes existing GitHub issue
Fixes #2986
New Public APIs
None. Updated
writeToServer(ResultSet)Javadoc describes native selection and retained fallbacks. Getter/setter/updater conversion mappings are unchanged.Compatibility behavior
Regression tests compare the native GUID path with the legacy character path and assert expected stored values or rejection, not merely agreement. The previous GUID character-wire path was also temporarily restored during the earlier investigation to confirm precision behavior.
Here,
GUIDmeans a complete 36-character canonical GUID:{GUID}{GUID}followed by text or spacesThe former
CHAR(n)metadata range of 1 through 8000 remains enforced, including null rows. Full input must fit before suffix processing. This is a legacy compatibility restriction, not the native GUID wire size. Exact length alone is insufficient: Java accepts some malformed 36-character inputs, so the parser's format checks remain necessary.Outstanding transaction compatibility decision
Invalid inputs that previously failed on the server can now fail on the client. The reported caller-owned transaction regression remains open: this can change whether earlier work is committed by a subsequent
commit(). See the original reproduction and the outstanding review comment.This PR currently defers this policy decision. It does not introduce an opt-in, force rollback of caller-owned transactions, or claim complete failure-semantics compatibility. Native conversion failures remain driver
SQLServerExceptions and surface asBatchUpdateExceptionthrough the batch APIs.CSV bulk load
SQLServerBulkCSVFileRecordsupplies strings. DeclaringaddColumnMetadata(..., microsoft.sql.Types.GUID, precision, 0)selects native GUID transfer into a plaintext uniqueidentifier destination. Declaring CHAR retains character transfer. Empty CSV fields remain SQL NULL, unlike an empty String from a custom record.CSV round-trip/compatibility tests do not require Extended Events permissions and are not excluded from Azure SQL Database.
Verification
Before rebasing, combined validation passed under both
jre11andjre8Maven profiles on JDK 21: 449 tests per profile, zero failures/errors/skips. After rebasing ontomainand removing the explanatory Markdown document, the same combined suite passed again underjre11. Thejre8profile was not rerun after the rebase. These runs are not execution on an actual JDK 8 runtime or a full repository suite pass.Coverage:
BulkCopyGuidParserTest: 33 unit cases, including exactly-36-character malformed group lengths.BulkCopyGuidMetadataTest: 11 cases for per-destination eligibility, current ResultSet metadata, conflicting cached/current bulk-data types, active ResultSet precedence over retained bulk data, fallback guards, exact mixed-endian payload bytes, and null encoding.BulkCopyGuidTest: 375 integration cases covering custom-record/CSV compatibility; ResultSet GUID/CHAR/VARCHAR/VARCHAR(MAX) sources into GUID and character destinations; forward-only/scroll-insensitive cursors; first-row nulls; mixed columns; reordered/duplicate mappings; batch boundaries; source reuse; native command declarations and server-side conversion checks. Custom-record and CSV reuse is covered in both GUID-to-CHAR and CHAR-to-GUID order, asserting declarations, stored values, and malformed-input error origin.BulkCopyGuidBatchInsertTest: 26 cases covering both APIs, bulk/non-bulk successful-value parity, string/UUID/GUID/null bindings, mixed columns, statement reuse, bulk options, malformed values, and bulk precision failures.BulkCopyAllTypesTest: 4 existing cases.Native declaration assertions run without Extended Events permissions. Server conversion assertions use character sources as positive controls and require suitable server permissions. They ran without skips in the pre-rebase validation.
Before implementation, the native ResultSet conversion test failed while its character controls passed. The six added custom-record/CSV source-reuse regression cases also failed before the current-bulk-data eligibility fix and passed afterward. These fixes preserve general source metadata caching without resets. Changed Java was formatted with the repository Eclipse profile.
Remaining validation limits
BulkCopyGuidAETestwas attempted during the core follow-up, but shared AE setup fails becausetarget/test-classes/JavaKeyStore.txtis missing. Existing encrypted-source/destination coverage is retained; no AE execution pass is claimed.executeBatch;executeLargeBatchuses destination order because its existing reordered-column mapping limitation is outside this change.