Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions SqlScriptDom/ScriptDom/SqlServer/IdentifierCasing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//------------------------------------------------------------------------------
// <copyright file="IdentifierCasing.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Text;

namespace Microsoft.SqlServer.TransactSql.ScriptDom
{
/// <summary>
/// Represents the possible ways of casing SQL identifiers
/// </summary>
public enum IdentifierCasing
{
/// <summary>
/// Preserve original casing
/// </summary>
PreserveOriginal,

/// <summary>
/// All letters in lower case
/// </summary>
Lowercase,

/// <summary>
/// All letters in upper case
/// </summary>
Uppercase,

/// <summary>
/// First letter of each word capitalized, remaining letters lower case
/// </summary>
PascalCase
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ public static Int32 TokenTypeCount
}

/// <summary>
/// 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
/// </summary>
/// <param name="str">The string to get a specially cased version of</param>
/// <param name="casing">The casing method to use</param>
/// <param name="casing">The keyword casing method to use</param>
/// <returns>A version of the string in the casing format specified in <paramref name="casing"/></returns>
[SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
public static string GetCasedString(string str, KeywordCasing casing)
Expand All @@ -63,6 +63,33 @@ public static string GetCasedString(string str, KeywordCasing casing)
return String.Empty;
}

/// <summary>
/// Retrieves a version of the specified string, in the identifier casing format specified
/// </summary>
/// <param name="str">The string to get a specially cased version of</param>
/// <param name="casing">The identifier casing method to use</param>
/// <returns>A version of the string in the casing format specified in <paramref name="casing"/></returns>
[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;
}

/// <summary>
/// Retrieves a Pascal Cased version of the string
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
<Option value="KeywordCasing.PascalCase">KeywordCasing_PascalCase</Option>
<Summary>Gets or sets the keyword casing option to use during script generation</Summary>
</Setting>
<Setting name="IdentifierCasing" type="IdentifierCasing" default="IdentifierCasing.PreserveOriginal">
<Title>IdentifierCasing_Title</Title>
<Description>IdentifierCasing_Description</Description>
<Option value="IdentifierCasing.PreserveOriginal">IdentifierCasing_PreserveOriginal</Option>
<Option value="IdentifierCasing.Lowercase">IdentifierCasing_LowerCase</Option>
<Option value="IdentifierCasing.Uppercase">IdentifierCasing_UpperCase</Option>
<Option value="IdentifierCasing.PascalCase">IdentifierCasing_PascalCase</Option>
<Summary>Gets or sets the identifier casing option to use during script generation</Summary>
</Setting>
<Setting name="SqlVersion" type="SqlVersion" default="SqlVersion.Sql90" browsable="false">
<Summary>Gets or sets the Sql version to generate script for</Summary>
</Setting>
Expand Down
247 changes: 247 additions & 0 deletions Test/SqlDom/ScriptGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}