Adding decoding of multipart identifiers, default schema workaround (#295)

This change adds a couple things

_Multipart Identifier Decoding_
The ability to decode a multipart identifier (with or without escaping) has been added to the SqlScriptFormatter utility class. This code is utilized to split a table name provided to the edit/initialize request into schema and table name.

_Default Schema Workaround_
The code that retrieves the SMO metadata objects originally used the `[]` operator to access the objects. Due to a bug(?) in SMO, this results in problems when loading tables without a default schema (in our case if you're logged in as SA). Using the metadata object constructors gets around this issue, we are explicitly using them.

* Adding decoding of multipart identifiers
Adding code fix for default schema issue

* Adding some more localizable strings for errors when loading metadata

* Adding localization files... again?

* Changes as per pull request comments
This commit is contained in:
Benjamin Russell
2017-03-27 17:14:21 -07:00
committed by GitHub
parent 1909310a92
commit f7036f3f73
11 changed files with 235 additions and 17 deletions

View File

@@ -15,6 +15,7 @@ using Microsoft.SqlTools.ServiceLayer.EditData.Contracts;
using Microsoft.SqlTools.ServiceLayer.EditData.UpdateManagement;
using Microsoft.SqlTools.ServiceLayer.QueryExecution;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.EditData
@@ -423,14 +424,14 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
try
{
// Step 1) Look up the SMO metadata
objectMetadata = metadataFactory.GetObjectMetadata(await connector(), initParams.ObjectName,
string[] namedParts = SqlScriptFormatter.DecodeMultipartIdenfitier(initParams.ObjectName);
objectMetadata = metadataFactory.GetObjectMetadata(await connector(), namedParts,
initParams.ObjectType);
// Step 2) Get and execute a query for the rows in the object we're looking up
EditSessionQueryExecutionState state = await queryRunner(ConstructInitializeQuery(objectMetadata, initParams.Filters));
if (state.Query == null)
{
// TODO: Move to SR file
string message = state.Message ?? SR.EditDataQueryFailed;
throw new Exception(message);
}

View File

@@ -16,9 +16,13 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// Generates a edit-ready metadata object
/// </summary>
/// <param name="connection">Connection to use for getting metadata</param>
/// <param name="objectName">Name of the object to return metadata for</param>
/// <param name="objectNamedParts">
/// The multipart name for the object split and unwrapped. At most two components can be
/// provided (schema, table/view name). At minimum table/view name can be provided, and
/// default schema will be used for schema name.
/// </param>
/// <param name="objectType">Type of the object to return metadata for</param>
/// <returns>Metadata about the object requested</returns>
EditTableMetadata GetObjectMetadata(DbConnection connection, string objectName, string objectType);
EditTableMetadata GetObjectMetadata(DbConnection connection, string[] objectNamedParts, string objectType);
}
}

View File

@@ -11,6 +11,7 @@ using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.EditData
{
@@ -23,11 +24,21 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
/// Generates a edit-ready metadata object using SMO
/// </summary>
/// <param name="connection">Connection to use for getting metadata</param>
/// <param name="objectName">Name of the object to return metadata for</param>
/// <param name="objectNamedParts">Split and unwrapped name parts</param>
/// <param name="objectType">Type of the object to return metadata for</param>
/// <returns>Metadata about the object requested</returns>
public EditTableMetadata GetObjectMetadata(DbConnection connection, string objectName, string objectType)
public EditTableMetadata GetObjectMetadata(DbConnection connection, string[] objectNamedParts, string objectType)
{
Validate.IsNotNull(nameof(objectNamedParts), objectNamedParts);
if (objectNamedParts.Length <= 0)
{
throw new ArgumentNullException(nameof(objectNamedParts), SR.EditDataMetadataObjectNameRequired);
}
if (objectNamedParts.Length > 2)
{
throw new InvalidOperationException(SR.EditDataMetadataTooManyIdentifiers);
}
// Get a connection to the database for SMO purposes
SqlConnection sqlConn = connection as SqlConnection;
if (sqlConn == null)
@@ -46,22 +57,23 @@ namespace Microsoft.SqlTools.ServiceLayer.EditData
// Connect with SMO and get the metadata for the table
Server server = new Server(new ServerConnection(sqlConn));
Database db = new Database(server, sqlConn.Database);
TableViewTableTypeBase smoResult;
switch (objectType.ToLowerInvariant())
{
case "table":
smoResult = server.Databases[sqlConn.Database].Tables[objectName];
smoResult = objectNamedParts.Length == 1
? new Table(db, objectNamedParts[0]) // No schema provided
: new Table(db, objectNamedParts[1], objectNamedParts[0]); // Schema provided
break;
case "view":
smoResult = server.Databases[sqlConn.Database].Views[objectName];
smoResult = objectNamedParts.Length == 1
? new View(db, objectNamedParts[0]) // No schema provided
: new View(db, objectNamedParts[1], objectNamedParts[0]); // Schema provided
break;
default:
throw new ArgumentOutOfRangeException(nameof(objectType), SR.EditDataUnsupportedObjectType(objectType));
}
if (smoResult == null)
{
throw new ArgumentOutOfRangeException(nameof(objectName), SR.EditDataObjectMetadataNotFound);
}
// Generate the edit column metadata
List<EditColumnMetadata> editColumns = new List<EditColumnMetadata>();

View File

@@ -429,6 +429,22 @@ namespace Microsoft.SqlTools.ServiceLayer
}
}
public static string EditDataMetadataObjectNameRequired
{
get
{
return Keys.GetString(Keys.EditDataMetadataObjectNameRequired);
}
}
public static string EditDataMetadataTooManyIdentifiers
{
get
{
return Keys.GetString(Keys.EditDataMetadataTooManyIdentifiers);
}
}
public static string EditDataFilteringNegativeLimit
{
get
@@ -861,6 +877,14 @@ namespace Microsoft.SqlTools.ServiceLayer
}
}
public static string SqlScriptFormatterMultipartDecodeFail
{
get
{
return Keys.GetString(Keys.SqlScriptFormatterMultipartDecodeFail);
}
}
public static string ConnectionServiceListDbErrorNotConnected(string uri)
{
return Keys.GetString(Keys.ConnectionServiceListDbErrorNotConnected, uri);
@@ -1128,6 +1152,12 @@ namespace Microsoft.SqlTools.ServiceLayer
public const string EditDataMetadataNotExtended = "EditDataMetadataNotExtended";
public const string EditDataMetadataObjectNameRequired = "EditDataMetadataObjectNameRequired";
public const string EditDataMetadataTooManyIdentifiers = "EditDataMetadataTooManyIdentifiers";
public const string EditDataFilteringNegativeLimit = "EditDataFilteringNegativeLimit";
@@ -1293,6 +1323,9 @@ namespace Microsoft.SqlTools.ServiceLayer
public const string SqlScriptFormatterDecimalMissingPrecision = "SqlScriptFormatterDecimalMissingPrecision";
public const string SqlScriptFormatterMultipartDecodeFail = "SqlScriptFormatterMultipartDecodeFail";
private Keys()
{ }

View File

@@ -382,6 +382,14 @@
<value>Table metadata does not have extended properties</value>
<comment></comment>
</data>
<data name="EditDataMetadataObjectNameRequired" xml:space="preserve">
<value>A object name must be provided</value>
<comment></comment>
</data>
<data name="EditDataMetadataTooManyIdentifiers" xml:space="preserve">
<value>Explicitly specifying server or database is not supported</value>
<comment></comment>
</data>
<data name="EditDataFilteringNegativeLimit" xml:space="preserve">
<value>Result limit cannot be negative</value>
<comment></comment>
@@ -603,4 +611,8 @@
<value>Decimal column is missing numeric precision or numeric scale</value>
<comment></comment>
</data>
<data name="SqlScriptFormatterMultipartDecodeFail" xml:space="preserve">
<value>Multipart identifier is incorrectly formatted</value>
<comment></comment>
</data>
</root>

View File

@@ -180,6 +180,10 @@ EditDataSessionAlreadyInitializing = Edit session has already been initialized o
EditDataMetadataNotExtended = Table metadata does not have extended properties
EditDataMetadataObjectNameRequired = A object name must be provided
EditDataMetadataTooManyIdentifiers = Explicitly specifying server or database is not supported
EditDataFilteringNegativeLimit = Result limit cannot be negative
EditDataUnsupportedObjectType(string typeName) = Database object {0} cannot be used for editing.
@@ -298,3 +302,5 @@ TestLocalizationConstant = EN_LOCALIZATION
# Utilities
SqlScriptFormatterDecimalMissingPrecision = Decimal column is missing numeric precision or numeric scale
SqlScriptFormatterMultipartDecodeFail = Multipart identifier is incorrectly formatted

View File

@@ -601,6 +601,21 @@
<target state="new">NULL</target>
<note></note>
</trans-unit>
<trans-unit id="SqlScriptFormatterMultipartDecodeFail">
<source>Multipart identifier is incorrectly formatted</source>
<target state="new">Multipart identifier is incorrectly formatted</target>
<note></note>
</trans-unit>
<trans-unit id="EditDataMetadataObjectNameRequired">
<source>A object name must be provided</source>
<target state="new">A object name must be provided</target>
<note></note>
</trans-unit>
<trans-unit id="EditDataMetadataTooManyIdentifiers">
<source>Explicitly specifying server or database is not supported</source>
<target state="new">Explicitly specifying server or database is not supported</target>
<note></note>
</trans-unit>
<trans-unit id="EditDataMetadataNotExtended">
<source>Table metadata does not have extended properties</source>
<target state="new">Table metadata does not have extended properties</target>

View File

@@ -176,6 +176,83 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility
return literal;
}
public static string[] DecodeMultipartIdenfitier(string multipartIdentifier)
{
StringBuilder sb = new StringBuilder();
List<string> namedParts = new List<string>();
bool insideBrackets = false;
bool bracketsClosed = false;
for (int i = 0; i < multipartIdentifier.Length; i++)
{
char iChar = multipartIdentifier[i];
if (insideBrackets)
{
if (iChar == ']')
{
if (HasNextCharacter(multipartIdentifier, ']', i))
{
// This is an escaped ]
sb.Append(iChar);
i++;
}
else
{
// This bracket closes the bracket we were in
insideBrackets = false;
bracketsClosed = true;
}
}
else
{
// This is a standard character
sb.Append(iChar);
}
}
else
{
switch (iChar)
{
case '[':
if (bracketsClosed)
{
throw new FormatException();
}
// We're opening a set of brackets
insideBrackets = true;
bracketsClosed = false;
break;
case '.':
if (sb.Length == 0)
{
throw new FormatException();
}
// We're splitting the identifier into a new part
namedParts.Add(sb.ToString());
sb = new StringBuilder();
bracketsClosed = false;
break;
default:
if (bracketsClosed)
{
throw new FormatException();
}
// This is a standard character
sb.Append(iChar);
break;
}
}
}
if (sb.Length == 0)
{
throw new FormatException();
}
namedParts.Add(sb.ToString());
return namedParts.ToArray();
}
#region Private Helpers
private static string SimpleFormatter(object value)
@@ -260,6 +337,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Utility
return "0x" + BitConverter.ToString(bytes).Replace("-", string.Empty);
}
private static bool HasNextCharacter(string haystack, char needle, int position)
{
return position + 1 < haystack.Length
&& haystack[position + 1] == needle;
}
/// <summary>
/// Returns a valid SQL string packaged in single quotes with single quotes inside escaped
/// </summary>

