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 414be57eb0..ed9813389c 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs
@@ -477,11 +477,12 @@ private string CreateInitialQuery()
}
else if (!string.IsNullOrEmpty(CatalogName))
{
- CatalogName = SqlServerEscapeHelper.EscapeStringAsLiteral(SqlServerEscapeHelper.EscapeIdentifier(CatalogName));
+ CatalogName = SqlServerEscapeHelper.EscapeIdentifier(CatalogName);
}
string objectName = ADP.BuildMultiPartName(parts);
string escapedObjectName = SqlServerEscapeHelper.EscapeStringAsLiteral(objectName);
+ string catalogNameStringLiteral = CatalogName is null ? null : SqlServerEscapeHelper.EscapeStringAsLiteral(CatalogName);
// 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
@@ -526,6 +527,11 @@ private string CreateInitialQuery()
// we use STRING_AGG in that case and the COALESCE method otherwise.
//
// See: https://learn.microsoft.com/en-us/sql/t-sql/functions/serverproperty-transact-sql
+ //
+ // All of this is wrapped in an test against HAS_PERMS_BY_NAME. This test verifies that
+ // the user possesses the necessary permissions to access sys.all_columns. If they do not
+ // @Column_Names will remain NULL (and be coalesced to *) and SqlBulkCopy will degrade
+ // gracefully, silently dropping support for hidden columns and column aliases.
return $"""
SELECT @@TRANCOUNT;
@@ -535,6 +541,7 @@ private string CreateInitialQuery()
DECLARE @Column_Name_Query_SORT NVARCHAR(MAX);
DECLARE @Column_Name_Query NVARCHAR(MAX);
DECLARE @Column_Names NVARCHAR(MAX) = NULL;
+DECLARE @Has_Permissions INT = HAS_PERMS_BY_NAME('{catalogNameStringLiteral}.[sys].[all_columns]', 'OBJECT', 'SELECT');
CREATE TABLE #Column_Aliases
(
@@ -554,28 +561,35 @@ IF CAST(SERVERPROPERTY('EngineEdition') AS INT) = 6
SET @Column_Name_Query_SORT = N'ORDER BY [column_id] ASC';
END
-IF EXISTS (SELECT TOP 1 * FROM sys.all_columns WHERE [object_id] = OBJECT_ID('sys.all_columns') AND [name] = 'graph_type')
-BEGIN
- SET @Column_Name_Query_FILTER = N'WHERE [object_id] = @Object_ID AND COALESCE([graph_type], 0) NOT IN (1, 3, 4, 6, 7)';
-
- EXEC sp_executesql N'
- INSERT INTO #Column_Aliases ([Canonical_Column_Name], [Canonical_Column_Id], [Aliased_Column_Name])
- SELECT [name], [column_id], ''$to_id'' FROM {CatalogName}.[sys].[all_columns] WHERE [object_id] = @Object_ID AND COALESCE([graph_type], 0) = 8
- UNION ALL
- SELECT [name], [column_id], ''$from_id'' FROM {CatalogName}.[sys].[all_columns] WHERE [object_id] = @Object_ID AND COALESCE([graph_type], 0) = 5
- UNION ALL
- SELECT [name], [column_id], ''$edge_id'' FROM {CatalogName}.[sys].[all_columns] WHERE [object_id] = @Object_ID AND COALESCE([graph_type], 0) = 2 AND [name] LIKE ''$edge[_]id[_]%''
- UNION ALL
- SELECT [name], [column_id], ''$node_id'' FROM {CatalogName}.[sys].[all_columns] WHERE [object_id] = @Object_ID AND COALESCE([graph_type], 0) = 2 AND [name] LIKE ''$node[_]id[_]%''',
- N'@Object_ID INT', @Object_ID = @Object_ID
-END
-ELSE
+IF @Has_Permissions = 1
BEGIN
- SET @Column_Name_Query_FILTER = N'WHERE [object_id] = @Object_ID';
+ IF EXISTS (SELECT TOP 1 * FROM {CatalogName}.[sys].[all_columns] WHERE [object_id] = OBJECT_ID('{catalogNameStringLiteral}.[sys].[all_columns]') AND [name] = 'graph_type')
+ BEGIN
+ SET @Column_Name_Query_FILTER = N'WHERE [object_id] = @Object_ID AND COALESCE([graph_type], 0) NOT IN (1, 3, 4, 6, 7)';
+
+ EXEC sp_executesql N'
+ INSERT INTO #Column_Aliases ([Canonical_Column_Name], [Canonical_Column_Id], [Aliased_Column_Name])
+ SELECT [name], [column_id], ''$to_id'' FROM {catalogNameStringLiteral}.[sys].[all_columns] WHERE [object_id] = @Object_ID AND COALESCE([graph_type], 0) = 8
+ UNION ALL
+ SELECT [name], [column_id], ''$from_id'' FROM {catalogNameStringLiteral}.[sys].[all_columns] WHERE [object_id] = @Object_ID AND COALESCE([graph_type], 0) = 5
+ UNION ALL
+ SELECT [name], [column_id], ''$edge_id'' FROM {catalogNameStringLiteral}.[sys].[all_columns] WHERE [object_id] = @Object_ID AND COALESCE([graph_type], 0) = 2 AND [name] LIKE ''$edge[_]id[_]%''
+ UNION ALL
+ SELECT [name], [column_id], ''$node_id'' FROM {catalogNameStringLiteral}.[sys].[all_columns] WHERE [object_id] = @Object_ID AND COALESCE([graph_type], 0) = 2 AND [name] LIKE ''$node[_]id[_]%''',
+ N'@Object_ID INT', @Object_ID = @Object_ID
+ END
+ ELSE
+ BEGIN
+ SET @Column_Name_Query_FILTER = N'WHERE [object_id] = @Object_ID';
+ END
+ SET @Column_Name_Query = @Column_Name_Query_SELECT + ' FROM {catalogNameStringLiteral}.[sys].[all_columns] ' + @Column_Name_Query_FILTER + ' ' + @Column_Name_Query_SORT + ';'
+
+ EXEC sp_executesql @Column_Name_Query, N'@Object_ID INT, @Column_Names NVARCHAR(MAX) OUTPUT', @Object_ID = @Object_ID, @Column_Names = @Column_Names OUTPUT;
+
+ DELETE FROM #Column_Aliases
+ WHERE [Aliased_Column_Name] IN (SELECT [name] FROM {CatalogName}.[sys].[all_columns] WHERE [object_id] = @Object_ID)
END
-SET @Column_Name_Query = @Column_Name_Query_SELECT + ' FROM {CatalogName}.[sys].[all_columns] ' + @Column_Name_Query_FILTER + ' ' + @Column_Name_Query_SORT + ';'
-EXEC sp_executesql @Column_Name_Query, N'@Object_ID INT, @Column_Names NVARCHAR(MAX) OUTPUT', @Object_ID = @Object_ID, @Column_Names = @Column_Names OUTPUT;
SELECT @Column_Names = COALESCE(@Column_Names, '*');
SET FMTONLY ON;
@@ -586,7 +600,6 @@ UNION ALL
SELECT [Canonical_Column_Name], [Aliased_Column_Name]
FROM #Column_Aliases
-WHERE [Aliased_Column_Name] NOT IN (SELECT [name] FROM {CatalogName}.[sys].[all_columns] WHERE [object_id] = @Object_ID)
ORDER BY [Canonical_Column_Id] ASC
DROP TABLE #Column_Aliases
diff --git a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs
index 2dc539b5f5..d913724b50 100644
--- a/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs
+++ b/src/Microsoft.Data.SqlClient/tests/Common/Fixtures/DatabaseObjects/DatabaseObject.cs
@@ -12,19 +12,28 @@ namespace Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects;
/// Base class for a transient database object (such as a table, type or
/// stored procedure.)
///
-public abstract class DatabaseObject : IDisposable
+///
+/// The type of the internal state accessible to derived types at the point of object creation
+/// via the property.
+///
+public abstract class DatabaseObject : IDisposable
{
private readonly bool _shouldDrop;
protected SqlConnection Connection { get; }
+ protected TState State { get; }
+
public string Name { get; }
- protected DatabaseObject(SqlConnection connection, string name, string definition, bool shouldCreate, bool shouldDrop)
+ public string UnescapedName => Name.Substring(1, Name.Length - 2).Replace("]]", "]");
+
+ protected DatabaseObject(SqlConnection connection, string name, string definition, TState state, bool shouldCreate, bool shouldDrop)
{
_shouldDrop = shouldDrop;
Connection = connection;
+ State = state;
Name = name;
if (shouldCreate)
@@ -261,3 +270,15 @@ public void Dispose()
GC.SuppressFinalize(this);
}
}
+
+///
+/// Base class for a transient database object (such as a table, type or
+/// stored procedure.)
+///
+public abstract class DatabaseObject : DatabaseObject