From 281d48aa48ba9137e7e9ca4a666498be318e0dfb Mon Sep 17 00:00:00 2001
From: ondrejtucny <78753310+ondrejtucny@users.noreply.github.com>
Date: Mon, 27 Oct 2025 19:25:22 +0100
Subject: [PATCH 1/2] Introduced IdentifierCasing option enabling upper / lower
/ pascal-casing of all identifiers in ScriptWriter
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
SqlScriptGeneratorOptions now supports a new setting IdentifierCasing with the following options:
- PreserveOriginal — outputs identifiers as-is; this is the default behavior corresponding to the current state (no breaking change)
- Lowercase — lower-case all identifiers when producing the output
- Uppercase — upper-case all identifiers when producing the output
- PascalCase — first character upper, remaining lower
Added unit tests for the various options, including compatibility with KeywordCasing property and ensuring the default state.
---
.../ScriptDom/SqlServer/IdentifierCasing.cs | 38 +++
.../SqlServer/ScriptGenerator/ScriptWriter.cs | 21 ++
.../Settings/SqlScriptGeneratorOptions.xml | 9 +
Test/SqlDom/ScriptGeneratorTests.cs | 247 ++++++++++++++++++
4 files changed, 315 insertions(+)
create mode 100644 SqlScriptDom/ScriptDom/SqlServer/IdentifierCasing.cs
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/ScriptWriter.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs
index 85d2fac..56c680d 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs
@@ -170,8 +170,29 @@ 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)
+ switch (_options.IdentifierCasing)
+ {
+ case IdentifierCasing.Lowercase:
+ text = text.ToLowerInvariant();
+ break;
+ case IdentifierCasing.Uppercase:
+ text = text.ToUpperInvariant();
+ break;
+ case IdentifierCasing.PascalCase:
+ text = ScriptGeneratorSupporter.GetPascalCase(text);
+ break;
+ case IdentifierCasing.PreserveOriginal:
+ default:
+ // No transformation - preserve original casing
+ break;
+ }
+ }
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);
+ }
}
}
From 5b0f1e45507effc5a5454b62dba9fd94736ae8af Mon Sep 17 00:00:00 2001
From: ondrejtucny <78753310+ondrejtucny@users.noreply.github.com>
Date: Mon, 27 Oct 2025 19:47:45 +0100
Subject: [PATCH 2/2] Refactored identifier casing into a separate method,
in-line with keyword casing.
---
.../ScriptGeneratorSupporter.cs | 31 +++++++++++++++++--
.../SqlServer/ScriptGenerator/ScriptWriter.cs | 17 +---------
2 files changed, 30 insertions(+), 18 deletions(-)
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 56c680d..bf2df1a 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs
@@ -176,22 +176,7 @@ private void AddIdentifier(String text, Boolean applyCasing)
else
{
// Apply identifier casing (for actual identifiers)
- switch (_options.IdentifierCasing)
- {
- case IdentifierCasing.Lowercase:
- text = text.ToLowerInvariant();
- break;
- case IdentifierCasing.Uppercase:
- text = text.ToUpperInvariant();
- break;
- case IdentifierCasing.PascalCase:
- text = ScriptGeneratorSupporter.GetPascalCase(text);
- break;
- case IdentifierCasing.PreserveOriginal:
- default:
- // No transformation - preserve original casing
- break;
- }
+ text = ScriptGeneratorSupporter.GetCasedString(text, _options.IdentifierCasing);
}
TSqlParserToken token = new TSqlParserToken(TSqlTokenType.Identifier, text);