// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using Microsoft.SqlTools.Utility; namespace Microsoft.SqlTools.ServiceLayer.Utility.SqlScriptFormatters { /// /// Provides utilities for converting from SQL script syntax into POCOs. /// public static class FromSqlScript { // Regex: optionally starts with N, captures string wrapped in single quotes private static readonly Regex StringRegex = new Regex("^N?'(.*)'$", RegexOptions.Compiled); private static readonly Regex BracketRegex = new Regex(@"^\[(.*)\]$", RegexOptions.Compiled); /// /// Decodes a multipart identifier as used in a SQL script into an array of the multiple /// parts of the identifier. Implemented as a state machine that iterates over the /// characters of the multipart identifier. /// /// Multipart identifier to decode (eg, "[dbo].[test]") /// The parts of the multipart identifier in an array (eg, "dbo", "test") /// /// Thrown if an invalid state transition is made, indicating that the multipart identifer /// is not valid. /// public static string[] DecodeMultipartIdentifier(string multipartIdentifier) { StringBuilder sb = new StringBuilder(); List namedParts = new List(); 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(); } /// /// Converts a value from a script into a plain version by unwrapping literal wrappers /// and unescaping characters. /// /// The value to unwrap (eg, "(N'foo''bar')") /// The unwrapped/unescaped literal (eg, "foo'bar") public static string UnwrapLiteral(string literal) { // Always remove parens literal = literal.Trim('(', ')'); // Attempt to unwrap inverted commas around a string Match match = StringRegex.Match(literal); if (match.Success) { // Like: N'stuff' or 'stuff' return UnEscapeString(match.Groups[1].Value, '\''); } return literal; } /// /// Tests whether an identifier is escaped with brackets e.g. [Northwind].[dbo].[Orders] /// /// Identifier to check. /// Boolean indicating if identifier is escaped with brackets. public static bool IsIdentifierBracketed(string identifer) => BracketRegex.IsMatch(identifer); #region Private Helpers private static bool HasNextCharacter(string haystack, char needle, int position) { return position + 1 < haystack.Length && haystack[position + 1] == needle; } private static string UnEscapeString(string value, char escapeCharacter) { Validate.IsNotNull(nameof(value), value); // Replace 2x of the escape character with 1x of the escape character return value.Replace(new string(escapeCharacter, 2), escapeCharacter.ToString()); } #endregion } }