Extract SqlBulkCopy column names using dynamic SQL#4092
Extract SqlBulkCopy column names using dynamic SQL#4092paulmedynski wants to merge 7 commits intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates SqlBulkCopy’s target-column discovery query to use dynamic SQL so that referencing sys.all_columns.graph_type doesn’t cause compilation failures on SQL Server 2016, addressing regression #3714.
Changes:
- Reworks
SqlBulkCopy.CreateInitialQuery()to build and execute thesys.all_columnscolumn-name query viasp_executesql. - Adjusts manual test SQL statistics expectations to account for the additional statements executed.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs | Updates expected SelectCount / SelectRows due to extra statements from dynamic SQL. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs | Uses dynamic SQL for column-name extraction to avoid SQL 2016 compilation errors when graph_type is absent. |
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs
Show resolved
Hide resolved
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4092 +/- ##
==========================================
- Coverage 73.22% 66.51% -6.72%
==========================================
Files 280 274 -6
Lines 43000 65785 +22785
==========================================
+ Hits 31486 43754 +12268
- Misses 11514 22031 +10517
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs
Show resolved
Hide resolved
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/SqlGraphTables.cs
Show resolved
Hide resolved
5ef395f to
1be29d6
Compare
src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs
Show resolved
Hide resolved
|
/azp run |
|
Azure Pipelines successfully started running 2 pipeline(s). |
| IF EXISTS (SELECT TOP 1 * FROM sys.all_columns WHERE [object_id] = OBJECT_ID('sys.all_columns') AND [name] = 'graph_type') | ||
| BEGIN | ||
| SELECT @Column_Names = COALESCE(@Column_Names + ', ', '') + QUOTENAME([name]) FROM {CatalogName}.[sys].[all_columns] WHERE [object_id] = OBJECT_ID('{escapedObjectName}') AND COALESCE([graph_type], 0) NOT IN (1, 3, 4, 6, 7) ORDER BY [column_id] ASC; | ||
| SET @Column_Name_Query = N'SELECT @Column_Names = COALESCE(@Column_Names + '', '', '''') + QUOTENAME([name]) FROM {CatalogName}.[sys].[all_columns] WHERE [object_id] = @Object_ID AND COALESCE([graph_type], 0) NOT IN (1, 3, 4, 6, 7) ORDER BY [column_id] ASC;'; |
There was a problem hiding this comment.
Suggestion: It might be worth adding a brief inline comment here explaining why CatalogName is interpolated directly into the dynamic SQL string rather than parameterized. SQL doesn't allow parameterizing identifiers (table/schema/catalog names), so this is the correct approach — but without context, future reviewers may flag it as a potential SQL injection vector.
There was a problem hiding this comment.
Done in 70269ce — added an inline comment block explaining that CatalogName and escapedObjectName are interpolated directly because SQL Server doesn't allow parameterizing identifiers, and noting both values are pre-escaped via SqlServerEscapeHelper.
priyankatiwari08
left a comment
There was a problem hiding this comment.
Looks good to me, Just added a suggestion to add comment related to explaining why CatalogName is interpolated directly into the dynamic SQL string rather than parameterized
70269ce
|
Addressed both open review comments in 70269ce:
|
This enables the query to compile correctly when the graph_type column is missing
- Fix SQLServerVersion lazy-init: change field initializer from string.Empty to null so the ??= null-coalescing assignment actually triggers. This makes IsAtLeastSQL2017/IsAtLeastSQL2019 work correctly. - Add inline comment explaining why CatalogName and escapedObjectName are interpolated directly into the dynamic SQL string (SQL Server does not allow parameterizing identifiers).
70269ce to
29a3aff
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTestWithTdsServer.cs:97
ForceEncryptionRegistryPathno longer handles SQL Server 2016 (major 13). On SQL 2016 it will returnstring.Empty, andDispose()later passes that value toRemoveForceEncryptionFromRegistryPath, which may end up opening/setting values on an unintended registry key (or throwing). Add anIsSQL2016()branch that returns the MSSQL13 registry path, and/or defensively avoid calling registry cleanup when the path is empty.
if (DataTestUtility.IsSQL2022())
{
return $@"SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.{s_instanceName}\MSSQLSERVER\SuperSocketNetLib";
}
if (DataTestUtility.IsSQL2019())
{
return $@"SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL15.{s_instanceName}\MSSQLSERVER\SuperSocketNetLib";
}
if (DataTestUtility.IsSQL2017())
{
return $@"SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL14.{s_instanceName}\MSSQLSERVER\SuperSocketNetLib";
}
return string.Empty;
}
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTest.cs:59
ForceEncryptionRegistryPathno longer has an SQL Server 2016 (major 13) mapping; it falls back tostring.Empty. This means the test will skip certificate/registry cleanup inDispose()on SQL 2016 and can leave machine-level state behind. Add anIsSQL2016()branch returning the MSSQL13 registry path so cleanup works on SQL 2016 as well.
if (DataTestUtility.IsSQL2022())
{
return $@"SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.{InstanceName}\MSSQLSERVER\SuperSocketNetLib";
}
if (DataTestUtility.IsSQL2019())
{
return $@"SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL15.{InstanceName}\MSSQLSERVER\SuperSocketNetLib";
}
if (DataTestUtility.IsSQL2017())
{
return $@"SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL14.{InstanceName}\MSSQLSERVER\SuperSocketNetLib";
}
return string.Empty;
}
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs
Show resolved
Hide resolved
| public static bool IsSQL2022() => string.Equals("16", SQLServerVersion.Trim()); | ||
|
|
||
| public static bool IsSQL2019() => string.Equals("15", SQLServerVersion.Trim()); | ||
|
|
||
| public static bool IsSQL2016() => string.Equals("14", s_sQLServerVersion.Trim()); | ||
| public static bool IsSQL2017() => string.Equals("14", SQLServerVersion.Trim()); | ||
|
|
||
| public static bool IsSQL2016() => string.Equals("13", SQLServerVersion.Trim()); | ||
|
|
There was a problem hiding this comment.
IsSQL2022/IsSQL2019/IsSQL2017/IsSQL2016 call SQLServerVersion.Trim() without a null guard. After changing the cached version field to be nullable, SQLServerVersion can now be null (e.g., when TCPConnectionString is empty), which will throw NullReferenceException. Make these helpers null-safe (e.g., use SQLServerVersion?.Trim() or parse like the new IsAtLeastSQL* helpers).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTestWithTdsServer.cs:96
- ForceEncryptionRegistryPath no longer returns a path for SQL Server 2016 (major 13). On SQL 2016 this will return string.Empty, but Dispose() later calls RemoveForceEncryptionFromRegistryPath(ForceEncryptionRegistryPath) without checking for empty, which can lead to attempting to open/set values on the HKLM root key (or throwing). Add an IsSQL2016() branch mapping to MSSQL13.* or guard against empty before using the path.
if (DataTestUtility.IsSQL2017())
{
return $@"SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL14.{s_instanceName}\MSSQLSERVER\SuperSocketNetLib";
}
return string.Empty;
| // OBJECT_ID will return NULL and @Column_Names will remain non-null. The subsequent | ||
| // SELECT * query will then continue to fail with "Invalid object name" rather than with | ||
| // an unusual error because the query being executed is NULL. |
There was a problem hiding this comment.
The comment says "@Column_Names will remain non-null" when OBJECT_ID returns NULL, but @Column_Names is declared as NULL and will remain NULL until later COALESCE assigns ''. Update the comment to reflect the actual behavior (i.e., it remains NULL and is then set to '').
| // OBJECT_ID will return NULL and @Column_Names will remain non-null. The subsequent | |
| // SELECT * query will then continue to fail with "Invalid object name" rather than with | |
| // an unusual error because the query being executed is NULL. | |
| // OBJECT_ID will return NULL and @Column_Names will remain NULL. Later, a COALESCE | |
| // expression sets @Column_Names to '*', so the subsequent SELECT * query will still | |
| // fail with "Invalid object name" rather than with an unusual error because the query | |
| // being executed is NULL. |
src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectionTestWithSSLCert/CertificateTest.cs
Outdated
Show resolved
Hide resolved
|
/azp run |
|
Azure Pipelines successfully started running 2 pipeline(s). |
Description
#3590 allowed SqlBulkCopy to find and work with hidden columns (even if those columns weren't accessible server-side.) It contained a check on the
all_columnsDMV to make sure that we only checked thegraph_typecolumn when querying if that column existed; this was to maintain SQL 2016 compatibility.#3714 highlighted that both queries are compiled and fail at the point of compilation anyway, so this wasn't accomplishing anything. To make this work, we need to use dynamic SQL to run the column queries. This PR does so.
Supersedes #3719. Original PR by @edwardneal. This PR was created as a direct branch of the SqlClient repo so I can run the new SQL 16 and 17 CI pipeline stages manually to prove this fixes the issue.
Issues
Fixes #3714.
Testing
Manual testing of all test cases against a SQL 2016 instance (via the updated CI pipelines).