View File

@@ -42,7 +42,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
// Mock metadata factory
Mock<IEditMetadataFactory> metaFactory = new Mock<IEditMetadataFactory>();
metaFactory
.Setup(f => f.GetObjectMetadata(It.IsAny<DbConnection>(), It.IsAny<string>(), It.IsAny<string>()))
.Setup(f => f.GetObjectMetadata(It.IsAny<DbConnection>(), It.IsAny<string[]>(), It.IsAny<string>()))
.Returns(etm);
EditSession session = new EditSession(metaFactory.Object);

View File

@@ -379,7 +379,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
// Setup:
// ... Create a metadata factory that throws
Mock<IEditMetadataFactory> emf = new Mock<IEditMetadataFactory>();
emf.Setup(f => f.GetObjectMetadata(It.IsAny<DbConnection>(), It.IsAny<string>(), It.IsAny<string>()))
emf.Setup(f => f.GetObjectMetadata(It.IsAny<DbConnection>(), It.IsAny<string[]>(), It.IsAny<string>()))
.Throws<Exception>();
// ... Create a session that hasn't been initialized
@@ -412,7 +412,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
var b = QueryExecution.Common.GetBasicExecutedBatch();
var etm = Common.GetStandardMetadata(b.ResultSets[0].Columns);
Mock<IEditMetadataFactory> emf = new Mock<IEditMetadataFactory>();
emf.Setup(f => f.GetObjectMetadata(It.IsAny<DbConnection>(), It.IsAny<string>(), It.IsAny<string>()))
emf.Setup(f => f.GetObjectMetadata(It.IsAny<DbConnection>(), It.IsAny<string[]>(), It.IsAny<string>()))
.Returns(etm);
// ... Create a session that hasn't been initialized
@@ -451,7 +451,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
var b = QueryExecution.Common.GetBasicExecutedBatch();
var etm = Common.GetStandardMetadata(b.ResultSets[0].Columns);
Mock<IEditMetadataFactory> emf = new Mock<IEditMetadataFactory>();
emf.Setup(f => f.GetObjectMetadata(It.IsAny<DbConnection>(), It.IsAny<string>(), It.IsAny<string>()))
emf.Setup(f => f.GetObjectMetadata(It.IsAny<DbConnection>(), It.IsAny<string[]>(), It.IsAny<string>()))
.Returns(etm);
// ... Create a session that hasn't been initialized
@@ -490,7 +490,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.EditData
var rs = q.Batches[0].ResultSets[0];
var etm = Common.GetStandardMetadata(rs.Columns);
Mock<IEditMetadataFactory> emf = new Mock<IEditMetadataFactory>();
emf.Setup(f => f.GetObjectMetadata(It.IsAny<DbConnection>(), It.IsAny<string>(), It.IsAny<string>()))
emf.Setup(f => f.GetObjectMetadata(It.IsAny<DbConnection>(), It.IsAny<string[]>(), It.IsAny<string>()))
.Returns(etm);
// ... Create a session that hasn't been initialized

