Skip to content

Commit 0213b16

Browse files
committed
User Story 38481: Fix unique db object name issues
- Fixed the unique name generators to: - Keep max lengths to 30 and 96 characters respectively. - Ensure uniqueness at the start of the names. - Added link to database identifier syntax.
1 parent ee6e146 commit 0213b16

26 files changed

+376
-197
lines changed

src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ public void SqlParameterProperties(string connection)
148148
const string firstColumnName = @"firstColumn";
149149
const string secondColumnName = @"secondColumn";
150150
const string thirdColumnName = @"thirdColumn";
151-
string inputProcedureName = DataTestUtility.GetUniqueName("InputProc").ToString();
152-
string outputProcedureName = DataTestUtility.GetUniqueName("OutputProc").ToString();
151+
string inputProcedureName = DataTestUtility.GetShortName("InputProc").ToString();
152+
string outputProcedureName = DataTestUtility.GetShortName("OutputProc").ToString();
153153
const int charColumnSize = 100;
154154
const int decimalColumnPrecision = 10;
155155
const int decimalColumnScale = 4;
@@ -694,7 +694,7 @@ public void TestExecuteReader(string connection)
694694
[ClassData(typeof(AEConnectionStringProvider))]
695695
public async void TestExecuteReaderAsyncWithLargeQuery(string connectionString)
696696
{
697-
string randomName = DataTestUtility.GetUniqueName(Guid.NewGuid().ToString().Replace("-", ""), false);
697+
string randomName = DataTestUtility.GetShortName(Guid.NewGuid().ToString().Replace("-", ""), false);
698698
if (randomName.Length > 50)
699699
{
700700
randomName = randomName.Substring(0, 50);
@@ -878,8 +878,8 @@ public void TestEnclaveStoredProceduresWithAndWithoutParameters(string connectio
878878
using SqlCommand sqlCommand = new("", sqlConnection, transaction: null,
879879
columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled);
880880

881-
string procWithoutParams = DataTestUtility.GetUniqueName("EnclaveWithoutParams", withBracket: false);
882-
string procWithParam = DataTestUtility.GetUniqueName("EnclaveWithParams", withBracket: false);
881+
string procWithoutParams = DataTestUtility.GetShortName("EnclaveWithoutParams", withBracket: false);
882+
string procWithParam = DataTestUtility.GetShortName("EnclaveWithParams", withBracket: false);
883883

884884
try
885885
{

src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/CspProviderExt.cs

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -157,55 +157,6 @@ public void TestRoundTripWithCSPAndCertStoreProvider()
157157
}
158158
}
159159

160-
[ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringSetupForAE))]
161-
[ClassData(typeof(AEConnectionStringProvider))]
162-
public void TestEncryptDecryptWithCSP(string connectionString)
163-
{
164-
string providerName = @"Microsoft Enhanced RSA and AES Cryptographic Provider";
165-
string keyIdentifier = DataTestUtility.GetUniqueNameForSqlServer("CSP");
166-
167-
try
168-
{
169-
CertificateUtilityWin.RSAPersistKeyInCsp(providerName, keyIdentifier);
170-
string cspPath = String.Concat(providerName, @"/", keyIdentifier);
171-
172-
SQLSetupStrategyCspExt sqlSetupStrategyCsp = new SQLSetupStrategyCspExt(cspPath);
173-
string tableName = sqlSetupStrategyCsp.CspProviderTable.Name;
174-
175-
try
176-
{
177-
using SqlConnection sqlConn = new(connectionString);
178-
sqlConn.Open();
179-
180-
Table.DeleteData(tableName, sqlConn);
181-
182-
// insert 1 row data
183-
Customer customer = new Customer(45, "Microsoft", "Corporation");
184-
185-
DatabaseHelper.InsertCustomerData(sqlConn, null, tableName, customer);
186-
187-
// Test INPUT parameter on an encrypted parameter
188-
using SqlCommand sqlCommand = new(@$"SELECT CustomerId, FirstName, LastName FROM [{tableName}] WHERE FirstName = @firstName",
189-
sqlConn, null, SqlCommandColumnEncryptionSetting.Enabled);
190-
SqlParameter customerFirstParam = sqlCommand.Parameters.AddWithValue(@"firstName", @"Microsoft");
191-
Console.WriteLine(@"Exception: {0}");
192-
customerFirstParam.Direction = System.Data.ParameterDirection.Input;
193-
194-
using SqlDataReader sqlDataReader = sqlCommand.ExecuteReader();
195-
ValidateResultSet(sqlDataReader);
196-
}
197-
finally
198-
{
199-
// clean up database resources
200-
sqlSetupStrategyCsp.Dispose();
201-
}
202-
}
203-
finally
204-
{
205-
CertificateUtilityWin.RSADeleteKeyInCsp(providerName, keyIdentifier);
206-
}
207-
}
208-
209160
/// <summary>
210161
/// Validates that the results are the ones expected.
211162
/// </summary>

