diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/MultipartIdentifier.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/MultipartIdentifier.cs
index b89a080e99..8e3ee0b7d4 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/MultipartIdentifier.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/MultipartIdentifier.cs
@@ -2,30 +2,29 @@
// 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.Diagnostics;
using System.Text;
+#nullable enable
+
namespace Microsoft.Data.Common
{
internal class MultipartIdentifier
{
+ private const char IdentifierSeparator = '.';
+ // The indexes of identifier start and end characters in these strings need to
+ // align 1-to-1. An identifier which starts with [ can only end with ]. As such,
+ // both of the below constants must be the same length.
+ // Separately, neither constant may contain the identifier separator.
+ private const string IdentifierStartCharacters = "[\"";
+ private const string IdentifierEndCharacters = "]\"";
+
private const int MaxParts = 4;
internal const int ServerIndex = 0;
internal const int CatalogIndex = 1;
internal const int SchemaIndex = 2;
internal const int TableIndex = 3;
- /*
- Left quote strings need to correspond 1 to 1 with the right quote strings
- example: "ab" "cd", passed in for the left and the right quote
- would set a or b as a starting quote character.
- If a is the starting quote char then c would be the ending quote char
- otherwise if b is the starting quote char then d would be the ending quote character.
- */
- internal static string[] ParseMultipartIdentifier(string name, string leftQuote, string rightQuote, string property, bool ThrowOnEmptyMultipartName)
- {
- return ParseMultipartIdentifier(name, leftQuote, rightQuote, '.', MaxParts, true, property, ThrowOnEmptyMultipartName);
- }
-
private enum MPIState
{
MPI_Value,
@@ -36,83 +35,74 @@ private enum MPIState
MPI_RightQuote,
}
- /* Core function for parsing the multipart identifier string.
- * parameters: name - string to parse
- * leftquote: set of characters which are valid quoting characters to initiate a quote
- * rightquote: set of characters which are valid to stop a quote, array index's correspond to the leftquote array.
- * separator: separator to use
- * limit: number of names to parse out
- * removequote:to remove the quotes on the returned string
- */
- private static void IncrementStringCount(string name, string[] ary, ref int position, string property)
+ private static void IncrementStringCount(string identifier, string?[] ary, ref int position, string property)
{
++position;
int limit = ary.Length;
if (position >= limit)
{
- throw ADP.InvalidMultipartNameToManyParts(property, name, limit);
+ throw ADP.InvalidMultipartNameToManyParts(property, identifier, limit);
}
ary[position] = string.Empty;
}
- private static bool IsWhitespace(char ch)
- {
- return char.IsWhiteSpace(ch);
- }
+ private static bool ContainsChar(string str, char ch) =>
+#if NET
+ str.Contains(ch);
+#else
+ str.IndexOf(ch) != -1;
+#endif
- internal static string[] ParseMultipartIdentifier(string name, string leftQuote, string rightQuote, char separator, int limit, bool removequotes, string property, bool ThrowOnEmptyMultipartName)
+ ///
+ /// Core function for parsing the multipart identifier string.
+ ///
+ /// String to parse.
+ /// Name of the property containing the multipart identifier. If an exception is thrown, its message will include this property name.
+ /// If true, throw if the multipart identifier is whitespace.
+ /// Number of parts to parse out. Defaults to four (to allow for an identifier formatted as [server].[database].[schema].[object].)
+ /// An array of strings containing the various parts in the identifier.
+ internal static string?[] ParseMultipartIdentifier(string identifier, string property, bool throwOnEmptyMultipartIdentifier, int limit = MaxParts)
{
- if (limit <= 0)
- {
- throw ADP.InvalidMultipartNameToManyParts(property, name, limit);
- }
+ Debug.Assert(limit > 0 && limit <= MaxParts);
- if (-1 != leftQuote.IndexOf(separator) || -1 != rightQuote.IndexOf(separator) || leftQuote.Length != rightQuote.Length)
- {
- throw ADP.InvalidMultipartNameIncorrectUsageOfQuotes(property, name);
- }
-
- string[] parsedNames = new string[limit]; // return string array
+ string?[] parts = new string?[limit]; // return string array
int stringCount = 0; // index of current string in the buffer
MPIState state = MPIState.MPI_Value; // Initialize the starting state
- StringBuilder sb = new StringBuilder(name.Length); // String buffer to hold the string being currently built, init the string builder so it will never be resized
- StringBuilder whitespaceSB = null; // String buffer to hold whitespace used when parsing nonquoted strings 'a b . c d' = 'a b' and 'c d'
- char rightQuoteChar = ' '; // Right quote character to use given the left quote character found.
- for (int index = 0; index < name.Length; ++index)
+ StringBuilder sb = new StringBuilder(identifier.Length); // String buffer to hold the string being currently built, init the string builder so it will never be resized
+ StringBuilder? whitespaceSB = null; // String buffer to hold whitespace used when parsing nonquoted strings 'a b . c d' = 'a b' and 'c d'
+ char rightQuoteChar = ' '; // Right quote character to use given the left quote character found.
+ for (int index = 0; index < identifier.Length; ++index)
{
- char testchar = name[index];
+ char testchar = identifier[index];
switch (state)
{
case MPIState.MPI_Value:
{
int quoteIndex;
- if (IsWhitespace(testchar))
- { // Is White Space then skip the whitespace
+ if (char.IsWhiteSpace(testchar))
+ {
+ // Skip whitespace
continue;
}
- else
- if (testchar == separator)
- { // If we found a separator, no string was found, initialize the string we are parsing to Empty and the next one to Empty.
- // This is NOT a redundant setting of string.Empty it solves the case where we are parsing ".foo" and we should be returning null, null, empty, foo
- parsedNames[stringCount] = string.Empty;
- IncrementStringCount(name, parsedNames, ref stringCount, property);
+ else if (testchar == IdentifierSeparator)
+ {
+ // If we found a separator, no string was found, initialize the string we are parsing to Empty and the next one to Empty.
+ // This is NOT a redundant setting of string.Empty. It solves the case where we are parsing ".foo" and we should be returning null, null, empty, foo
+ parts[stringCount] = string.Empty;
+ IncrementStringCount(identifier, parts, ref stringCount, property);
}
- else
- if (-1 != (quoteIndex = leftQuote.IndexOf(testchar)))
- { // If we are a left quote
- rightQuoteChar = rightQuote[quoteIndex]; // record the corresponding right quote for the left quote
+ else if ((quoteIndex = IdentifierStartCharacters.IndexOf(testchar)) != -1)
+ {
+ // If we are a left quote, record the corresponding right quote for the left quote
+ rightQuoteChar = IdentifierEndCharacters[quoteIndex];
sb.Length = 0;
- if (!removequotes)
- {
- sb.Append(testchar);
- }
state = MPIState.MPI_ParseQuote;
}
- else
- if (-1 != rightQuote.IndexOf(testchar))
- { // If we shouldn't see a right quote
- throw ADP.InvalidMultipartNameIncorrectUsageOfQuotes(property, name);
+ else if (ContainsChar(IdentifierEndCharacters, testchar))
+ {
+ // If we shouldn't see a right quote
+ throw ADP.InvalidMultipartNameIncorrectUsageOfQuotes(property, identifier);
}
else
{
@@ -125,32 +115,31 @@ internal static string[] ParseMultipartIdentifier(string name, string leftQuote,
case MPIState.MPI_ParseNonQuote:
{
- if (testchar == separator)
+ if (testchar == IdentifierSeparator)
{
- parsedNames[stringCount] = sb.ToString(); // set the currently parsed string
- IncrementStringCount(name, parsedNames, ref stringCount, property);
+ // Set the currently parsed string
+ parts[stringCount] = sb.ToString();
+ IncrementStringCount(identifier, parts, ref stringCount, property);
state = MPIState.MPI_Value;
}
- else // Quotes are not valid inside a non-quoted name
- if (-1 != rightQuote.IndexOf(testchar))
+ else if (ContainsChar(IdentifierEndCharacters, testchar))
{
- throw ADP.InvalidMultipartNameIncorrectUsageOfQuotes(property, name);
+ // Quotes are not valid inside a non-quoted identifier
+ throw ADP.InvalidMultipartNameIncorrectUsageOfQuotes(property, identifier);
}
- else
- if (-1 != leftQuote.IndexOf(testchar))
+ else if (ContainsChar(IdentifierStartCharacters, testchar))
{
- throw ADP.InvalidMultipartNameIncorrectUsageOfQuotes(property, name);
+ throw ADP.InvalidMultipartNameIncorrectUsageOfQuotes(property, identifier);
}
- else
- if (IsWhitespace(testchar))
- { // If it is Whitespace
- parsedNames[stringCount] = sb.ToString(); // Set the currently parsed string
- if (whitespaceSB == null)
- {
- whitespaceSB = new StringBuilder();
- }
+ else if (char.IsWhiteSpace(testchar))
+ {
+ // If it is whitespace, set the currently parsed string
+ parts[stringCount] = sb.ToString();
+
+ whitespaceSB ??= new StringBuilder();
+ // Start to record the whitespace. If we are parsing an identifier like "foo bar" we should return "foo bar"
whitespaceSB.Length = 0;
- whitespaceSB.Append(testchar); // start to record the whitespace, if we are parsing a name like "foo bar" we should return "foo bar"
+ whitespaceSB.Append(testchar);
state = MPIState.MPI_LookForNextCharOrSeparator;
}
else
@@ -162,23 +151,22 @@ internal static string[] ParseMultipartIdentifier(string name, string leftQuote,
case MPIState.MPI_LookForNextCharOrSeparator:
{
- if (!IsWhitespace(testchar))
- { // If it is not whitespace
- if (testchar == separator)
- {
- IncrementStringCount(name, parsedNames, ref stringCount, property);
- state = MPIState.MPI_Value;
- }
- else
- { // If its not a separator and not whitespace
- sb.Append(whitespaceSB);
- sb.Append(testchar);
- parsedNames[stringCount] = sb.ToString(); // Need to set the name here in case the string ends here.
- state = MPIState.MPI_ParseNonQuote;
- }
+ if (testchar == IdentifierSeparator)
+ {
+ IncrementStringCount(identifier, parts, ref stringCount, property);
+ state = MPIState.MPI_Value;
+ }
+ else if (!char.IsWhiteSpace(testchar))
+ {
+ sb.Append(whitespaceSB);
+ sb.Append(testchar);
+ // Need to set the identifier part here in case the string ends here.
+ parts[stringCount] = sb.ToString();
+ state = MPIState.MPI_ParseNonQuote;
}
else
{
+ whitespaceSB ??= new StringBuilder();
whitespaceSB.Append(testchar);
}
break;
@@ -186,17 +174,14 @@ internal static string[] ParseMultipartIdentifier(string name, string leftQuote,
case MPIState.MPI_ParseQuote:
{
+ // If we are on a right quote, see if we are escaping the right quote or ending the quoted string
if (testchar == rightQuoteChar)
- { // if se are on a right quote see if we are escaping the right quote or ending the quoted string
- if (!removequotes)
- {
- sb.Append(testchar);
- }
+ {
state = MPIState.MPI_RightQuote;
}
else
{
- sb.Append(testchar); // Append what we are currently parsing
+ sb.Append(testchar);
}
break;
}
@@ -204,25 +189,27 @@ internal static string[] ParseMultipartIdentifier(string name, string leftQuote,
case MPIState.MPI_RightQuote:
{
if (testchar == rightQuoteChar)
- { // If the next char is another right quote then we were escaping the right quote
+ {
+ // If the next char is another right quote then we were escaping the right quote
sb.Append(testchar);
state = MPIState.MPI_ParseQuote;
}
- else
- if (testchar == separator)
- { // If its a separator then record what we've parsed
- parsedNames[stringCount] = sb.ToString();
- IncrementStringCount(name, parsedNames, ref stringCount, property);
+ else if (testchar == IdentifierSeparator)
+ {
+ // If it's a separator then record what we've parsed
+ parts[stringCount] = sb.ToString();
+ IncrementStringCount(identifier, parts, ref stringCount, property);
state = MPIState.MPI_Value;
}
- else
- if (!IsWhitespace(testchar))
- { // If it is not whitespace we got problems
- throw ADP.InvalidMultipartNameIncorrectUsageOfQuotes(property, name);
+ else if (!char.IsWhiteSpace(testchar))
+ {
+ // If it is not whitespace then we have problems
+ throw ADP.InvalidMultipartNameIncorrectUsageOfQuotes(property, identifier);
}
else
- { // It is a whitespace character so the following char should be whitespace, separator, or end of string anything else is bad
- parsedNames[stringCount] = sb.ToString();
+ {
+ // It is a whitespace character so the following char should be whitespace, separator, or end of string. Anything else is bad
+ parts[stringCount] = sb.ToString();
state = MPIState.MPI_LookForSeparator;
}
break;
@@ -230,17 +217,16 @@ internal static string[] ParseMultipartIdentifier(string name, string leftQuote,
case MPIState.MPI_LookForSeparator:
{
- if (!IsWhitespace(testchar))
- { // If it is not whitespace
- if (testchar == separator)
- { // If it is a separator
- IncrementStringCount(name, parsedNames, ref stringCount, property);
- state = MPIState.MPI_Value;
- }
- else
- { // Otherwise not a separator
- throw ADP.InvalidMultipartNameIncorrectUsageOfQuotes(property, name);
- }
+ if (testchar == IdentifierSeparator)
+ {
+ // If it is a separator
+ IncrementStringCount(identifier, parts, ref stringCount, property);
+ state = MPIState.MPI_Value;
+ }
+ else if (!char.IsWhiteSpace(testchar))
+ {
+ // Otherwise not a separator
+ throw ADP.InvalidMultipartNameIncorrectUsageOfQuotes(property, identifier);
}
break;
}
@@ -250,42 +236,46 @@ internal static string[] ParseMultipartIdentifier(string name, string leftQuote,
// Resolve final states after parsing the string
switch (state)
{
- case MPIState.MPI_Value: // These states require no extra action
+ // These states require no extra action
+ case MPIState.MPI_Value:
case MPIState.MPI_LookForSeparator:
case MPIState.MPI_LookForNextCharOrSeparator:
break;
- case MPIState.MPI_ParseNonQuote: // Dump what ever was parsed
+ // Dump whatever was parsed
+ case MPIState.MPI_ParseNonQuote:
case MPIState.MPI_RightQuote:
- parsedNames[stringCount] = sb.ToString();
+ parts[stringCount] = sb.ToString();
break;
- case MPIState.MPI_ParseQuote: // Invalid Ending States
+ // Invalid Ending States
+ case MPIState.MPI_ParseQuote:
default:
- throw ADP.InvalidMultipartNameIncorrectUsageOfQuotes(property, name);
+ throw ADP.InvalidMultipartNameIncorrectUsageOfQuotes(property, identifier);
}
- if (parsedNames[0] == null)
+ if (parts[0] == null)
{
- if (ThrowOnEmptyMultipartName)
+ // Identifier is entirely made up of whitespace
+ if (throwOnEmptyMultipartIdentifier)
{
- throw ADP.InvalidMultipartName(property, name); // Name is entirely made up of whitespace
+ throw ADP.InvalidMultipartName(property, identifier);
}
}
else
{
- // Shuffle the parsed name, from left justification to right justification, i.e. [a][b][null][null] goes to [null][null][a][b]
+ // Shuffle the identifier parts, from left justification to right justification, i.e. [a][b][null][null] goes to [null][null][a][b]
int offset = limit - stringCount - 1;
if (offset > 0)
{
for (int x = limit - 1; x >= offset; --x)
{
- parsedNames[x] = parsedNames[x - offset];
- parsedNames[x - offset] = null;
+ parts[x] = parts[x - offset];
+ parts[x - offset] = null;
}
}
}
- return parsedNames;
+ return parts;
}
}
}
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 39e4f570c7..980b066e80 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs
@@ -411,7 +411,7 @@ private string CreateInitialQuery()
string[] parts;
try
{
- parts = MultipartIdentifier.ParseMultipartIdentifier(DestinationTableName, "[\"", "]\"", Strings.SQL_BulkCopyDestinationTableName, true);
+ parts = MultipartIdentifier.ParseMultipartIdentifier(DestinationTableName, Strings.SQL_BulkCopyDestinationTableName, true);
}
catch (Exception e)
{
@@ -542,7 +542,7 @@ private string AnalyzeTargetAndCreateUpdateBulkCommand(BulkCopySimpleResultSet i
throw SQL.BulkLoadNoCollation();
}
- string[] parts = MultipartIdentifier.ParseMultipartIdentifier(DestinationTableName, "[\"", "]\"", Strings.SQL_BulkCopyDestinationTableName, true);
+ string[] parts = MultipartIdentifier.ParseMultipartIdentifier(DestinationTableName, Strings.SQL_BulkCopyDestinationTableName, true);
updateBulkCommandText.AppendFormat("insert bulk {0} (", ADP.BuildMultiPartName(parts));
// Throw if there is a transaction but no flag is set
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs
index f3a85723c6..c073bef4d9 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs
@@ -1291,11 +1291,9 @@ internal void DeriveParameters()
// Use common parser for SqlClient and OleDb - parse into 4 parts - Server, Catalog,
// Schema, ProcedureName
string[] parsedSProc = MultipartIdentifier.ParseMultipartIdentifier(
- name: CommandText,
- leftQuote: "[\"",
- rightQuote: "]\"",
+ identifier: CommandText,
property: Strings.SQL_SqlCommandCommandText,
- ThrowOnEmptyMultipartName: false);
+ throwOnEmptyMultipartIdentifier: false);
if (string.IsNullOrEmpty(parsedSProc[3]))
{
@@ -2809,10 +2807,8 @@ private void SetUpRPCParameters(_SqlRPC rpc, bool inSchema, SqlParameterCollecti
{
string[] parts = MultipartIdentifier.ParseMultipartIdentifier(
parameter.TypeName,
- leftQuote: "[\"",
- rightQuote: "]\"",
property: Strings.SQL_TDSParserTableName,
- ThrowOnEmptyMultipartName: false);
+ throwOnEmptyMultipartIdentifier: false);
// @TODO: Combine this and inner if statement
if (parts?.Length == 4)
{
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs
index e9d18d9f66..2defc2e950 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs
@@ -2514,7 +2514,7 @@ internal static string[] ParseTypeName(string typeName, bool isUdtTypeName)
try
{
string errorMsg = isUdtTypeName ? Strings.SQL_UDTTypeName : Strings.SQL_TypeName;
- return MultipartIdentifier.ParseMultipartIdentifier(typeName, "[\"", "]\"", '.', 3, true, errorMsg, true);
+ return MultipartIdentifier.ParseMultipartIdentifier(typeName, errorMsg, true, limit: 3);
}
catch (ArgumentException)
{
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs
index f189030d1e..06965068b3 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs
@@ -765,7 +765,7 @@ private void ParseMultipartName()
{
if (_multipartName != null)
{
- string[] parts = MultipartIdentifier.ParseMultipartIdentifier(_multipartName, "[\"", "]\"", Strings.SQL_TDSParserTableName, false);
+ string[] parts = MultipartIdentifier.ParseMultipartIdentifier(_multipartName, Strings.SQL_TDSParserTableName, false);
_serverName = parts[0];
_catalogName = parts[1];
_schemaName = parts[2];
diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj
index ec13e9a857..9b33898dcf 100644
--- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj
+++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.FunctionalTests.csproj
@@ -15,7 +15,6 @@
-
diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/MultipartIdentifierTests.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/MultipartIdentifierTests.cs
deleted file mode 100644
index 6e6ba3a751..0000000000
--- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/MultipartIdentifierTests.cs
+++ /dev/null
@@ -1,260 +0,0 @@
-// 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 Microsoft.Data.Common;
-using Xunit;
-
-namespace Microsoft.Data.SqlClient.Tests
-{
- public class MultipartIdentifierTests
- {
- [Fact]
- public void SingleUnquoted() => RunParse("foo", new[] { "foo" });
-
- [Fact]
- public void SingleUnquotedOvercount() => RunParse("foo", new[] { null, "foo" }, maxCount: 2);
-
- [Fact]
- public void SingleUnquotedContainsWhitespace() => RunParse("foo bar", new[] { "foo bar" });
-
- [Fact]
- public void SingleUnquotedStartWithShitespace() => RunParse(" foo", new[] { "foo" });
-
- [Fact]
- public void SingleUnquotedEndWithShitespace() => RunParse("foo ", new[] { "foo" });
-
- [Fact]
- public void SingleQuotedRemoveQuote() => RunParse("[foo]", new[] { "[foo]" }, false);
-
- [Fact]
- public void SingleQuotedKeepQuote() => RunParse("[foo]", new[] { "foo" }, true);
-
- [Fact]
- public void SingleQuotedLeadingWhitespace() => RunParse("[ foo]", new[] { " foo" }, true);
-
- [Fact]
- public void SingleQuotedTrailingWhitespace() => RunParse("[foo ]", new[] { "foo " }, true);
-
- [Fact]
- public void QuotedContainsWhitespace() => RunParse("[foo bar]", new[] { "foo bar" }, true);
-
- [Fact]
- public void SingleQuotedContainsAndTrailingWhitespace() => RunParse("[foo bar ]", new[] { "foo bar " });
-
- [Fact]
- public void SingleQuotedInternalAndLeadingWhitespace() => RunParse("[ foo bar]", new[] { " foo bar" });
-
- [Fact]
- public void SingleQuotedContainsAndLeadingAndTrailingWhitespace() => RunParse("[ foo bar ]", new[] { " foo bar " });
-
- [Fact]
- public void SingleQuotedEscapedQuote() => RunParse("[foo]]bar]", new[] { "foo]bar" }, true);
-
-
- [Fact]
- public void DoubleUnquotedParts() => RunParse("foo.bar", new[] { "foo", "bar" });
-
- [Fact]
- public void DoubleUnquotedPartContainsTrailngWhitespace() => RunParse("foo .bar", new[] { "foo", "bar" });
-
- [Fact]
- public void DoubleUnquotedPartContainsLeadingWhitespace() => RunParse("foo. bar", new[] { "foo", "bar" });
-
- [Fact]
- public void DoubleUnquotedEmptyFirst() => RunParse(".bar", new[] { "", "bar" });
-
- [Fact]
- public void DoubleUnquotedEmptyLast() => RunParse("foo.", new[] { "foo", "" });
-
- [Fact]
- public void DoubleQuotedParts() => RunParse("[foo].[bar]", new string[] { "foo", "bar" });
-
- [Fact]
- public void DoubleQuotedPartContainsLeadingWhitespace() => RunParse("[foo]. [bar]", new[] { "foo", "bar" });
-
- [Fact]
- public void DoubleQuotedPartContainsTrailngWhitespace() => RunParse("[foo] .[bar]", new[] { "foo", "bar" });
-
-
- [Fact]
- public void TripleUnquotedParts() => RunParse("foo.bar.ed", new[] { "foo", "bar", "ed" });
-
- [Fact]
- public void TripleUnquotedMissingMiddle() => RunParse("foo..bar", new[] { "foo", "", "bar" });
-
- [Fact]
- public void TripleUnquotedPartContainsTrailingWhitespace() => RunParse("foo .bar .ed", new[] { "foo", "bar", "ed" });
-
- [Fact]
- public void TripleUnquotedPartContainsEmptyAndTrailngWhitespace() => RunParse(" .bar .ed", new[] { "", "bar", "ed" });
-
- [Fact]
- public void TripleUnquotedPartContainsLeadingWhitespace() => RunParse("foo. bar.", new[] { "foo", "bar", "" });
-
- [Fact]
- public void TripleUnquotedEmptyPart() => RunParse(".bar", new[] { "", "bar" });
-
- [Fact]
- public void TripleQuotedParts() => RunParse("[foo].[bar]", new[] { "foo", "bar" });
-
- [Fact]
- public void TripleQuotedPartContainsLeadingWhitespace() => RunParse("[foo]. [bar]", new[] { "foo", "bar" });
-
- [Fact]
- public void TripleQuotedPartContainsTrailngWhitespace() => RunParse("[foo] .[bar]", new[] { "foo", "bar" });
-
- [Fact]
- public void InvalidUnquotedEmpty() => ThrowParse("", new[] { "" });
-
- [Fact]
- public void InvalidContainsOpen() => ThrowParse("foo[bar", new[] { "foo[bar" });
-
- [Fact]
- public void InvalidContainsClose() => ThrowParse("foo]bar", new[] { "foo]bar" });
-
- [Fact]
- public void InvalidStartsWithClose() => ThrowParse("]bar", new[] { "]bar" });
-
- [Fact]
- public void InvalidEndsWithClose() => ThrowParse("bar]", new[] { "bar]" });
-
- [Fact]
- public void InvalidUnfinishedBraceOpen() => ThrowParse("[foo", new[] { "[foo" });
-
- [Fact]
- public void InvalidUnfinishedQuoteOpen() => ThrowParse("\"foo", new[] { "\"foo" });
-
- [Fact]
- public void InvalidCapacity()
- {
- ThrowParse("", Array.Empty());
- }
-
- [Fact]
- public void InvalidLeftQuote()
- {
- ThrowParse("foo", new[] { "foo" }, leftQuotes: "[.");
- }
-
- [Fact]
- public void InvalidRightQuote()
- {
- ThrowParse("foo", new[] { "foo" }, rightQuotes: "[.");
- }
-
- [Fact]
- public void InvalidQuotedPartContainsTrailngNonWhitespace() => ThrowParse("[foo]!.[bar]", new[] { "foo", "bar" });
-
- [Fact]
- public void InvalidQuotedPartContainsTrailngWhiteSpaceThenNonWhitespace() => ThrowParse("[foo] !.[bar]", new[] { "foo", "bar" });
-
- [Fact]
- public void InvalidTooManyParts_2to1() => ThrowParse("foo.bar", new[] { "foo" });
-
- [Fact]
- public void InvalidTooManyPartsEndsInSeparator() => ThrowParse("a.", 1);
-
- [Fact]
- public void InvalidTooManyPartsAfterTrailingWhitespace() => ThrowParse("foo .bar .ed", 1);
-
- [Fact]
- public void InvalidTooManyPartsEndsWithCloseQuote() => ThrowParse("a.[b]", 1);
-
- [Fact]
- public void InvalidTooManyPartsEndsWithWhitespace() => ThrowParse("a.foo ", 1);
-
- [Fact]
- public void InvalidTooManyPartsQuotedPartContainsLeadingWhitespace() => ThrowParse("a.[b].c", 1);
-
- [Fact]
- public void InvalidTooManyPartsWhiteSpaceBeforeSeparator() => ThrowParse("a.b ..", 2);
-
- [Fact]
- public void InvalidTooManyPartsAfterCloseQuote() => ThrowParse("a.[b] .c", 1);
-
- [Fact]
- public void InvalidTooManyPartsSeparatorAfterPart() => ThrowParse("a.b.c", 1);
-
-
- private static void RunParse(string name, string[] expected, bool removeQuotes = true, int maxCount = 0)
- {
- if (maxCount == 0)
- {
- for (int index = 0; index < expected.Length; index++)
- {
- if (expected[index] != null)
- {
- maxCount += 1;
- }
- }
- }
-
- string[] originalParts = MultipartIdentifier.ParseMultipartIdentifier(name, "[\"", "]\"", '.', maxCount, removeQuotes, "", true);
-
- for (int index = 0; index < expected.Length; index++)
- {
- string expectedPart = expected[index];
- string originalPart = originalParts[index];
-
- Assert.Equal(expectedPart, originalPart);
- }
- }
-
- private static void ThrowParse(string name, string[] expected, bool removeQuotes = true, string leftQuotes = "[\"", string rightQuotes = "]\"", char separator = '.')
- where TException : Exception
- {
- int maxCount = 0;
- for (int index = 0; index < expected.Length; index++)
- {
- if (expected[index] != null)
- {
- maxCount += 1;
- }
- }
-
- Exception originalException = Assert.Throws(() =>
- MultipartIdentifier.ParseMultipartIdentifier(name, leftQuotes, rightQuotes, separator, maxCount, removeQuotes, "", true)
- );
-
- Assert.NotNull(originalException);
- }
-
-
-
- private static void ThrowParse(string name, int expectedLength, bool removeQuotes = true, string leftQuotes = "[\"", string rightQuotes = "]\"", char separator = '.')
- {
- Exception originalException = Assert.Throws(
- () =>
- {
- MultipartIdentifier.ParseMultipartIdentifier(name, leftQuotes, rightQuotes, separator, expectedLength, removeQuotes, "test", true);
- }
- );
- Assert.NotNull(originalException);
- }
-
- }
-}
-
-namespace Microsoft.Data.Common
-{
- // this is needed for the inclusion of MultipartIdentifier class
- internal class ADP
- {
- internal static ArgumentException InvalidMultipartName(string property, string name)
- {
- return new ArgumentException();
- }
-
- internal static ArgumentException InvalidMultipartNameIncorrectUsageOfQuotes(string property, string name)
- {
- return new ArgumentException();
- }
-
- internal static ArgumentException InvalidMultipartNameToManyParts(string property, string name, int limit)
- {
- return new ArgumentException();
- }
- }
-}
diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Common/MultipartIdentifierTests.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Common/MultipartIdentifierTests.cs
new file mode 100644
index 0000000000..72f07573a1
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/Common/MultipartIdentifierTests.cs
@@ -0,0 +1,374 @@
+// 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.Collections.Generic;
+using System.Diagnostics;
+using Xunit;
+
+namespace Microsoft.Data.Common.UnitTests;
+
+public class MultipartIdentifierTests
+{
+ ///
+ /// Gets a collection of test data containing various part identifier strings and their expected
+ /// parse results.
+ ///
+ ///
+ /// The returned data includes different combinations of part identifier formats, such as
+ /// those with embedded whitespace, leading or trailing whitespace, and bracket characters.
+ ///
+ ///
+ public static TheoryData ValidSinglePartIdentifierVariations
+ {
+ get
+ {
+ ReadOnlySpan part1Words = ["word1", "word 1"];
+ TheoryData data = [];
+
+ // Combination 1: embedded and non-embedded whitespace.
+ // Combination 2: leading and/or trailing whitespace, and no whitespace
+ // Combination 3: bracket characters "" or []
+ // Combination 4: wrapped in bracket characters, no bracket characters
+ // Combination 5: if wrapped in bracket characters, embedded (escaped) characters
+ foreach (string part1 in part1Words)
+ {
+ foreach ((string p1Combination, string p1Expected) in GeneratePartCombinations(part1))
+ {
+ string onePartCombination = p1Combination;
+ string[] onePartExpected = [p1Expected];
+
+ data.Add(onePartCombination, onePartExpected);
+ }
+ }
+
+ return data;
+ }
+ }
+
+ ///
+ /// Gets a collection of test cases representing various formats and structures of multipart identifiers.
+ ///
+ ///
+ /// This property provides examples of multipart identifiers with different numbers of
+ /// parts, including cases with empty segments, variations of whitespace around the separator, and bracketed components.
+ ///
+ ///
+ public static TheoryData ValidMultipartIdentifierVariations =>
+ new()
+ {
+ // Two parts, bracketed and unbracketed
+ { "[word1].[word2]", ["word1", "word2"] },
+ { "word1.word2", ["word1", "word2"] },
+ // Two parts, one of which is empty
+ { ".word2", ["", "word2"] },
+ { "word1.", ["word1", ""] },
+ // Two parts, with whitespace around the separator
+ { "word1 .word2", ["word1", "word2"] },
+ { "word1. word2", ["word1", "word2"] },
+ { "word1 . word2", ["word1", "word2"] },
+ { "[word1] .[word2]", ["word1", "word2"] },
+ { "[word1]. [word2]", ["word1", "word2"] },
+ { "[word1] . [word2]", ["word1", "word2"] },
+ // Three parts, one of which is empty
+ { ".word2.word3", ["", "word2", "word3"] },
+ { "word1..word3", ["word1", "", "word3"] },
+ { "word1.word2.", ["word1", "word2", ""] },
+ // Four parts, one of which is empty
+ { ".word2.word3.word4", ["", "word2", "word3", "word4"] },
+ { "word1..word3.word4", ["word1", "", "word3", "word4"] },
+ { "word1.word2..word4", ["word1", "word2", "", "word4"] },
+ { "word1.word2.word3.", ["word1", "word2", "word3", ""] },
+ };
+
+ ///
+ /// Gets a collection of test cases representing various formats of invalid part identifiers.
+ ///
+ ///
+ /// This property provides examples of single part identifiers with mismatched brackets, invalid bracket
+ /// placement, unclosed brackets or quotes, and cases with more parts than expected.
+ /// These cases are intended to test the parser's ability to correctly identify and reject invalid formats.
+ ///
+ ///
+ public static TheoryData InvalidSinglePartIdentifierVariations =>
+ new()
+ {
+ // Empty string
+ { "" },
+ // Bracket halfway through a part
+ { "word1[word2" },
+ { "word1]word2" },
+ { "word1\"word2" },
+ // Invalid bracket placement (i.e. starts with a close bracket or ends with an open bracket)
+ { "]word1" },
+ { "word1[" },
+ // Unclosed brackets or quotes
+ { "[word1" },
+ { "\"word1" },
+ // Part starts with one bracket and ends with another
+ { "[word1\"" },
+ { "\"word1]" },
+ // More parts than expected, in various conditions.
+ // Additional part is empty
+ { "word1." },
+ // Additional part, wrapped in brackets
+ { "word1.word2" },
+ { "word1.[word2]" },
+ { "word1.\"word2\"" },
+ // Additional part, with whitespace before or after the separator
+ { "word1 .word2" },
+ { "word1. word2" },
+ // Additional part, with whitespace after the second part
+ { "word1.word2 " },
+ { "word1.[word2 ]" },
+ { "word1.\"word2 \"" },
+ // Additional part, with separator after the second part
+ { "word1.word2." },
+ { "word1.word2.word3" },
+ { "word1.[word2]." },
+ { "word1.\"word2\"." },
+ { "word1.[word2].word3" },
+ { "word1.\"word2\".word3" },
+ };
+
+ ///
+ /// Gets a collection of test cases representing various formats and structures of invalid multipart identifiers.
+ ///
+ ///
+ /// This property provides examples of multipart identifiers with trailing non-whitespace characters after bracketed
+ /// parts, and cases with whitespace followed by non-whitespace characters after bracketed parts.
+ /// These cases are intended to test the parser's ability to correctly identify and reject invalid multipart identifier formats.
+ ///
+ ///
+ public static TheoryData InvalidMultipartIdentifierVariations =>
+ new()
+ {
+ // Bracketed part with trailing non-whitespace characters
+ { "[foo]!.[bar]", 2 },
+ { "\"foo\"!.\"bar\"", 2 },
+ { "[foo].[bar]!", 2 },
+ { "\"foo\".\"bar\"!", 2 },
+ // Bracketed part with trailing whitespace followed by non-whitespace characters
+ { "[foo] !.[bar]", 2 },
+ { "\"foo\" !.\"bar\"", 2 },
+ { "[foo]. ![bar]", 2 },
+ { "\"foo\". !\"bar\"", 2 },
+ { "[foo].[bar] !", 2 },
+ { "\"foo\".\"bar\" !", 2 },
+ };
+
+ ///
+ /// Gets a collection of test cases representing the results of processing an identifier with fewer parts than expected.
+ ///
+ ///
+ public static TheoryData OvercountMultipartIdentifierVariations =>
+ new()
+ {
+ { "word1", [null, "word1"], 2 },
+ { "word1", [null, null, "word1"], 3 },
+ { "word1", [null, null, null, "word1"], 4 },
+
+ { "word1.word2", [null, "word1", "word2"], 3 },
+ { "word1.word2", [null, null, "word1", "word2"], 4 },
+
+ { "word1.word2.word3", [null, "word1", "word2", "word3"], 4 },
+ };
+
+ ///
+ /// Verifies that one part in an identifier parses successfully when it contains various
+ /// combinations of brackets and whitespace, and that the expected value is returned.
+ ///
+ /// The raw identifier to parse.
+ /// The expected output of parsing the identifier.
+ [Theory]
+ [MemberData(nameof(ValidSinglePartIdentifierVariations))]
+ public void SinglePartIdentifierWithBracketsAndWhiteSpace_ParsesSuccessfully(string partIdentifier, string[] expected) =>
+ RunParse(partIdentifier, expected);
+
+ ///
+ /// Verifies that a multi-part identifier parses successfully when its parts are combinations
+ /// of bracketed and unbracketed identifiers, with various placements of whitespace, and that
+ /// the expected values are returned.
+ ///
+ /// The raw identifier to parse.
+ /// The expected output of parsing the identifier.
+ [Theory]
+ [MemberData(nameof(ValidMultipartIdentifierVariations))]
+ public void MultipartIdentifier_ParsesSuccessfully(string partIdentifier, string[] expected) =>
+ RunParse(partIdentifier, expected);
+
+ ///
+ /// Verifies that parsing one part in an identifier throws an exception when it is invalid.
+ /// This encompasses mismatched, misplaced or unclosed brackets and more parts than expected,
+ /// in various combinations with whitespace.
+ ///
+ /// The raw identifier to parse.
+ [Theory]
+ [MemberData(nameof(InvalidSinglePartIdentifierVariations))]
+ public void InvalidSinglePartIdentifier_Throws(string partIdentifier) =>
+ ThrowParse(partIdentifier, expectedLength: 1);
+
+ ///
+ /// Verifies that parsing a multi-part identifier throws an exception when it is invalid (such as
+ /// containing non-whitespace characters between a closing bracket and the separator, or between a
+ /// closing bracket and the end of the string.)
+ ///
+ /// The raw identifier to parse.
+ /// The expected number of components in the identifier.
+ [Theory]
+ [MemberData(nameof(InvalidMultipartIdentifierVariations))]
+ public void InvalidMultipartIdentifier_Throws(string partIdentifier, int expectedLength) =>
+ ThrowParse(partIdentifier, expectedLength);
+
+ ///
+ /// Verifies that when a multipart identifier contains fewer parts than expected, the parser fills the
+ /// missing elements in the array with null values (starting from the first element) and successfully
+ /// parses the identifier.
+ ///
+ /// The raw identifier to parse.
+ /// The expected output of parsing the identifier.
+ /// The number of parts which the part parsing should normally expect.
+ [Theory]
+ [MemberData(nameof(OvercountMultipartIdentifierVariations))]
+ public void SingleUnbracketedOvercount_FillsFirstElementsWithNull(string partIdentifier, string?[] parts, int maxCount) =>
+ RunParse(partIdentifier, parts, maxCount);
+
+ ///
+ /// Verifies that multipart identifier strings containing zero-length segments are parsed into the expected
+ /// array of empty strings.
+ ///
+ ///
+ /// This test case contrasts with , where the
+ /// input is a completely empty string rather than a multipart identifier with empty segments.
+ ///
+ /// The raw identifier to parse.
+ [Theory]
+ [InlineData("[].[].[].[]")]
+ [InlineData("...")]
+ [InlineData(".[].[].")]
+ [InlineData("[]...[]")]
+ [InlineData(" . . . ")]
+ [InlineData(" []. [] . [] .[] ")]
+ public void MultipartIdentifierOfZeroLengthParts_ParsesSuccessfully(string partIdentifier) =>
+ RunParse(partIdentifier, ["", "", "", ""]);
+
+ ///
+ /// Verifies that parsing a multipart identifier with more parts than expected throws an exception.
+ ///
+ /// The number of parts which the part parsing should normally expect.
+ [Theory]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(3)]
+ public void MultipartIdentifierWithMorePartsThanExpected_Throws(int maxCount) =>
+ ThrowParse("word1.word2.word3.word4", maxCount);
+
+ ///
+ /// Verifies that parsing an empty multipart identifier with the throwOnEmpty flag set to false returns an array
+ /// of nulls with the specified number of parts (rather than throwing an exception.)
+ ///
+ /// The expected number of components in the identifier.
+ [Theory]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(3)]
+ [InlineData(4)]
+ public void EmptyMultipartIdentifierWithThrowOnEmptyFalse_ReturnsArrayOfNulls(int expectedLength) =>
+ RunParse("", new string?[expectedLength], expectedLength, throwOnEmpty: false);
+
+ ///
+ /// Verifies that parsing an empty multipart identifier with the throwOnEmpty flag set to true throws an exception.
+ ///
+ ///
+ /// This test case contrasts with , where the
+ /// input is a multipart identifier with empty segments rather than a completely empty string.
+ ///
+ /// The expected number of components in the identifier.
+ [Theory]
+ [InlineData(1)]
+ [InlineData(2)]
+ [InlineData(3)]
+ [InlineData(4)]
+ public void EmptyMultipartIdentifierWithThrowOnEmptyTrue_Throws(int expectedLength) =>
+ ThrowParse("", expectedLength);
+
+ private static void RunParse(string name, string?[] expected, int maxCount = 0, bool throwOnEmpty = true)
+ {
+ if (maxCount == 0)
+ {
+ for (int index = 0; index < expected.Length; index++)
+ {
+ if (expected[index] != null)
+ {
+ maxCount += 1;
+ }
+ }
+ }
+
+ string?[] originalParts = MultipartIdentifier.ParseMultipartIdentifier(name, "", throwOnEmpty, maxCount);
+
+ Assert.Equal(expected.Length, originalParts.Length);
+ for (int index = 0; index < expected.Length; index++)
+ {
+ string? expectedPart = expected[index];
+ string? originalPart = originalParts[index];
+
+ Assert.Equal(expectedPart, originalPart);
+ }
+ }
+
+ private static void ThrowParse(string name, int expectedLength)
+ {
+ ArgumentException originalException = Assert.Throws(() =>
+ MultipartIdentifier.ParseMultipartIdentifier(name, "test", true, expectedLength)
+ );
+
+ Assert.NotNull(originalException);
+ }
+
+ private static IEnumerable<(string, string)> GeneratePartCombinations(string word)
+ {
+ Debug.Assert(word is "word1" or "word 1");
+ (string OpeningBracket, string ClosingBracket)[] bracketCombinations = [("[", "]"), ("\"", "\"")];
+
+ // Combinations of whitespace, contained entirely within various combinations of brackets
+ foreach (string wsCombination in GenerateWhitespaceCombinations(word))
+ {
+ foreach ((string openingBracket, string closingBracket) in bracketCombinations)
+ {
+ foreach ((string bracketCombination, string expectedValue) in GenerateBracketCombinations(wsCombination, openingBracket, closingBracket))
+ {
+ yield return (bracketCombination, expectedValue);
+ }
+ }
+ }
+
+ // Combinations of brackets, with whitespace outside the brackets
+ foreach ((string openingBracket, string closingBracket) in bracketCombinations)
+ {
+ foreach ((string bracketCombination, string unbracketedValue) in GenerateBracketCombinations(word, openingBracket, closingBracket))
+ {
+ foreach (string wsCombination in GenerateWhitespaceCombinations(bracketCombination))
+ {
+ yield return (wsCombination, unbracketedValue);
+ }
+ }
+ }
+
+ static IEnumerable GenerateWhitespaceCombinations(string word)
+ {
+ yield return word;
+ yield return $" {word}";
+ yield return $"{word} ";
+ yield return $" {word} ";
+ }
+
+ static IEnumerable<(string Combination, string Expected)> GenerateBracketCombinations(string word, string openingBracket, string closingBracket)
+ {
+ yield return (word, word.Trim());
+ yield return (openingBracket + word + closingBracket, word);
+ yield return (openingBracket + word.Insert(word.Length - 3, closingBracket + closingBracket) + closingBracket, word.Insert(word.Length - 3, closingBracket));
+ }
+ }
+}