View File

@@ -7,6 +7,8 @@ using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Text.RegularExpressions;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlServer.Management.SqlParser.SqlCodeDom;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Xunit;
@@ -308,6 +310,56 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Utility
#endregion
#region DecodeMultipartIdentifier Tests
[Theory]
[MemberData(nameof(DecodeMultipartIdentifierTestData))]
public void DecodeMultipartIdentifierTest(string input, string[] output)
{
// If: I decode the input
string[] decoded = SqlScriptFormatter.DecodeMultipartIdenfitier(input);
// Then: The output should match what was expected
Assert.Equal(output, decoded);
}
public static IEnumerable<object> DecodeMultipartIdentifierTestData
{
get
{
yield return new object[] {"identifier", new[] {"identifier"}};
yield return new object[] {"simple.split", new[] {"simple", "split"}};
yield return new object[] {"multi.simple.split", new[] {"multi", "simple", "split"}};
yield return new object[] {"[escaped]", new[] {"escaped"}};
yield return new object[] {"[escaped].[split]", new[] {"escaped", "split"}};
yield return new object[] {"[multi].[escaped].[split]", new[] {"multi", "escaped", "split"}};
yield return new object[] {"[escaped]]characters]", new[] {"escaped]characters"}};
yield return new object[] {"[multi]]escaped]]chars]", new[] {"multi]escaped]chars"}};
yield return new object[] {"[multi]]]]chars]", new[] {"multi]]chars"}};
yield return new object[] {"unescaped]chars", new[] {"unescaped]chars"}};
yield return new object[] {"multi]unescaped]chars", new[] {"multi]unescaped]chars"}};
yield return new object[] {"multi]]chars", new[] {"multi]]chars"}};
yield return new object[] {"[escaped.dot]", new[] {"escaped.dot"}};
yield return new object[] {"mixed.[escaped]", new[] {"mixed", "escaped"}};
yield return new object[] {"[escaped].mixed", new[] {"escaped", "mixed"}};
yield return new object[] {"dbo.[[].weird", new[] {"dbo", "[", "weird"}};
}
}
[Theory]
[InlineData("[bracket]closed")]
[InlineData("[bracket][closed")]
[InlineData(".stuff")]
[InlineData(".")]
public void DecodeMultipartIdentifierFailTest(string input)
{
// If: I decode an invalid input
// Then: It should throw an exception
Assert.Throws<FormatException>(() => SqlScriptFormatter.DecodeMultipartIdenfitier(input));
}
#endregion
[Theory]
[InlineData("(0)", "0")]
[InlineData("((0))", "0")]