Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -472,11 +472,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
Expand Down Expand Up @@ -512,6 +513,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 a 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;

Expand All @@ -521,6 +527,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_Sys_All_Columns_Permissions INT = HAS_PERMS_BY_NAME('{catalogNameStringLiteral}.[sys].[all_columns]', 'OBJECT', 'SELECT');

IF CAST(SERVERPROPERTY('EngineEdition') AS INT) = 6
BEGIN
Expand All @@ -533,17 +540,21 @@ 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)';
END
ELSE
IF @Has_Sys_All_Columns_Permissions = 1
BEGIN
SET @Column_Name_Query_FILTER = N'WHERE [object_id] = @Object_ID';
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)';
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;
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
// 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.Text;
using System.Threading;

namespace Microsoft.Data.SqlClient.Tests.Common.Fixtures.DatabaseObjects;

/// <summary>
/// Base class for a transient database object (such as a table, type or
/// stored procedure.)
/// </summary>
/// <typeparam name="TState">
/// The type of the internal state accessible to derived types at the point of object creation
/// via the <see cref="State"/> property.
/// </typeparam>
public abstract class DatabaseObject<TState> : IDisposable
{
private readonly bool _shouldDrop;

protected SqlConnection Connection { get; }

protected TState State { get; }

public string Name { get; }

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)
{
EnsureConnectionOpen();
DropObject();
CreateObject(definition);
}
}

private void EnsureConnectionOpen()
{
const int MaxWaits = 2;
int counter = MaxWaits;

if (Connection.State is System.Data.ConnectionState.Closed)
{
Connection.Open();
}
while (counter-- > 0 && Connection.State is System.Data.ConnectionState.Connecting)
{
Thread.Sleep(80);
}
}

/// <summary>
/// Generate a new GUID and return the characters from its 1st and 4th
/// parts, as shown here:
///
/// <code>
/// 7ff01cb8-88c7-11f0-b433-00155d7e531e
/// ^^^^^^^^ ^^^^
/// </code>
///
/// These 12 characters are concatenated together without any
/// separators. These 2 parts typically comprise a timestamp and clock
/// sequence, most likely to be unique for tests that generate names in
/// quick succession.
/// </summary>
private static string GetGuidParts()
{
var guid = Guid.NewGuid().ToString();
// GOTCHA: The slice operator is inclusive of the start index and
// exclusive of the end index!
return guid.Substring(0, 8) + guid.Substring(19, 4);
}

/// <summary>
/// Generate a long unique database object name, whose maximum length is
/// 96 characters, with the format:
///
/// <c>{Prefix}_{GuidParts}_{UserName}_{MachineName}</c>
///
/// The Prefix will be truncated to satisfy the overall maximum length.
///
/// The GUID Parts will be the characters from the 1st and 4th blocks
/// from a traditional string representation, as shown here:
///
/// <code>
/// 7ff01cb8-88c7-11f0-b433-00155d7e531e
/// ^^^^^^^^ ^^^^
/// </code>
///
/// These 2 parts typically comprise a timestamp and clock sequence,
/// most likely to be unique for tests that generate names in quick
/// succession. The 12 characters are concatenated together without any
/// separators.
///
/// The UserName and MachineName are obtained from the Environment,
/// and will be truncated to satisfy the maximum overall length.
/// </summary>
///
/// <param name="prefix">
/// The prefix to use when generating the unique name, truncated to at
/// most 32 characters.
///
/// This should not contain any characters that cannot be used in
/// database object names. See:
///
/// https://learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-ver17#rules-for-regular-identifiers
/// </param>
///
/// <param name="escape">
/// When true, the entire generated name will be enclosed in square
/// brackets, for example:
///
/// <c>[MyPrefix_7ff01cb811f0_test_user_ci_agent_machine_name]</c>
/// </param>
///
/// <returns>
/// A unique database object name, no more than 96 characters long.
/// </returns>
public static string GenerateLongName(string prefix, bool escape = true)
{
StringBuilder name = new(96);

if (escape)
{
name.Append('[');
}

if (prefix.Length > 32)
{
prefix = prefix.Substring(0, 32);
}

name.Append(prefix);
name.Append('_');
name.Append(GetGuidParts());
name.Append('_');

var suffix =
Environment.UserName + '_' +
Environment.MachineName;

int maxSuffixLength = 96 - name.Length;
if (escape)
{
--maxSuffixLength;
}
if (suffix.Length > maxSuffixLength)
{
suffix = suffix.Substring(0, maxSuffixLength);
}

name.Append(suffix);

if (escape)
{
name.Append(']');
}

return name.ToString();
}

/// <summary>
/// Generate a short unique database object name, whose maximum length
/// is 30 characters, with the format:
///
/// <c>{Prefix}_{GuidParts}</c>
///
/// The Prefix will be truncated to satisfy the overall maximum length.
///
/// The GUID parts will be the characters from the 1st and 4th blocks
/// from a traditional string representation, as shown here:
///
/// <code>
/// 7ff01cb8-88c7-11f0-b433-00155d7e531e
/// ^^^^^^^^ ^^^^
/// </code>
///
/// These 2 parts typically comprise a timestamp and clock sequence,
/// most likely to be unique for tests that generate names in quick
/// succession. The 12 characters are concatenated together without any
/// separators.
/// </summary>
///
/// <param name="prefix">
/// The prefix to use when generating the unique name, truncated to at
/// most 18 characters when withBracket is false, and 16 characters when
/// withBracket is true.
///
/// This should not contain any characters that cannot be used in
/// database object names. See:
///
/// https://learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-ver17#rules-for-regular-identifiers
/// </param>
///
/// <param name="escape">
/// When true, the entire generated name will be enclosed in square
/// brackets, for example:
///
/// <c>[MyPrefix_7ff01cb811f0]</c>
/// </param>
///
/// <returns>
/// A unique database object name, no more than 30 characters long.
/// </returns>
public static string GenerateShortName(string prefix, bool escape = true)
{
StringBuilder name = new(30);

if (escape)
{
name.Append('[');
}

int maxPrefixLength = escape ? 16 : 18;
if (prefix.Length > maxPrefixLength)
{
prefix = prefix.Substring(0, maxPrefixLength);
}

name.Append(prefix);
name.Append('_');
name.Append(GetGuidParts());

if (escape)
{
name.Append(']');
}

return name.ToString();
}

/// <summary>
/// Creates the object with a given definition.
/// </summary>
/// <param name="definition">Definition of the object to create.</param>
/// <remarks>
/// By the time this is called, <see cref="Connection"/> will be open.
/// </remarks>
protected abstract void CreateObject(string definition);

/// <summary>
/// Drops the object created by <see cref="CreateObject"/>.
/// </summary>
/// <remarks>
/// By the time this is called, <see cref="Connection"/> will be open.
/// Must not throw an exception if the object does not exist.
/// </remarks>
protected abstract void DropObject();

public void Dispose()
{
if (_shouldDrop)
{
EnsureConnectionOpen();
DropObject();
}
// This explicitly does not drop the wrapped SqlConnection; this is sometimes
// used in a loop to create multiple UDTs.

GC.SuppressFinalize(this);
}
}

/// <summary>
/// Base class for a transient database object (such as a table, type or
/// stored procedure.)
/// </summary>
public abstract class DatabaseObject : DatabaseObject<object?>
{
protected DatabaseObject(SqlConnection connection, string name, string definition, bool shouldCreate, bool shouldDrop)
: base(connection, name, definition, state: null, shouldCreate, shouldDrop)
{
}
}
Loading
Loading