Skip to content

Commit b2fc7e0

Browse files
authored
Fix | Allow SqlBulkCopy to operate on hidden columns (#3590)
1 parent d5ba064 commit b2fc7e0

File tree

5 files changed

+160
-23
lines changed

5 files changed

+160
-23
lines changed

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,6 @@ private string CreateInitialQuery()
420420
{
421421
throw SQL.BulkLoadInvalidDestinationTable(DestinationTableName, null);
422422
}
423-
string TDSCommand;
424-
425-
TDSCommand = "select @@trancount; SET FMTONLY ON select * from " + ADP.BuildMultiPartName(parts) + " SET FMTONLY OFF ";
426423

427424
string TableCollationsStoredProc;
428425
if (_connection.Is2008OrNewer)
@@ -456,27 +453,41 @@ private string CreateInitialQuery()
456453
string CatalogName = parts[MultipartIdentifier.CatalogIndex];
457454
if (isTempTable && string.IsNullOrEmpty(CatalogName))
458455
{
459-
TDSCommand += string.Format("exec tempdb..{0} N'{1}.{2}'",
460-
TableCollationsStoredProc,
461-
SchemaName,
462-
TableName
463-
);
456+
CatalogName = "tempdb";
464457
}
465-
else
458+
else if (!string.IsNullOrEmpty(CatalogName))
466459
{
467-
// Escape the catalog name
468-
if (!string.IsNullOrEmpty(CatalogName))
469-
{
470-
CatalogName = SqlServerEscapeHelper.EscapeIdentifier(CatalogName);
471-
}
472-
TDSCommand += string.Format("exec {0}..{1} N'{2}.{3}'",
473-
CatalogName,
474-
TableCollationsStoredProc,
475-
SchemaName,
476-
TableName
477-
);
460+
CatalogName = SqlServerEscapeHelper.EscapeIdentifier(CatalogName);
478461
}
479-
return TDSCommand;
462+
463+
string objectName = ADP.BuildMultiPartName(parts);
464+
string escapedObjectName = SqlServerEscapeHelper.EscapeStringAsLiteral(objectName);
465+
// Specify the column names explicitly. This is to ensure that we can map to hidden columns (e.g. columns in temporal tables.)
466+
// If the target table doesn't exist, OBJECT_ID will return NULL and @Column_Names will remain non-null. The subsequent SELECT *
467+
// query will then continue to fail with "Invalid object name" rather than with an unusual error because the query being executed
468+
// is NULL.
469+
// Some hidden columns (e.g. SQL Graph columns) cannot be selected, so we need to exclude them explicitly.
470+
return $"""
471+
SELECT @@TRANCOUNT;
472+
473+
DECLARE @Column_Names NVARCHAR(MAX) = NULL;
474+
IF EXISTS (SELECT TOP 1 * FROM sys.all_columns WHERE [object_id] = OBJECT_ID('sys.all_columns') AND [name] = 'graph_type')
475+
BEGIN
476+
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;
477+
END
478+
ELSE
479+
BEGIN
480+
SELECT @Column_Names = COALESCE(@Column_Names + ', ', '') + QUOTENAME([name]) FROM {CatalogName}.[sys].[all_columns] WHERE [object_id] = OBJECT_ID('{escapedObjectName}') ORDER BY [column_id] ASC;
481+
END
482+
483+
SELECT @Column_Names = COALESCE(@Column_Names, '*');
484+
485+
SET FMTONLY ON;
486+
EXEC(N'SELECT ' + @Column_Names + N' FROM {escapedObjectName}');
487+
SET FMTONLY OFF;
488+
489+
EXEC {CatalogName}..{TableCollationsStoredProc} N'{SchemaName}.{TableName}';
490+
""";
480491
}
481492

482493
// Creates and then executes initial query to get information about the targettable

src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,12 @@
143143
<Compile Include="SQL\SqlBulkCopyTest\CopyWithEventAsync.cs" />
144144
<Compile Include="SQL\SqlBulkCopyTest\FireTrigger.cs" />
145145
<Compile Include="SQL\SqlBulkCopyTest\Helpers.cs" />
146+
<Compile Include="SQL\SqlBulkCopyTest\HiddenTargetColumn.cs" />
146147
<Compile Include="SQL\SqlBulkCopyTest\MissingTargetColumn.cs" />
147148
<Compile Include="SQL\SqlBulkCopyTest\MissingTargetColumns.cs" />
148149
<Compile Include="SQL\SqlBulkCopyTest\MissingTargetTable.cs" />
149150
<Compile Include="SQL\SqlBulkCopyTest\SqlBulkCopyTest.cs" />
151+
<Compile Include="SQL\SqlBulkCopyTest\SqlGraphTables.cs" />
150152
<Compile Include="SQL\SqlBulkCopyTest\TestBulkCopyWithUTF8.cs" />
151153
<Compile Include="SQL\SqlBulkCopyTest\Transaction.cs" />
152154
<Compile Include="SQL\SqlBulkCopyTest\Transaction1.cs" />