src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs

Lines changed: 163 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public static bool TcpConnectionStringDoesNotUseAadAuth
8888
{
8989
get
9090
{
91-
SqlConnectionStringBuilder builder = new (TCPConnectionString);
91+
SqlConnectionStringBuilder builder = new(TCPConnectionString);
9292
return builder.Authentication == SqlAuthenticationMethod.SqlPassword || builder.Authentication == SqlAuthenticationMethod.NotSpecified;
9393
}
9494
}
@@ -415,59 +415,176 @@ public static bool DoesHostAddressContainBothIPv4AndIPv6()
415415
}
416416
}
417417

418+
// Generate a new GUID and return the characters from its 1st and 4th
419+
// parts, as shown here:
420+
//
421+
// 7ff01cb8-88c7-11f0-b433-00155d7e531e
422+
// ^^^^^^^^ ^^^^
423+
//
424+
// These 12 characters are concatenated together without any
425+
// separators. These 2 parts typically comprise a timestamp and clock
426+
// sequence, most likely to be unique for tests that generate names in
427+
// quick succession.
428+
private static string GetGuidParts()
429+
{
430+
var guid = Guid.NewGuid().ToString();
431+
// GOTCHA: The slice operator is inclusive of the start index and
432+
// exclusive of the end index!
433+
return guid.Substring(0, 8) + guid.Substring(19, 4);
434+
}
435+
418436
/// <summary>
419-
/// Generate a unique name to use in Sql Server;
420-
/// some providers does not support names (Oracle supports up to 30).
437+
/// Generate a short unique database object name, whose maximum length
438+
/// is 30 characters, with the format:
439+
///
440+
/// <Prefix>_<GuidParts>
441+
///
442+
/// The Prefix will be truncated to satisfy the overall maximum length.
443+
///
444+
/// The GUID parts will be the characters from the 1st and 4th blocks
445+
/// from a traditional string representation, as shown here:
446+
///
447+
/// 7ff01cb8-88c7-11f0-b433-00155d7e531e
448+
/// ^^^^^^^^ ^^^^
449+
///
450+
/// These 2 parts typically comprise a timestamp and clock sequence,
451+
/// most likely to be unique for tests that generate names in quick
452+
/// succession. The 12 characters are concatenated together without any
453+
/// separators.
421454
/// </summary>
422-
/// <param name="prefix">The name length will be no more then (16 + prefix.Length + escapeLeft.Length + escapeRight.Length).</param>
423-
/// <param name="withBracket">Name without brackets.</param>
424-
/// <returns>Unique name by considering the Sql Server naming rules.</returns>
425-
public static string GetUniqueName(string prefix, bool withBracket = true)
426-
{
427-
string escapeLeft = withBracket ? "[" : string.Empty;
428-
string escapeRight = withBracket ? "]" : string.Empty;
429-
string uniqueName = string.Format("{0}{1}_{2}_{3}{4}",
430-
escapeLeft,
431-
prefix,
432-
DateTime.Now.Ticks.ToString("X", CultureInfo.InvariantCulture), // up to 8 characters
433-
Guid.NewGuid().ToString().Substring(0, 6), // take the first 6 characters only
434-
escapeRight);
435-
return uniqueName;
455+
///
456+
/// <param name="prefix">
457+
/// The prefix to use when generating the unique name, truncated to at
458+
/// most 18 characters when withBracket is false, and 16 characters when
459+
/// withBracket is true.
460+
///
461+
/// This should not contain any characters that cannot be used in
462+
/// database object names. See:
463+
///
464+
/// https://learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-ver17#rules-for-regular-identifiers
465+
/// </param>
466+
///
467+
/// <param name="withBracket">
468+
/// When true, the entire generated name will be enclosed in square
469+
/// brackets, for example:
470+
///
471+
/// [MyPrefix_7ff01cb811f0]
472+
/// </param>
473+
///
474+
/// <returns>
475+
/// A unique database object name, no more than 30 characters long.
476+
/// </returns>
477+
public static string GetShortName(string prefix, bool withBracket = true)
478+
{
479+
StringBuilder name = new(30);
480+
481+
if (withBracket)
482+
{
483+
name.Append('[');
484+
}
485+
486+
int maxPrefixLength = withBracket ? 16 : 18;
487+
if (prefix.Length > maxPrefixLength)
488+
{
489+
prefix = prefix.Substring(0, maxPrefixLength);
490+
}
491+
492+
name.Append(prefix);
493+
name.Append('_');
494+
name.Append(GetGuidParts());
495+
496+
if (withBracket)
497+
{
498+
name.Append(']');
499+
}
500+
501+
return name.ToString();
436502
}
437503

438504
/// <summary>
439-
/// Uses environment values `UserName` and `MachineName` in addition to the specified `prefix` and current date
440-
/// to generate a unique name to use in Sql Server;
441-
/// SQL Server supports long names (up to 128 characters), add extra info for troubleshooting.
505+
/// Generate a long unique database object name, whose maximum length is
506+
/// 96 characters, with the format:
507+
///
508+
/// <Prefix>_<GuidParts>_<UserName>_<MachineName>
509+
///
510+
/// The Prefix will be truncated to satisfy the overall maximum length.
511+
///
512+
/// The GUID Parts will be the characters from the 1st and 4th blocks
513+
/// from a traditional string representation, as shown here:
514+
///
515+
/// 7ff01cb8-88c7-11f0-b433-00155d7e531e
516+
/// ^^^^^^^^ ^^^^
517+
///
518+
/// These 2 parts typically comprise a timestamp and clock sequence,
519+
/// most likely to be unique for tests that generate names in quick
520+
/// succession. The 12 characters are concatenated together without any
521+
/// separators.
522+
///
523+
/// The UserName and MachineName are obtained from the Environment,
524+
/// and will be truncated to satisfy the maximum overall length.
442525
/// </summary>
443-
/// <param name="prefix">Add the prefix to the generate string.</param>
444-
/// <param name="withBracket">Database name must be pass with brackets by default.</param>
445-
/// <returns>Unique name by considering the Sql Server naming rules, never longer than 96 characters.</returns>
446-
public static string GetUniqueNameForSqlServer(string prefix, bool withBracket = true)
447-
{
448-
string extendedPrefix = string.Format(
449-
"{0}_{1}_{2}@{3}",
450-
prefix,
451-
Environment.UserName,
452-
Environment.MachineName,
453-
DateTime.Now.ToString("yyyy_MM_dd", CultureInfo.InvariantCulture));
454-
string name = GetUniqueName(extendedPrefix, withBracket);
455-
456-
// Truncate to no more than 96 characters.
457-
const int maxLen = 96;
458-
if (name.Length > maxLen)
459-
{
460-
if (withBracket)
461-
{
462-
name = name.Substring(0, maxLen - 1) + ']';
463-
}
464-
else
465-
{
466-
name = name.Substring(0, maxLen);
467-
}
526+
///
527+
/// <param name="prefix">
528+
/// The prefix to use when generating the unique name, truncated to at
529+
/// most 32 characters.
530+
///
531+
/// This should not contain any characters that cannot be used in
532+
/// database object names. See:
533+
///
534+
/// https://learn.microsoft.com/en-us/sql/relational-databases/databases/database-identifiers?view=sql-server-ver17#rules-for-regular-identifiers
535+
/// </param>
536+
///
537+
/// <param name="withBracket">
538+
/// When true, the entire generated name will be enclosed in square
539+
/// brackets, for example:
540+
///
541+
/// [MyPrefix_7ff01cb811f0_test_user_ci_agent_machine_name]
542+
/// </param>
543+
///
544+
/// <returns>
545+
/// A unique database object name, no more than 96 characters long.
546+
/// </returns>
547+
public static string GetLongName(string prefix, bool withBracket = true)
548+
{
549+
StringBuilder name = new(96);
550+
551+
if (withBracket)
552+
{
553+
name.Append('[');
554+
}
555+
556+
if (prefix.Length > 32)
557+
{
558+
prefix = prefix.Substring(0, 32);
559+
}
560+
561+
name.Append(prefix);
562+
name.Append('_');
563+
name.Append(GetGuidParts());
564+
name.Append('_');
565+
566+
var suffix =
567+
Environment.UserName + '_' +
568+
Environment.MachineName;
569+
570+
int maxSuffixLength = 96 - name.Length;
571+
if (withBracket)
572+
{
573+
--maxSuffixLength;
574+
}
575+
if (suffix.Length > maxSuffixLength)
576+
{
577+
suffix = suffix.Substring(0, maxSuffixLength);
578+
}
579+
580+
name.Append(suffix);
581+
582+
if (withBracket)
583+
{
584+
name.Append(']');
468585
}
469586

470-
return name;
587+
return name.ToString();
471588
}
472589

473590
public static bool IsSupportingDistributedTransactions()

src/Microsoft.Data.SqlClient/tests/ManualTests/ProviderAgnostic/ReaderTest/ReaderTest.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public static void TestMain()
1919
{
2020
string connectionString = DataTestUtility.TCPConnectionString;
2121

22-
string tempTable = DataTestUtility.GetUniqueNameForSqlServer("table");
22+
string tempTable = DataTestUtility.GetLongName("table");
2323

2424
DbProviderFactory provider = SqlClientFactory.Instance;
2525
try
@@ -275,7 +275,7 @@ public static void TestMain()
275275
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))]
276276
public static void SqlDataReader_SqlBuffer_GetFieldValue()
277277
{
278-
string tableName = DataTestUtility.GetUniqueNameForSqlServer("SqlBuffer_GetFieldValue");
278+
string tableName = DataTestUtility.GetLongName("SqlBuffer_GetFieldValue");
279279
DateTimeOffset dtoffset = DateTimeOffset.Now;
280280
DateTime dt = DateTime.Now;
281281
//Exclude the millisecond because of rounding at some points by SQL Server.
@@ -374,7 +374,7 @@ public static void SqlDataReader_SqlBuffer_GetFieldValue()
374374
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))]
375375
public static async Task SqlDataReader_SqlBuffer_GetFieldValue_Async()
376376
{
377-
string tableName = DataTestUtility.GetUniqueNameForSqlServer("SqlBuffer_GetFieldValue_Async");
377+
string tableName = DataTestUtility.GetLongName("SqlBuffer_GetFieldValue_Async");
378378
DateTimeOffset dtoffset = DateTimeOffset.Now;
379379
DateTime dt = DateTime.Now;
380380
//Exclude the millisecond because of rounding at some points by SQL Server.

src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/AdapterTest/AdapterTest.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public class AdapterTest
5454
public AdapterTest()
5555
{
5656
// create random name for temp tables
57-
_tempTable = DataTestUtility.GetUniqueName("AdapterTest");
57+
_tempTable = DataTestUtility.GetShortName("AdapterTest");
5858
_tempTable = _tempTable.Replace('-', '_');
5959

6060
_randomGuid = Guid.NewGuid().ToString();
@@ -487,7 +487,7 @@ public void ParameterTest_AllTypes()
487487
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))]
488488
public void ParameterTest_InOut()
489489
{
490-
string procName = DataTestUtility.GetUniqueName("P");
490+
string procName = DataTestUtility.GetShortName("P");
491491
// input, output
492492
string spCreateInOut =
493493
"CREATE PROCEDURE " + procName + " @in int, @inout int OUTPUT, @out nvarchar(8) OUTPUT " +
@@ -768,13 +768,13 @@ public void BulkUpdateTest()
768768
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))]
769769
public void UpdateRefreshTest()
770770
{
771-
string identTableName = DataTestUtility.GetUniqueName("ID_");
771+
string identTableName = DataTestUtility.GetShortName("ID_");
772772
string createIdentTable =
773773
$"CREATE TABLE {identTableName} (id int IDENTITY," +
774774
"LastName nvarchar(50) NULL," +
775775
"Firstname nvarchar(50) NULL)";
776776

777-
string spName = DataTestUtility.GetUniqueName("sp_insert", withBracket: false);
777+
string spName = DataTestUtility.GetShortName("sp_insert", withBracket: false);
778778
string spCreateInsert =
779779
$"CREATE PROCEDURE {spName}" +
780780
"(@FirstName nvarchar(50), @LastName nvarchar(50), @id int OUTPUT) " +
@@ -1087,7 +1087,7 @@ public void AutoGenUpdateTest()
10871087
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureSynapse))]
10881088
public void AutoGenErrorTest()
10891089
{
1090-
string identTableName = DataTestUtility.GetUniqueName("ID_");
1090+
string identTableName = DataTestUtility.GetShortName("ID_");
10911091
string createIdentTable =
10921092
$"CREATE TABLE {identTableName} (id int IDENTITY," +
10931093
"LastName nvarchar(50) NULL," +

0 commit comments

Comments
 (0)