diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs
index cdcd02efea..8f9836f3ab 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs
@@ -420,9 +420,6 @@ private string CreateInitialQuery()
{
throw SQL.BulkLoadInvalidDestinationTable(DestinationTableName, null);
}
- string TDSCommand;
-
- TDSCommand = "select @@trancount; SET FMTONLY ON select * from " + ADP.BuildMultiPartName(parts) + " SET FMTONLY OFF ";
string TableCollationsStoredProc;
if (_connection.Is2008OrNewer)
@@ -456,27 +453,41 @@ private string CreateInitialQuery()
string CatalogName = parts[MultipartIdentifier.CatalogIndex];
if (isTempTable && string.IsNullOrEmpty(CatalogName))
{
- TDSCommand += string.Format("exec tempdb..{0} N'{1}.{2}'",
- TableCollationsStoredProc,
- SchemaName,
- TableName
- );
+ CatalogName = "tempdb";
}
- else
+ else if (!string.IsNullOrEmpty(CatalogName))
{
- // Escape the catalog name
- if (!string.IsNullOrEmpty(CatalogName))
- {
- CatalogName = SqlServerEscapeHelper.EscapeIdentifier(CatalogName);
- }
- TDSCommand += string.Format("exec {0}..{1} N'{2}.{3}'",
- CatalogName,
- TableCollationsStoredProc,
- SchemaName,
- TableName
- );
+ CatalogName = SqlServerEscapeHelper.EscapeIdentifier(CatalogName);
}
- return TDSCommand;
+
+ string objectName = ADP.BuildMultiPartName(parts);
+ string escapedObjectName = SqlServerEscapeHelper.EscapeStringAsLiteral(objectName);
+ // Specify the column names explicitly. This is to ensure that we can map to hidden columns (e.g. columns in temporal tables.)
+ // If the target table doesn't exist, 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.
+ // Some hidden columns (e.g. SQL Graph columns) cannot be selected, so we need to exclude them explicitly.
+ return $"""
+SELECT @@TRANCOUNT;
+
+DECLARE @Column_Names NVARCHAR(MAX) = NULL;
+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;
+END
+ELSE
+BEGIN
+ SELECT @Column_Names = COALESCE(@Column_Names + ', ', '') + QUOTENAME([name]) FROM {CatalogName}.[sys].[all_columns] WHERE [object_id] = OBJECT_ID('{escapedObjectName}') ORDER BY [column_id] ASC;
+END
+
+SELECT @Column_Names = COALESCE(@Column_Names, '*');
+
+SET FMTONLY ON;
+EXEC(N'SELECT ' + @Column_Names + N' FROM {escapedObjectName}');
+SET FMTONLY OFF;
+
+EXEC {CatalogName}..{TableCollationsStoredProc} N'{SchemaName}.{TableName}';
+""";
}
// Creates and then executes initial query to get information about the targettable
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj
index 94819c9bb4..981f035965 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj
@@ -143,10 +143,12 @@
+
+
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs
index beb8df7992..5ba727be5d 100644
--- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/CopyAllFromReader.cs
@@ -61,9 +61,9 @@ public static void Test(string srcConstr, string dstConstr, string dstTable)
DataTestUtility.AssertEqualsWithDescription((long)3, stats["BuffersReceived"], "Unexpected BuffersReceived value.");
DataTestUtility.AssertEqualsWithDescription((long)3, stats["BuffersSent"], "Unexpected BuffersSent value.");
DataTestUtility.AssertEqualsWithDescription((long)0, stats["IduCount"], "Unexpected IduCount value.");
- DataTestUtility.AssertEqualsWithDescription((long)3, stats["SelectCount"], "Unexpected SelectCount value.");
+ DataTestUtility.AssertEqualsWithDescription((long)6, stats["SelectCount"], "Unexpected SelectCount value.");
DataTestUtility.AssertEqualsWithDescription((long)3, stats["ServerRoundtrips"], "Unexpected ServerRoundtrips value.");
- DataTestUtility.AssertEqualsWithDescription((long)4, stats["SelectRows"], "Unexpected SelectRows value.");
+ DataTestUtility.AssertEqualsWithDescription((long)9, stats["SelectRows"], "Unexpected SelectRows value.");
DataTestUtility.AssertEqualsWithDescription((long)2, stats["SumResultSets"], "Unexpected SumResultSets value.");
DataTestUtility.AssertEqualsWithDescription((long)0, stats["Transactions"], "Unexpected Transactions value.");
}
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/HiddenTargetColumn.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/HiddenTargetColumn.cs
new file mode 100644
index 0000000000..1322120b55
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/HiddenTargetColumn.cs
@@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Data.Common;
+using Xunit;
+
+namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SqlBulkCopyTests
+{
+ public class HiddenTargetColumn
+ {
+ [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))]
+ public void WriteToServer_CopyToHiddenTargetColumn_ThrowsSqlException()
+ {
+ string connectionString = DataTestUtility.TCPConnectionString;
+ string destinationTable = DataTestUtility.GetShortName("HiddenTargetColumn");
+ string destinationHistoryTable = DataTestUtility.GetShortName("HiddenTargetColumn_History");
+
+ using (SqlConnection dstConn = new SqlConnection(connectionString))
+ using (SqlCommand dstCmd = dstConn.CreateCommand())
+ {
+ dstConn.Open();
+
+ try
+ {
+ DataTestUtility.CreateTable(dstConn, destinationTable, $"""
+(
+ Column1 int primary key not null,
+ Column2 nvarchar(10) not null,
+ [Employee's First Name] varchar(max) null,
+ ValidFrom datetime2 generated always as row start hidden not null,
+ ValidTo datetime2 generated always as row end hidden not null,
+ period for system_time (ValidFrom, ValidTo)
+)
+with (system_versioning = on(history_table = dbo.{destinationHistoryTable}));
+""");
+
+ using (SqlConnection srcConn = new SqlConnection(connectionString))
+ using (SqlCommand srcCmd = new SqlCommand("select top 5 EmployeeID, FirstName, LastName, HireDate, sysdatetime() as CurrentDate from employees", srcConn))
+ {
+ srcConn.Open();
+
+ using (DbDataReader reader = srcCmd.ExecuteReader())
+ using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn))
+ {
+ bulkcopy.DestinationTableName = destinationTable;
+ SqlBulkCopyColumnMappingCollection ColumnMappings = bulkcopy.ColumnMappings;
+
+ ColumnMappings.Add("EmployeeID", "Column1");
+ ColumnMappings.Add("LastName", "Column2");
+ ColumnMappings.Add("FirstName", "Employee's First Name");
+ ColumnMappings.Add("HireDate", "ValidFrom");
+ ColumnMappings.Add("CurrentDate", "ValidTo");
+
+ SqlException sqlEx = Assert.Throws(() => bulkcopy.WriteToServer(reader));
+
+ Assert.Equal(13536, sqlEx.Number);
+ Assert.StartsWith("Cannot insert an explicit value into a GENERATED ALWAYS column in table", sqlEx.Message);
+ }
+ }
+ }
+ finally
+ {
+ DataTestUtility.RunNonQuery(connectionString, $"""
+alter table {destinationTable} set (system_versioning = off);
+alter table {destinationTable} drop period for system_time;
+""");
+ DataTestUtility.DropTable(dstConn, destinationTable);
+ DataTestUtility.DropTable(dstConn, destinationHistoryTable);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/SqlGraphTables.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/SqlGraphTables.cs
new file mode 100644
index 0000000000..d83693080f
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/SqlGraphTables.cs
@@ -0,0 +1,49 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Data;
+using System.Data.Common;
+using Xunit;
+
+namespace Microsoft.Data.SqlClient.ManualTesting.Tests.SqlBulkCopyTests
+{
+ public class SqlGraphTables
+ {
+ [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))]
+ public void WriteToServer_CopyToSqlGraphNodeTable_Succeeds()
+ {
+ string connectionString = DataTestUtility.TCPConnectionString;
+ string destinationTable = DataTestUtility.GetShortName("SqlGraphNodeTable");
+
+ using SqlConnection dstConn = new SqlConnection(connectionString);
+ using DataTable nodes = new DataTable()
+ {
+ Columns = { new DataColumn("Name", typeof(string)) }
+ };
+
+ dstConn.Open();
+
+ for (int i = 0; i < 5; i++)
+ {
+ nodes.Rows.Add($"Name {i}");
+ }
+
+ try
+ {
+ DataTestUtility.CreateTable(dstConn, destinationTable, "(Id INT PRIMARY KEY IDENTITY(1,1), [Name] VARCHAR(100)) AS NODE");
+
+ using SqlBulkCopy nodeCopy = new SqlBulkCopy(dstConn);
+
+ nodeCopy.DestinationTableName = destinationTable;
+ nodeCopy.ColumnMappings.Add("Name", "Name");
+ nodeCopy.WriteToServer(nodes);
+ }
+ finally
+ {
+ DataTestUtility.DropTable(dstConn, destinationTable);
+ }
+ }
+ }
+}