diff --git a/SqlScriptDom/ScriptDom/SqlServer/IdentifierCasing.cs b/SqlScriptDom/ScriptDom/SqlServer/IdentifierCasing.cs new file mode 100644 index 0000000..02c59f2 --- /dev/null +++ b/SqlScriptDom/ScriptDom/SqlServer/IdentifierCasing.cs @@ -0,0 +1,38 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.SqlServer.TransactSql.ScriptDom +{ + /// + /// Represents the possible ways of casing SQL identifiers + /// + public enum IdentifierCasing + { + /// + /// Preserve original casing + /// + PreserveOriginal, + + /// + /// All letters in lower case + /// + Lowercase, + + /// + /// All letters in upper case + /// + Uppercase, + + /// + /// First letter of each word capitalized, remaining letters lower case + /// + PascalCase + } +} diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptGeneratorSupporter.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptGeneratorSupporter.cs index 9e9abe0..189e318 100644 --- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptGeneratorSupporter.cs +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptGeneratorSupporter.cs @@ -40,10 +40,10 @@ public static Int32 TokenTypeCount } /// - /// Retrieves a version of the specified string, in the casing format specified + /// Retrieves a version of the specified string, in the keyword casing format specified /// /// The string to get a specially cased version of - /// The casing method to use + /// The keyword casing method to use /// A version of the string in the casing format specified in [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")] public static string GetCasedString(string str, KeywordCasing casing) @@ -63,6 +63,33 @@ public static string GetCasedString(string str, KeywordCasing casing) return String.Empty; } + /// + /// Retrieves a version of the specified string, in the identifier casing format specified + /// + /// The string to get a specially cased version of + /// The identifier casing method to use + /// A version of the string in the casing format specified in + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")] + public static string GetCasedString(string str, IdentifierCasing casing) + { + switch (casing) + { + case IdentifierCasing.PreserveOriginal: + // No transformation - preserve original casing + return str; + case IdentifierCasing.Lowercase: + return str.ToLowerInvariant(); + case IdentifierCasing.Uppercase: + return str.ToUpperInvariant(); + case IdentifierCasing.PascalCase: + return GetPascalCase(str); + default: + Debug.Fail("Invalid IdentifierCasing value"); + break; + } + return String.Empty; + } + /// /// Retrieves a Pascal Cased version of the string /// diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs index 85d2fac..bf2df1a 100644 --- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs @@ -170,8 +170,14 @@ private void AddIdentifier(String text, Boolean applyCasing) { if (applyCasing) { + // Apply keyword casing (for identifiers that are actually keywords) text = ScriptGeneratorSupporter.GetCasedString(text, _options.KeywordCasing); } + else + { + // Apply identifier casing (for actual identifiers) + text = ScriptGeneratorSupporter.GetCasedString(text, _options.IdentifierCasing); + } TSqlParserToken token = new TSqlParserToken(TSqlTokenType.Identifier, text); AddToken(token); diff --git a/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml b/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml index 551033a..aa56b18 100644 --- a/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml +++ b/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml @@ -14,6 +14,15 @@ Gets or sets the keyword casing option to use during script generation + + IdentifierCasing_Title + IdentifierCasing_Description + + + + + Gets or sets the identifier casing option to use during script generation + Gets or sets the Sql version to generate script for diff --git a/Test/SqlDom/ScriptGeneratorTests.cs b/Test/SqlDom/ScriptGeneratorTests.cs index 9a92ce3..3ac9c7f 100644 --- a/Test/SqlDom/ScriptGeneratorTests.cs +++ b/Test/SqlDom/ScriptGeneratorTests.cs @@ -334,5 +334,252 @@ void ParseAndAssertEquality(string sqlText, SqlScriptGeneratorOptions generatorO Assert.AreEqual(sqlText, generatedSqlText); } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestIdentifierCasingDefault() + { + Assert.AreEqual(IdentifierCasing.PreserveOriginal, new SqlScriptGeneratorOptions().IdentifierCasing); + } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestIdentifierCasingPreserveOriginal() + { + var expectedSqlText = @"CREATE TABLE MyTableName ( + MyColumnName VARCHAR (50), + anotherColumn INT, + MixedCaseColumn DECIMAL (10, 2) +);"; + + ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions { + IdentifierCasing = IdentifierCasing.PreserveOriginal, + AlignColumnDefinitionFields = false + }); + } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestIdentifierCasingLowercase() + { + var inputSqlText = @"CREATE TABLE MyTableName ( + MyColumnName VARCHAR (50), + AnotherColumn INT, + MixedCaseColumn DECIMAL (10, 2) +);"; + + var expectedSqlText = @"CREATE TABLE mytablename ( + mycolumnname VARCHAR (50), + anothercolumn INT, + mixedcasecolumn DECIMAL (10, 2) +);"; + + ParseTransformAndAssertEquality(inputSqlText, expectedSqlText, new SqlScriptGeneratorOptions { + IdentifierCasing = IdentifierCasing.Lowercase, + AlignColumnDefinitionFields = false + }); + } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestIdentifierCasingUppercase() + { + var inputSqlText = @"CREATE TABLE MyTableName ( + MyColumnName VARCHAR (50), + anotherColumn INT, + MixedCaseColumn DECIMAL (10, 2) +);"; + + var expectedSqlText = @"CREATE TABLE MYTABLENAME ( + MYCOLUMNNAME VARCHAR (50), + ANOTHERCOLUMN INT, + MIXEDCASECOLUMN DECIMAL (10, 2) +);"; + + ParseTransformAndAssertEquality(inputSqlText, expectedSqlText, new SqlScriptGeneratorOptions { + IdentifierCasing = IdentifierCasing.Uppercase, + AlignColumnDefinitionFields = false + }); + } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestIdentifierCasingPascalCase() + { + var inputSqlText = @"CREATE TABLE MyTableName ( + MyColumnName VARCHAR (50), + anotherColumn INT, + MIXEDCASECOLUMN DECIMAL (10, 2) +);"; + + var expectedSqlText = @"CREATE TABLE Mytablename ( + Mycolumnname VARCHAR (50), + Anothercolumn INT, + Mixedcasecolumn DECIMAL (10, 2) +);"; + + ParseTransformAndAssertEquality(inputSqlText, expectedSqlText, new SqlScriptGeneratorOptions { + IdentifierCasing = IdentifierCasing.PascalCase, + AlignColumnDefinitionFields = false + }); + } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestIdentifierCasingWithAccentedCharactersLowercase() + { + var inputSqlText = @"CREATE TABLE [Příliš Žluťoučký Kůň] ( + [Úpěl Ďábelské] VARCHAR (50), + [Ódy] INT +);"; + + var expectedSqlText = @"CREATE TABLE [příliš žluťoučký kůň] ( + [úpěl ďábelské] VARCHAR (50), + [ódy] INT +);"; + + ParseTransformAndAssertEquality(inputSqlText, expectedSqlText, new SqlScriptGeneratorOptions { + IdentifierCasing = IdentifierCasing.Lowercase, + AlignColumnDefinitionFields = false + }); + } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestIdentifierCasingWithAccentedCharactersUppercase() + { + var inputSqlText = @"CREATE TABLE [Příliš Žluťoučký Kůň] ( + [Úpěl Ďábelské] VARCHAR (50), + [Ódy] INT +);"; + + var expectedSqlText = @"CREATE TABLE [PŘÍLIŠ ŽLUŤOUČKÝ KŮŇ] ( + [ÚPĚL ĎÁBELSKÉ] VARCHAR (50), + [ÓDY] INT +);"; + + ParseTransformAndAssertEquality(inputSqlText, expectedSqlText, new SqlScriptGeneratorOptions { + IdentifierCasing = IdentifierCasing.Uppercase, + AlignColumnDefinitionFields = false + }); + } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestIdentifierCasingWithAccentedCharactersPascalCase() + { + var inputSqlText = @"CREATE TABLE [PŘÍLIŠ ŽLUŤOUČKÝ KŮŇ] ( + [ÚPĚL ĎÁBELSKÉ ÓDY] VARCHAR (50), + [ódy] INT +);"; + + // Note: PascalCase with quoted identifiers containing spaces produces lowercase after first character + // because the GetPascalCase method only capitalizes the first character of the entire string + var expectedSqlText = @"CREATE TABLE [příliš žluťoučký kůň] ( + [úpěl ďábelské ódy] VARCHAR (50), + [ódy] INT +);"; + + ParseTransformAndAssertEquality(inputSqlText, expectedSqlText, new SqlScriptGeneratorOptions { + IdentifierCasing = IdentifierCasing.PascalCase, + AlignColumnDefinitionFields = false + }); + } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestIdentifierCasingDoesNotAffectKeywords() + { + var inputSqlText = @"Create Table MyTable ( + MyColumn VarChar (50) not null, + MyId Int Primary Key +);"; + + var expectedSqlTextLowercaseKeywords = @"create table MYTABLE ( + MYCOLUMN varchar (50) not null, + MYID int primary key +);"; + + var expectedSqlTextUppercaseKeywords = @"CREATE TABLE mytable ( + mycolumn VARCHAR (50) NOT NULL, + myid INT PRIMARY KEY +);"; + + var parser = new TSql160Parser(true); + var fragment = parser.ParseStatementList(new StringReader(inputSqlText), out var errors); + + Assert.AreEqual(0, errors.Count); + + // The setting trigger both keyword and identifier casing, set opposite to each other, + // and hence it's clear which part is affected by which setting. + + // Test with lowercase keywords and uppercase identifiers + var generatorLowerKeywords = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { + KeywordCasing = KeywordCasing.Lowercase, + IdentifierCasing = IdentifierCasing.Uppercase, + AlignColumnDefinitionFields = false + }); + generatorLowerKeywords.GenerateScript(fragment, out var generatedSqlTextLower); + Assert.AreEqual(expectedSqlTextLowercaseKeywords, generatedSqlTextLower); + + // Test with uppercase keywords and lowercase identifiers + var generatorUpperKeywords = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { + KeywordCasing = KeywordCasing.Uppercase, + IdentifierCasing = IdentifierCasing.Lowercase, + AlignColumnDefinitionFields = false + }); + generatorUpperKeywords.GenerateScript(fragment, out var generatedSqlTextUpper); + Assert.AreEqual(expectedSqlTextUppercaseKeywords, generatedSqlTextUpper); + } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestIdentifierCasingWithComplexStatement() + { + var inputSqlText = @"SELECT T1.MyColumn, + T2.AnotherColumn +FROM MySchema.MyTable AS T1 + INNER JOIN AnotherSchema.AnotherTable AS T2 ON T1.Id = T2.ForeignId +WHERE T1.StatusCode = 'ACTIVE';"; + + var expectedSqlText = @"SELECT t1.mycolumn, + t2.anothercolumn +FROM myschema.mytable AS t1 + INNER JOIN + anotherschema.anothertable AS t2 + ON t1.id = t2.foreignid +WHERE t1.statuscode = 'ACTIVE';"; + + ParseTransformAndAssertEquality(inputSqlText, expectedSqlText, new SqlScriptGeneratorOptions { + IdentifierCasing = IdentifierCasing.Lowercase, + AlignClauseBodies = true, + NewLineBeforeFromClause = true, + NewLineBeforeWhereClause = true, + NewLineBeforeJoinClause = true + }); + } + + void ParseTransformAndAssertEquality(string inputSqlText, string expectedSqlText, SqlScriptGeneratorOptions generatorOptions) + { + var parser = new TSql160Parser(true); + var fragment = parser.ParseStatementList(new StringReader(inputSqlText), out var errors); + + Assert.AreEqual(0, errors.Count); + + var generator = new Sql160ScriptGenerator(generatorOptions); + generator.GenerateScript(fragment, out var generatedSqlText); + + Assert.AreEqual(expectedSqlText, generatedSqlText); + } } }