src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ public static void Test(string srcConstr, string dstConstr, string dstTable)
6161
DataTestUtility.AssertEqualsWithDescription((long)3, stats["BuffersReceived"], "Unexpected BuffersReceived value.");
6262
DataTestUtility.AssertEqualsWithDescription((long)3, stats["BuffersSent"], "Unexpected BuffersSent value.");
6363
DataTestUtility.AssertEqualsWithDescription((long)0, stats["IduCount"], "Unexpected IduCount value.");
64-
DataTestUtility.AssertEqualsWithDescription((long)3, stats["SelectCount"], "Unexpected SelectCount value.");
64+
DataTestUtility.AssertEqualsWithDescription((long)6, stats["SelectCount"], "Unexpected SelectCount value.");
6565
DataTestUtility.AssertEqualsWithDescription((long)3, stats["ServerRoundtrips"], "Unexpected ServerRoundtrips value.");
66-
DataTestUtility.AssertEqualsWithDescription((long)4, stats["SelectRows"], "Unexpected SelectRows value.");
66+
DataTestUtility.AssertEqualsWithDescription((long)9, stats["SelectRows"], "Unexpected SelectRows value.");
6767
DataTestUtility.AssertEqualsWithDescription((long)2, stats["SumResultSets"], "Unexpected SumResultSets value.");
6868
DataTestUtility.AssertEqualsWithDescription((long)0, stats["Transactions"], "Unexpected Transactions value.");
6969
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Data.Common;
7+
using Xunit;
8+
9+
namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SqlBulkCopyTests
10+
{
11+
public class HiddenTargetColumn
12+
{
13+
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))]
14+
public void WriteToServer_CopyToHiddenTargetColumn_ThrowsSqlException()
15+
{
16+
string connectionString = DataTestUtility.TCPConnectionString;
17+
string destinationTable = DataTestUtility.GetShortName("HiddenTargetColumn");
18+
string destinationHistoryTable = DataTestUtility.GetShortName("HiddenTargetColumn_History");
19+
20+
using (SqlConnection dstConn = new SqlConnection(connectionString))
21+
using (SqlCommand dstCmd = dstConn.CreateCommand())
22+
{
23+
dstConn.Open();
24+
25+
try
26+
{
27+
DataTestUtility.CreateTable(dstConn, destinationTable, $"""
28+
(
29+
Column1 int primary key not null,
30+
Column2 nvarchar(10) not null,
31+
[Employee's First Name] varchar(max) null,
32+
ValidFrom datetime2 generated always as row start hidden not null,
33+
ValidTo datetime2 generated always as row end hidden not null,
34+
period for system_time (ValidFrom, ValidTo)
35+
)
36+
with (system_versioning = on(history_table = dbo.{destinationHistoryTable}));
37+
""");
38+
39+
using (SqlConnection srcConn = new SqlConnection(connectionString))
40+
using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, FirstName, LastName, HireDate, sysdatetime() as CurrentDate from employees", srcConn))
41+
{
42+
srcConn.Open();
43+
44+
using (DbDataReader reader = srcCmd.ExecuteReader())
45+
using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn))
46+
{
47+
bulkcopy.DestinationTableName = destinationTable;
48+
SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings;
49+
50+
ColumnMappings.Add("EmployeeID", "Column1");
51+
ColumnMappings.Add("LastName", "Column2");
52+
ColumnMappings.Add("FirstName", "Employee's First Name");
53+
ColumnMappings.Add("HireDate", "ValidFrom");
54+
ColumnMappings.Add("CurrentDate", "ValidTo");
55+
56+
SqlException sqlEx = Assert.Throws<SqlException>(() => bulkcopy.WriteToServer(reader));
57+
58+
Assert.Equal(13536, sqlEx.Number);
59+
Assert.StartsWith("Cannot insert an explicit value into a GENERATED ALWAYS column in table", sqlEx.Message);
60+
}
61+
}
62+
}
63+
finally
64+
{
65+
DataTestUtility.RunNonQuery(connectionString, $"""
66+
alter table {destinationTable} set (system_versioning = off);
67+
alter table {destinationTable} drop period for system_time;
68+
""");
69+
DataTestUtility.DropTable(dstConn, destinationTable);
70+
DataTestUtility.DropTable(dstConn, destinationHistoryTable);
71+
}
72+
}
73+
}
74+
}
75+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Data;
7+
using System.Data.Common;
8+
using Xunit;
9+
10+
namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SqlBulkCopyTests
11+
{
12+
public class SqlGraphTables
13+
{
14+
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))]
15+
public void WriteToServer_CopyToSqlGraphNodeTable_Succeeds()
16+
{
17+
string connectionString = DataTestUtility.TCPConnectionString;
18+
string destinationTable = DataTestUtility.GetShortName("SqlGraphNodeTable");
19+
20+
using SqlConnection dstConn = new SqlConnection(connectionString);
21+
using DataTable nodes = new DataTable()
22+
{
23+
Columns = { new DataColumn("Name", typeof(string)) }
24+
};
25+
26+
dstConn.Open();
27+
28+
for (int i = 0; i < 5; i++)
29+
{
30+
nodes.Rows.Add($"Name {i}");
31+
}
32+
33+
try
34+
{
35+
DataTestUtility.CreateTable(dstConn, destinationTable, "(Id INT PRIMARY KEY IDENTITY(1,1), [Name] VARCHAR(100)) AS NODE");
36+
37+
using SqlBulkCopy nodeCopy = new SqlBulkCopy(dstConn);
38+
39+
nodeCopy.DestinationTableName = destinationTable;
40+
nodeCopy.ColumnMappings.Add("Name", "Name");
41+
nodeCopy.WriteToServer(nodes);
42+
}
43+
finally
44+
{
45+
DataTestUtility.DropTable(dstConn, destinationTable);
46+
}
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)