From 6ba004c996eb2c0a562a5d6604c74cc8a5014a17 Mon Sep 17 00:00:00 2001 From: Drew Skwiers-Koballa Date: Sun, 21 Sep 2025 19:38:26 -0700 Subject: [PATCH 1/5] adds formatter option to preserve comments correcting logic in tests single line comments handled correctly next set of tests passing comment inclusion defaults to false cleanup debugging files cleanup debugging output extend tests improve multi-comment handling --- .github/copilot-instructions.md | 4 + CommentTest.cs | 42 +++ CommentTest.csproj | 12 + .../SqlServer/ScriptGenerator/ScriptWriter.cs | 127 +++++++ .../SqlScriptGeneratorVisitor.FromClause.cs | 15 +- ...SqlScriptGeneratorVisitor.StatementList.cs | 32 +- .../SqlScriptGeneratorVisitor.Utils.cs | 209 +++++++++++- .../Settings/SqlScriptGeneratorOptions.xml | 5 + Test/SqlDom/ScriptGeneratorTests.cs | 320 ++++++++++++++++-- 9 files changed, 726 insertions(+), 40 deletions(-) create mode 100644 CommentTest.cs create mode 100644 CommentTest.csproj diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9347f5a..1953bc6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -63,3 +63,7 @@ For a practical guide on fixing bugs, including the detailed workflow for genera - **Operator vs. Function-Style Predicates:** Be careful to distinguish between standard T-SQL operators (like `NOT LIKE`, `>`, `=`) and the function-style predicates used in some contexts (like `package.equals(...)` in `CREATE EVENT SESSION`). For example, `NOT LIKE` in an event session's `WHERE` clause is a standard comparison operator, not a function call. Always verify the exact T-SQL syntax before modifying the grammar. - **Logical `NOT` vs. Compound Operators:** The grammar handles the logical `NOT` operator (e.g., `WHERE NOT (condition)`) in a general way, often in a `booleanExpressionUnary` rule. This is distinct from compound operators like `NOT LIKE` or `NOT IN`, which are typically parsed as a single unit within a comparison rule. Don't assume that because `NOT` is supported, `NOT LIKE` will be automatically supported in all predicate contexts. +## Local development on macOS and Linux + +- Run build for .NET 8.0 target framework only +- Run tests for .NET 8.0 target framework only diff --git a/CommentTest.cs b/CommentTest.cs new file mode 100644 index 0000000..e35f24d --- /dev/null +++ b/CommentTest.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using Microsoft.SqlServer.TransactSql.ScriptDom; + +class Program +{ + static void Main(string[] args) + { + var sql = @"-- This is a single line comment +SELECT * FROM MyTable;"; + + var parser = new TSql160Parser(true); + var fragment = parser.ParseStatementList(new StringReader(sql), out var errors); + + Console.WriteLine($"Parse errors: {errors.Count}"); + Console.WriteLine($"Fragment type: {fragment.GetType().Name}"); + Console.WriteLine($"Script token stream count: {fragment.ScriptTokenStream?.Count ?? 0}"); + + if (fragment.ScriptTokenStream != null) + { + Console.WriteLine("\nAll tokens in stream:"); + for (int i = 0; i < fragment.ScriptTokenStream.Count; i++) + { + var token = fragment.ScriptTokenStream[i]; + Console.WriteLine($" [{i}] {token.TokenType}: '{token.Text}'"); + } + } + + Console.WriteLine($"\nFragment FirstTokenIndex: {fragment.FirstTokenIndex}"); + Console.WriteLine($"Fragment LastTokenIndex: {fragment.LastTokenIndex}"); + + // Generate script without comment preservation + var generator = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { PreserveComments = false }); + generator.GenerateScript(fragment, out var generatedScript); + Console.WriteLine($"\nGenerated script without comments:\n'{generatedScript}'"); + + // Generate script with comment preservation + var generatorWithComments = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { PreserveComments = true }); + generatorWithComments.GenerateScript(fragment, out var generatedScriptWithComments); + Console.WriteLine($"\nGenerated script with comments:\n'{generatedScriptWithComments}'"); + } +} \ No newline at end of file diff --git a/CommentTest.csproj b/CommentTest.csproj new file mode 100644 index 0000000..a51797a --- /dev/null +++ b/CommentTest.csproj @@ -0,0 +1,12 @@ + + + + Exe + net8.0 + + + + + + + \ No newline at end of file diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs index 85d2fac..057cab8 100644 --- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs @@ -412,6 +412,133 @@ private static AlignmentPointData FindOneAlignmentPointWithOutDependent(HashSet< return value; } + // Helper: replace a trailing newline element with a single space token. + public bool ReplaceTrailingNewLineWithSpaceIfPresent() + { + if (_scriptWriterElements.Count == 0) + return false; + + int index = _scriptWriterElements.Count - 1; + // Skip alignment points (they sit logically at position 0 width) + while (index >= 0 && _scriptWriterElements[index] is AlignmentPointData) + { + index--; + } + if (index < 0) return false; + var last = _scriptWriterElements[index]; + if (last is NewLineElement) + { + _scriptWriterElements.RemoveAt(index); + AddToken(ScriptGeneratorSupporter.CreateWhitespaceToken(1)); + return true; + } + if (last is TokenWrapper tw && (tw.Token.Text == "\n" || tw.Token.Text == "\r\n")) + { + _scriptWriterElements.RemoveAt(index); + AddToken(ScriptGeneratorSupporter.CreateWhitespaceToken(1)); + return true; + } + return false; + } + + // Helper: ensure exactly one space at the end (unless last element already whitespace or newline). + public void EnsureSingleTrailingSpace() + { + if (_scriptWriterElements.Count == 0) + return; + var last = _scriptWriterElements[_scriptWriterElements.Count - 1]; + if (last is NewLineElement) + return; // newline already provides separation + if (last is TokenWrapper tw) + { + var txt = tw.Token.Text; + if (txt.EndsWith(" ") || txt.EndsWith("\t") || txt.EndsWith("\n") || txt.EndsWith("\r")) + return; + } + AddToken(ScriptGeneratorSupporter.CreateWhitespaceToken(1)); + } + + public bool IsLastElementNewLine() + { + if (_scriptWriterElements.Count == 0) return false; + return _scriptWriterElements[_scriptWriterElements.Count - 1] is NewLineElement; + } + + public bool HasElements() + { + return _scriptWriterElements.Count > 0; + } + + // If the last non-alignment element is a single-line comment token, remove and return it. + public TSqlParserToken PopLastSingleLineCommentIfAny() + { + int index = _scriptWriterElements.Count - 1; + while (index >= 0 && _scriptWriterElements[index] is AlignmentPointData) + { + index--; + } + if (index >= 0 && _scriptWriterElements[index] is TokenWrapper tw && tw.Token.TokenType == TSqlTokenType.SingleLineComment) + { + _scriptWriterElements.RemoveAt(index); + return tw.Token; + } + return null; + } + + public void TrimTrailingWhitespace() + { + int index = _scriptWriterElements.Count - 1; + while (index >= 0 && _scriptWriterElements[index] is AlignmentPointData) + { + index--; + } + if (index >= 0 && _scriptWriterElements[index] is TokenWrapper tw && tw.Token.TokenType == TSqlTokenType.WhiteSpace) + { + _scriptWriterElements.RemoveAt(index); + } + } + + // Rewrites a trailing pattern (NewLine, optional alignment points) so that a multiline comment can appear inline + // at the end of the previous line. Restores the newline and alignment points after the comment to preserve alignment. + public void InsertInlineTrailingComment(TSqlParserToken commentToken) + { + if (commentToken == null) return; + int scan = _scriptWriterElements.Count - 1; + // Skip alignment points to locate newline element + while (scan >= 0 && _scriptWriterElements[scan] is AlignmentPointData) + { + scan--; + } + if (scan >= 0 && _scriptWriterElements[scan] is NewLineElement) + { + int newlineIndex = scan; + // Determine if we need a space before inserting comment + bool needsSpace = true; + int beforeNewline = newlineIndex - 1; + while (beforeNewline >= 0 && _scriptWriterElements[beforeNewline] is AlignmentPointData) + { + beforeNewline--; + } + if (beforeNewline >= 0 && _scriptWriterElements[beforeNewline] is TokenWrapper twPrev) + { + var txt = twPrev.Token.Text; + if (txt.EndsWith(" ") || txt.EndsWith("\t")) + needsSpace = false; + } + if (needsSpace) + { + _scriptWriterElements.Insert(newlineIndex, new TokenWrapper(ScriptGeneratorSupporter.CreateWhitespaceToken(1))); + newlineIndex++; + } + _scriptWriterElements.Insert(newlineIndex, new TokenWrapper(commentToken)); + } + else + { + EnsureSingleTrailingSpace(); + AddToken(commentToken); + } + } + #endregion } } diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs index 41d4dae..9f66241 100644 --- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs @@ -28,10 +28,17 @@ protected void GenerateFromClause(FromClause fromClause, AlignmentPoint clauseBo MarkAndPushAlignmentPoint(start); GenerateKeyword(TSqlTokenType.From); - - MarkClauseBodyAlignmentWhenNecessary(_options.NewLineBeforeFromClause, clauseBody); - - GenerateSpace(); + if (_suppressNextClauseAlignment) + { + // Simple single space after FROM when coming directly after an inline trailing comment + _suppressNextClauseAlignment = false; + GenerateSpace(); + } + else + { + MarkClauseBodyAlignmentWhenNecessary(_options.NewLineBeforeFromClause, clauseBody); + GenerateSpace(); + } AlignmentPoint fromItems = new AlignmentPoint(); MarkAndPushAlignmentPoint(fromItems); diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.StatementList.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.StatementList.cs index 6dedb21..f109138 100644 --- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.StatementList.cs +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.StatementList.cs @@ -14,24 +14,40 @@ public override void ExplicitVisit(StatementList node) { if (node.Statements != null) { - Boolean first = true; - foreach (TSqlStatement statement in node.Statements) + for (int i = 0; i < node.Statements.Count; i++) { - if (first) + var statement = node.Statements[i]; + if (i > 0) { - first = false; - } - else - { - for (var i = 0; i < _options.NumNewlinesAfterStatement; i++) + for (var nl = 0; nl < _options.NumNewlinesAfterStatement; nl++) { NewLine(); } + // Emit any deferred comments captured from previous statement before generating the next. + EmitPendingLeadingComments(); } GenerateFragmentIfNotNull(statement); + + // If we just generated a statement and the last emitted token is a single-line comment + // while more statements follow, and PreserveComments + spacing option indicate separation, + // defer that comment so it becomes a leading comment for the next statement instead of + // trailing inline before the semicolon. + if (_options.PreserveComments && i < node.Statements.Count - 1 && _options.NumNewlinesAfterStatement > 0) + { + var deferred = _writer.PopLastSingleLineCommentIfAny(); + if (deferred != null) + { + _pendingLeadingComments.Add(deferred); + _suppressNextClauseAlignment = false; // ensure next clause aligns normally + _writer.TrimTrailingWhitespace(); // remove space that was inserted for inline comment + } + } + GenerateSemiColonWhenNecessary(statement); } + // In case script ends with deferred comments (edge case), emit them. + EmitPendingLeadingComments(); } } } diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.Utils.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.Utils.cs index f646523..85b216b 100644 --- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.Utils.cs +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.Utils.cs @@ -13,6 +13,29 @@ namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator { partial class SqlScriptGeneratorVisitor { + // When a trailing single-line comment appears at end of a SELECT list line, suppress alignment padding + // for the very next clause body (e.g., FROM) so we preserve a single space formatting. + private bool _suppressNextClauseAlignment = false; + // Track which comment tokens have already been generated to avoid duplicates + private readonly HashSet _generatedComments = new HashSet(); + // Track whether we already emitted leading (file-level) comments + + private bool _leadingCommentsEmitted = false; + // Track the highest token index logically emitted (fragment or comment) + private int _lastEmittedTokenIndex = -1; + // Deferred single-line comments that actually belong to the next statement (prevented from being inlined before a semicolon) + private readonly List _pendingLeadingComments = new List(); + + private void EmitPendingLeadingComments() + { + if (_pendingLeadingComments.Count == 0) return; + foreach (var tok in _pendingLeadingComments) + { + _writer.AddToken(tok); + _writer.NewLine(); + } + _pendingLeadingComments.Clear(); + } // get the name for an enum value public static TValue GetValueForEnumKey(Dictionary dict, TKey key) where TKey : struct, IConvertible @@ -166,12 +189,37 @@ protected void GenerateTokenAndEqualSign(TSqlTokenType keywordId) GenerateSpaceAndSymbol(TSqlTokenType.EqualsSign); } - // generate the script from the given fragement if the fragment is not null + // generate the script from the given fragment if the fragment is not null protected void GenerateFragmentIfNotNull(TSqlFragment fragment) { if (fragment != null) { + // Emit leading comments (those before the first real token) once + if (_options.PreserveComments && !_leadingCommentsEmitted) + { + EmitLeadingComments(fragment); + _leadingCommentsEmitted = true; + } + + // Gap comments: any comments between the last emitted token and this fragment's first token + if (_options.PreserveComments) + { + EmitGapCommentsBeforeFragment(fragment); + } + fragment.Accept(this); + + // Attach trailing/inline comments to the fragment just generated + if (_options.PreserveComments) + { + GenerateCommentsAfterFragment(fragment); + } + + // Update last emitted token index (fragment itself) + if (fragment.LastTokenIndex > _lastEmittedTokenIndex) + { + _lastEmittedTokenIndex = fragment.LastTokenIndex; + } } } @@ -615,9 +663,160 @@ protected void GenerateFragmentWithAlignmentPointIfNotNull(TSqlFragment node, Al ClearAlignmentPointsForFragment(node); } } -#if false - // check if a string is a keyword - protected abstract Boolean IsKeyword(String identifier); -#endif + + /// + /// Emit leading (file-level) comments appearing before the first token of the first fragment we generate. + /// + private void EmitLeadingComments(TSqlFragment fragment) + { + if (fragment?.ScriptTokenStream == null || fragment.FirstTokenIndex <= 0) + return; + + for (int i = 0; i < fragment.FirstTokenIndex && i < fragment.ScriptTokenStream.Count; i++) + { + var token = fragment.ScriptTokenStream[i]; + if ((token.TokenType == TSqlTokenType.SingleLineComment || + token.TokenType == TSqlTokenType.MultilineComment) && + !_generatedComments.Contains(token)) + { + GenerateCommentToken(token); + _generatedComments.Add(token); + } + } + } + + // Emit comments (not already emitted) located strictly between the last emitted token and the fragment's first token. + private void EmitGapCommentsBeforeFragment(TSqlFragment fragment) + { + if (!_options.PreserveComments) + return; + if (fragment?.ScriptTokenStream == null || fragment.FirstTokenIndex < 0) + return; + + int start = _lastEmittedTokenIndex + 1; + int end = fragment.FirstTokenIndex - 1; + if (end < start) + return; // no gap + + var tokens = fragment.ScriptTokenStream; + for (int i = start; i <= end && i < tokens.Count; i++) + { + var t = tokens[i]; + if ((t.TokenType == TSqlTokenType.SingleLineComment || t.TokenType == TSqlTokenType.MultilineComment) && !_generatedComments.Contains(t)) + { + // Heuristic: if single-line comment directly follows a comma or identifier in same logical area, keep inline. + // For multiline, existing logic will put it on its own line (GenerateCommentToken handles that). + GenerateCommentToken(t); + _generatedComments.Add(t); + if (i > _lastEmittedTokenIndex) + _lastEmittedTokenIndex = i; + } + } + } + + /// + /// Generates comment tokens that appear after the specified fragment in the original script. + /// + /// The fragment to check for trailing comments. + protected void GenerateCommentsAfterFragment(TSqlFragment fragment) + { + if (!_options.PreserveComments) + return; + if (fragment?.ScriptTokenStream == null || fragment.LastTokenIndex < 0) + return; + + // Walk forward from the last token of the fragment until we reach the next non-whitespace, non-comment token. + // Any comments in this window are treated as trailing (including inline) comments of this fragment. + for (int i = fragment.LastTokenIndex + 1; i < fragment.ScriptTokenStream.Count; i++) + { + var token = fragment.ScriptTokenStream[i]; + if ((token.TokenType == TSqlTokenType.SingleLineComment || + token.TokenType == TSqlTokenType.MultilineComment) && + !_generatedComments.Contains(token)) + { + GenerateCommentToken(token); + _generatedComments.Add(token); + } + else if (token.TokenType != TSqlTokenType.WhiteSpace) + { + // Stop at the next non-whitespace, non-comment token + break; + } + } + } + + /// + /// Generates a comment token to the output. + /// + /// The comment token to generate. + protected void GenerateCommentToken(TSqlParserToken commentToken) + { + if (commentToken.TokenType == TSqlTokenType.SingleLineComment) + { + bool atLineStart = _writer.IsLastElementNewLine() || !_writer.HasElements(); + if (atLineStart) + { + // Standalone comment line; just emit and ensure newline after + _writer.AddToken(commentToken); + if (!commentToken.Text.EndsWith("\n") && !commentToken.Text.EndsWith("\r\n")) + { + NewLine(); + } + } + else + { + // Inline trailing single-line comment: ensure space then emit; no immediate newline (next clause handles it) + _writer.EnsureSingleTrailingSpace(); + _writer.AddToken(commentToken); + _suppressNextClauseAlignment = true; + // Do not add newline here to avoid extra blank line; clause separator will introduce it. + } + } + else if (commentToken.TokenType == TSqlTokenType.MultilineComment) + { + // Decide if this multiline comment should be inline or block. + bool isFirstLeadingComment = _generatedComments.Count == 0 && !_leadingCommentsEmitted; + bool inlineContext = IsLikelyInlineMultilineComment(commentToken); + + if (inlineContext) + { + _writer.InsertInlineTrailingComment(commentToken); + _generatedComments.Add(commentToken); + return; // already handled fully + } + else if (!isFirstLeadingComment) + { + NewLine(); + } + + _writer.AddToken(commentToken); + + if (!commentToken.Text.EndsWith("\n") && !commentToken.Text.EndsWith("\r\n")) + { + NewLine(); + } + } + } + + // Heuristic: treat a multiline comment as inline if it appeared between tokens on the same original line + // and immediately after a comma or identifier in a SELECT list. + // We approximate using token indices we have tracked: we look backwards in the token stream for the last non-whitespace token. + private bool IsLikelyInlineMultilineComment(TSqlParserToken commentToken) + { + // Need script token stream: attempt to cast through reflection via known field not available here; rely on commentToken.Offset only is insufficient. + // Fallback heuristic: if previous emitted token index is >= 0 and difference between current token index and last emitted <= 3 (implying only whitespace/newline tokens between), + // AND the whitespace did not include a newline (cannot directly inspect), we relax and allow inline. + // Since we cannot read intervening token kinds here without passing more context, store last index in _lastEmittedTokenIndex. + // If distance small, treat as inline unless we just began (first leading comment handled earlier). + // This is conservative: will inline some cases that are acceptable. + if (_lastEmittedTokenIndex < 0) + return false; + + // If we just emitted a fragment and the gap scanner placed us here, small gap implies inline. + int currentApproxIndex = _lastEmittedTokenIndex + 1; // best-effort; real index not carried with token instance + // Without actual token index on the token, we cannot be precise; return false only when first leading. + // Provide a configurable hook if later needed. + return true; // prefer inline for multiline comments discovered in gaps + } } } diff --git a/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml b/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml index 551033a..083bd56 100644 --- a/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml +++ b/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml @@ -28,6 +28,11 @@ IncludeSemiColons_Description Gets or sets a boolean indicating if a semi colon should be included after each statement + + PreserveComments_Title + PreserveComments_Description + Gets or sets a boolean indicating if single-line and multi-line comments should be preserved during script generation + Gets or sets a boolean indicating if index definitions should have UNIQUE, INCLUDE and WHERE on their own line diff --git a/Test/SqlDom/ScriptGeneratorTests.cs b/Test/SqlDom/ScriptGeneratorTests.cs index 9a92ce3..9430a02 100644 --- a/Test/SqlDom/ScriptGeneratorTests.cs +++ b/Test/SqlDom/ScriptGeneratorTests.cs @@ -119,7 +119,8 @@ public void TestSqlServerlessScriptGenerator() [TestMethod] [Priority(0)] [SqlStudioTestCategory(Category.UnitTest)] - public void TestNewlinesBetweenStatementsGeneratorOption() { + public void TestNewlinesBetweenStatementsGeneratorOption() + { var tableName = new SchemaObjectName(); tableName.Identifiers.Add(new Identifier { Value = "TableName" }); @@ -133,7 +134,8 @@ public void TestNewlinesBetweenStatementsGeneratorOption() { statements.Statements.Add(tableStatement); statements.Statements.Add(tableStatement); - var generatorOptions = new SqlScriptGeneratorOptions { + var generatorOptions = new SqlScriptGeneratorOptions + { KeywordCasing = KeywordCasing.Uppercase, IncludeSemicolons = true, NumNewlinesAfterStatement = 0 @@ -162,40 +164,46 @@ public void TestNewlinesBetweenStatementsGeneratorOption() { [TestMethod] [Priority(0)] [SqlStudioTestCategory(Category.UnitTest)] - public void TestNewLineFormattedIndexDefinitionDefault() { + public void TestNewLineFormattedIndexDefinitionDefault() + { Assert.AreEqual(false, new SqlScriptGeneratorOptions().NewLineFormattedIndexDefinition); } [TestMethod] [Priority(0)] [SqlStudioTestCategory(Category.UnitTest)] - public void TestNewlineFormattedCheckConstraintDefault() { + public void TestNewlineFormattedCheckConstraintDefault() + { Assert.AreEqual(false, new SqlScriptGeneratorOptions().NewlineFormattedCheckConstraint); } [TestMethod] [Priority(0)] [SqlStudioTestCategory(Category.UnitTest)] - public void TestSpaceBetweenDataTypeAndParametersDefault() { + public void TestSpaceBetweenDataTypeAndParametersDefault() + { Assert.AreEqual(true, new SqlScriptGeneratorOptions().SpaceBetweenDataTypeAndParameters); } [TestMethod] [Priority(0)] [SqlStudioTestCategory(Category.UnitTest)] - public void TestSpaceBetweenParametersInDataTypeDefault() { + public void TestSpaceBetweenParametersInDataTypeDefault() + { Assert.AreEqual(true, new SqlScriptGeneratorOptions().SpaceBetweenParametersInDataType); } [TestMethod] [Priority(0)] [SqlStudioTestCategory(Category.UnitTest)] - public void TestSpaceBetweenDataTypeAndParametersWhenFalse() { + public void TestSpaceBetweenDataTypeAndParametersWhenFalse() + { var expectedSqlText = @"CREATE TABLE DummyTable ( ColumnName VARCHAR(50) );"; - ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions { + ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions + { SpaceBetweenDataTypeAndParameters = false }); } @@ -203,12 +211,14 @@ ColumnName VARCHAR(50) [TestMethod] [Priority(0)] [SqlStudioTestCategory(Category.UnitTest)] - public void TestSpaceBetweenDataTypeAndParametersWhenTrue() { + public void TestSpaceBetweenDataTypeAndParametersWhenTrue() + { var expectedSqlText = @"CREATE TABLE DummyTable ( ColumnName VARCHAR (50) );"; - ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions { + ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions + { SpaceBetweenDataTypeAndParameters = true }); } @@ -216,12 +226,14 @@ ColumnName VARCHAR (50) [TestMethod] [Priority(0)] [SqlStudioTestCategory(Category.UnitTest)] - public void TestSpaceBetweenParametersInDataTypeWhenFalse() { + public void TestSpaceBetweenParametersInDataTypeWhenFalse() + { var expectedSqlText = @"CREATE TABLE DummyTable ( ColumnName DECIMAL (5,2) );"; - ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions { + ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions + { SpaceBetweenParametersInDataType = false }); } @@ -229,12 +241,14 @@ ColumnName DECIMAL (5,2) [TestMethod] [Priority(0)] [SqlStudioTestCategory(Category.UnitTest)] - public void TestSpaceBetweenParametersInDataTypeWhenTrue() { + public void TestSpaceBetweenParametersInDataTypeWhenTrue() + { var expectedSqlText = @"CREATE TABLE DummyTable ( ColumnName DECIMAL (5, 2) );"; - ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions { + ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions + { SpaceBetweenParametersInDataType = true }); } @@ -242,7 +256,8 @@ ColumnName DECIMAL (5, 2) [TestMethod] [Priority(0)] [SqlStudioTestCategory(Category.UnitTest)] - public void TestNewlineFormattedCheckConstraintWhenFalse() { + public void TestNewlineFormattedCheckConstraintWhenFalse() + { var expectedSqlText = @"CREATE TABLE DummyTable ( CONSTRAINT ComplicatedConstraint CHECK ((Col1 IS NULL AND (Col2 <> '' @@ -256,7 +271,8 @@ AND Col3 < 0 OR Col6 <> ''))))) );"; - ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions { + ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions + { NewlineFormattedCheckConstraint = false }); } @@ -264,7 +280,8 @@ AND Col3 < 0 [TestMethod] [Priority(0)] [SqlStudioTestCategory(Category.UnitTest)] - public void TestNewlineFormattedCheckConstraintWhenTrue() { + public void TestNewlineFormattedCheckConstraintWhenTrue() + { var expectedSqlText = @"CREATE TABLE DummyTable ( CONSTRAINT ComplicatedConstraint CHECK ((Col1 IS NULL @@ -279,7 +296,8 @@ AND Col3 < 0 OR Col6 <> ''))))) );"; - ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions { + ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions + { NewlineFormattedCheckConstraint = true }); } @@ -287,7 +305,8 @@ AND Col3 < 0 [TestMethod] [Priority(0)] [SqlStudioTestCategory(Category.UnitTest)] - public void TestNewLineFormattedIndexDefinitionWhenFalse() { + public void TestNewLineFormattedIndexDefinitionWhenFalse() + { var expectedSqlText = @"CREATE TABLE DummyTable ( INDEX ComplicatedIndex UNIQUE (Col1, Col2, Col3) INCLUDE (Col4, Col5, Col6, Col7, Col8) WHERE Col4 = 'AR' AND Col3 IN ('ABC', 'XYZ') @@ -297,7 +316,8 @@ AND Col3 IN ('ABC', 'XYZ') AND Col8 IS NOT NULL );"; - ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions { + ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions + { NewLineFormattedIndexDefinition = false }); } @@ -305,7 +325,8 @@ AND Col8 IS NOT NULL [TestMethod] [Priority(0)] [SqlStudioTestCategory(Category.UnitTest)] - public void TestNewLineFormattedIndexDefinitionWhenTrue() { + public void TestNewLineFormattedIndexDefinitionWhenTrue() + { var expectedSqlText = @"CREATE TABLE DummyTable ( INDEX ComplicatedIndex UNIQUE (Col1, Col2, Col3) @@ -318,12 +339,14 @@ AND Col3 IN ('ABC', 'XYZ') AND Col8 IS NOT NULL );"; - ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions { + ParseAndAssertEquality(expectedSqlText, new SqlScriptGeneratorOptions + { NewLineFormattedIndexDefinition = true }); } - void ParseAndAssertEquality(string sqlText, SqlScriptGeneratorOptions generatorOptions) { + void ParseAndAssertEquality(string sqlText, SqlScriptGeneratorOptions generatorOptions) + { var parser = new TSql160Parser(true); var fragment = parser.ParseStatementList(new StringReader(sqlText), out var errors); @@ -334,5 +357,256 @@ void ParseAndAssertEquality(string sqlText, SqlScriptGeneratorOptions generatorO Assert.AreEqual(sqlText, generatedSqlText); } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestPreserveCommentsDefault() + { + var generator = new Sql170ScriptGenerator(); + Assert.AreEqual(false, generator.Options.PreserveComments, "PreserveComments should default to false unless explicitly enabled"); + } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestPreserveCommentsExplicitOptions() + { + var options = new SqlScriptGeneratorOptions { PreserveComments = true }; + var generator = new Sql160ScriptGenerator(options); + Assert.AreEqual(true, generator.Options.PreserveComments, "Explicitly set PreserveComments should be respected"); + } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestPreserveSingleLineComments() + { + var sqlWithComments = @"-- This is a single line comment +SELECT * FROM MyTable;"; + var formattedSqlWithComments = @"-- This is a single line comment +SELECT * +FROM MyTable;"; + var formattedSqlWithoutComments = @"SELECT * +FROM MyTable;"; + var parser = new TSql160Parser(true); + var fragment = parser.ParseStatementList(new StringReader(sqlWithComments), out var errors); + Assert.AreEqual(0, errors.Count); + + // Test with PreserveComments = true + var generatorWithComments = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { PreserveComments = true }); + generatorWithComments.GenerateScript(fragment, out var generatedWithComments); + Assert.IsTrue(generatedWithComments.Equals(formattedSqlWithComments), + "Generated script should preserve single-line comments when PreserveComments is true"); + + // Test with PreserveComments = false + var generatorWithoutComments = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { PreserveComments = false }); + generatorWithoutComments.GenerateScript(fragment, out var generatedWithoutComments); + Assert.IsTrue(generatedWithoutComments.Equals(formattedSqlWithoutComments), + "Generated script should not contain comments when PreserveComments is false"); + } + + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestPreserveCommentsRespectsNumNewlines() + { + var sqlWithComments = @"-- This is a single line comment +SELECT * FROM MyTable; +-- This is a single line comment +SELECT * FROM MyTable;"; + var formattedSqlWithComments = @"-- This is a single line comment +SELECT * +FROM MyTable; + +-- This is a single line comment +SELECT * +FROM MyTable;"; + var formattedSqlWithoutComments = @"SELECT * +FROM MyTable; + +SELECT * +FROM MyTable;"; + var parser = new TSql160Parser(true); + var fragment = parser.ParseStatementList(new StringReader(sqlWithComments), out var errors); + Assert.AreEqual(0, errors.Count); + + // Test with PreserveComments = true + var generatorWithComments = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions + { + IncludeSemicolons = true, + PreserveComments = true, + NumNewlinesAfterStatement = 2 + }); + generatorWithComments.GenerateScript(fragment, out var generatedWithComments); + Assert.IsTrue(generatedWithComments.Equals(formattedSqlWithComments), + "Generated script should preserve single-line comments when PreserveComments is true"); + + // Test with PreserveComments = false + var generatorWithoutComments = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions + { + IncludeSemicolons = true, + PreserveComments = false, + NumNewlinesAfterStatement = 2 + }); + generatorWithoutComments.GenerateScript(fragment, out var generatedWithoutComments); + Assert.IsTrue(generatedWithoutComments.Equals(formattedSqlWithoutComments), + "Generated script should not contain comments when PreserveComments is false"); + } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestPreserveMultiLineComments() + { + var sqlWithComments = @"/* This is a + multi-line comment */ +SELECT * FROM MyTable;"; + var formattedSqlWithComments = @"/* This is a + multi-line comment */ +SELECT * +FROM MyTable;"; + var formattedSqlWithoutComments = @"SELECT * +FROM MyTable;"; + + var parser = new TSql160Parser(true); + var fragment = parser.ParseStatementList(new StringReader(sqlWithComments), out var errors); + Assert.AreEqual(0, errors.Count); + + // Test with PreserveComments = true + var generatorWithComments = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { PreserveComments = true }); + generatorWithComments.GenerateScript(fragment, out var generatedWithComments); + Assert.IsTrue(generatedWithComments.Equals(formattedSqlWithComments), + "Generated script should preserve multi-line comments when PreserveComments is true"); + + // Test with PreserveComments = false + var generatorWithoutComments = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { PreserveComments = false }); + generatorWithoutComments.GenerateScript(fragment, out var generatedWithoutComments); + Assert.IsTrue(generatedWithoutComments.Equals(formattedSqlWithoutComments), + "Generated script should not contain comments when PreserveComments is false"); + } + + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestPreserveMultiLineComplexComments() + { + var sqlWithComments = @"/****************************************************************************************** + Script: MyScriptName.sql + Author: Drew Skwiers-Koballa + Created: 2025-09-22 + Description: This script does X, Y, Z (summary of purpose). + + Parameters: @StartDate DATETIME -- Beginning of reporting range + @EndDate DATETIME -- End of reporting range + + Change Log: + 2025-09-22 DSK Initial version + 2025-09-25 JAD Added filter on [Status] +******************************************************************************************/ +SELECT Col1, /* inline comment */ Col2, * -- all the columns +FROM MyTable;"; + var formattedSqlWithComments = @"/****************************************************************************************** + Script: MyScriptName.sql + Author: Drew Skwiers-Koballa + Created: 2025-09-22 + Description: This script does X, Y, Z (summary of purpose). + + Parameters: @StartDate DATETIME -- Beginning of reporting range + @EndDate DATETIME -- End of reporting range + + Change Log: + 2025-09-22 DSK Initial version + 2025-09-25 JAD Added filter on [Status] +******************************************************************************************/ +SELECT Col1, /* inline comment */ + Col2, + * -- all the columns +FROM MyTable;"; + var formattedSqlWithoutComments = @"SELECT Col1, + Col2, + * +FROM MyTable;"; + + var parser = new TSql160Parser(true); + var fragment = parser.ParseStatementList(new StringReader(sqlWithComments), out var errors); + Assert.AreEqual(0, errors.Count); + + // Test with PreserveComments = true + var generatorWithComments = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { PreserveComments = true }); + generatorWithComments.GenerateScript(fragment, out var generatedWithComments); + Assert.IsTrue(generatedWithComments.Equals(formattedSqlWithComments), + "Generated script should preserve multi-line comments when PreserveComments is true"); + + // Test with PreserveComments = false + var generatorWithoutComments = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { PreserveComments = false }); + generatorWithoutComments.GenerateScript(fragment, out var generatedWithoutComments); + Assert.IsTrue(generatedWithoutComments.Equals(formattedSqlWithoutComments), + "Generated script should not contain comments when PreserveComments is false"); + } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestPreserveInlineComments() + { + var sqlWithComments = @"SELECT Col1, /* inline comment */ Col2 FROM MyTable;"; + var formattedSqlWithComments = @"SELECT Col1, /* inline comment */ + Col2 +FROM MyTable;"; + var formattedSqlWithoutComments = @"SELECT Col1, + Col2 +FROM MyTable;"; + + var parser = new TSql160Parser(true); + var fragment = parser.ParseStatementList(new StringReader(sqlWithComments), out var errors); + Assert.AreEqual(0, errors.Count); + + // Test with PreserveComments = true + var generatorWithComments = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { PreserveComments = true }); + generatorWithComments.GenerateScript(fragment, out var generatedWithComments); + Assert.IsTrue(generatedWithComments.Equals(formattedSqlWithComments), + "Generated script should preserve inline comments when PreserveComments is true"); + + // Test with PreserveComments = false + var generatorWithoutComments = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { PreserveComments = false }); + generatorWithoutComments.GenerateScript(fragment, out var generatedWithoutComments); + Assert.IsTrue(generatedWithoutComments.Equals(formattedSqlWithoutComments), + "Generated script should not contain inline comments when PreserveComments is false"); + } + + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void TestPreserveMixedComments() + { + var sqlWithComments = @"-- This is a single line comment +SELECT Col1, /* inline comment */ Col2 FROM MyTable;"; + var formattedSqlWithComments = @"-- This is a single line comment +SELECT Col1, /* inline comment */ + Col2 +FROM MyTable;"; + var formattedSqlWithoutComments = @"SELECT Col1, + Col2 +FROM MyTable;"; + + var parser = new TSql160Parser(true); + var fragment = parser.ParseStatementList(new StringReader(sqlWithComments), out var errors); + Assert.AreEqual(0, errors.Count); + + // Test with PreserveComments = true + var generatorWithComments = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { PreserveComments = true }); + generatorWithComments.GenerateScript(fragment, out var generatedWithComments); + Assert.IsTrue(generatedWithComments.Equals(formattedSqlWithComments), + "Generated script should preserve inline comments when PreserveComments is true"); + + // Test with PreserveComments = false + var generatorWithoutComments = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { PreserveComments = false }); + generatorWithoutComments.GenerateScript(fragment, out var generatedWithoutComments); + Assert.IsTrue(generatedWithoutComments.Equals(formattedSqlWithoutComments), + "Generated script should not contain inline comments when PreserveComments is false"); + } } } From bce5b4caca17b95f76d8566224b8aa10e63f1efe Mon Sep 17 00:00:00 2001 From: Drew Skwiers-Koballa Date: Thu, 2 Oct 2025 07:41:27 -0700 Subject: [PATCH 2/5] cleanup temporary dev changes --- .github/copilot-instructions.md | 4 ---- CommentTest.cs | 42 --------------------------------- CommentTest.csproj | 12 ---------- 3 files changed, 58 deletions(-) delete mode 100644 CommentTest.cs delete mode 100644 CommentTest.csproj diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b6b1db3..3baaa07 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -74,7 +74,3 @@ For specific parser predicate recognition issues (when identifier-based predicat - **Full Test Suite Validation:** After any grammar changes, **always run the complete test suite** (`dotnet test Test/SqlDom/UTSqlScriptDom.csproj -c Debug`) to catch regressions. Grammar changes can have far-reaching effects on seemingly unrelated functionality. - **Extending Literals to Expressions:** When functions/constructs currently accept only literal values (e.g., `IntegerLiteral`, `StringLiteral`) but need to support dynamic values (parameters, variables, outer references), change both the AST definition (in `Ast.xml`) and grammar rules (in `TSql*.g`) to use `ScalarExpression` instead. This pattern was used for VECTOR_SEARCH TOP_N parameter. See the detailed example in [BUG_FIXING_GUIDE.md](BUG_FIXING_GUIDE.md#special-case-extending-grammar-rules-from-literals-to-expressions) and [GRAMMAR_EXTENSION_PATTERNS.md](GRAMMAR_EXTENSION_PATTERNS.md) for comprehensive patterns. -## Local development on macOS and Linux - -- Run build for .NET 8.0 target framework only -- Run tests for .NET 8.0 target framework only diff --git a/CommentTest.cs b/CommentTest.cs deleted file mode 100644 index e35f24d..0000000 --- a/CommentTest.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.IO; -using Microsoft.SqlServer.TransactSql.ScriptDom; - -class Program -{ - static void Main(string[] args) - { - var sql = @"-- This is a single line comment -SELECT * FROM MyTable;"; - - var parser = new TSql160Parser(true); - var fragment = parser.ParseStatementList(new StringReader(sql), out var errors); - - Console.WriteLine($"Parse errors: {errors.Count}"); - Console.WriteLine($"Fragment type: {fragment.GetType().Name}"); - Console.WriteLine($"Script token stream count: {fragment.ScriptTokenStream?.Count ?? 0}"); - - if (fragment.ScriptTokenStream != null) - { - Console.WriteLine("\nAll tokens in stream:"); - for (int i = 0; i < fragment.ScriptTokenStream.Count; i++) - { - var token = fragment.ScriptTokenStream[i]; - Console.WriteLine($" [{i}] {token.TokenType}: '{token.Text}'"); - } - } - - Console.WriteLine($"\nFragment FirstTokenIndex: {fragment.FirstTokenIndex}"); - Console.WriteLine($"Fragment LastTokenIndex: {fragment.LastTokenIndex}"); - - // Generate script without comment preservation - var generator = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { PreserveComments = false }); - generator.GenerateScript(fragment, out var generatedScript); - Console.WriteLine($"\nGenerated script without comments:\n'{generatedScript}'"); - - // Generate script with comment preservation - var generatorWithComments = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions { PreserveComments = true }); - generatorWithComments.GenerateScript(fragment, out var generatedScriptWithComments); - Console.WriteLine($"\nGenerated script with comments:\n'{generatedScriptWithComments}'"); - } -} \ No newline at end of file diff --git a/CommentTest.csproj b/CommentTest.csproj deleted file mode 100644 index a51797a..0000000 --- a/CommentTest.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - Exe - net8.0 - - - - - - - \ No newline at end of file From 231b2b387b6f92e057fe37941baabce6f260daf7 Mon Sep 17 00:00:00 2001 From: Drew Skwiers-Koballa Date: Thu, 2 Oct 2025 07:42:21 -0700 Subject: [PATCH 3/5] remove unused helper function --- .../SqlServer/ScriptGenerator/ScriptWriter.cs | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs index 057cab8..7fc9629 100644 --- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/ScriptWriter.cs @@ -412,35 +412,6 @@ private static AlignmentPointData FindOneAlignmentPointWithOutDependent(HashSet< return value; } - // Helper: replace a trailing newline element with a single space token. - public bool ReplaceTrailingNewLineWithSpaceIfPresent() - { - if (_scriptWriterElements.Count == 0) - return false; - - int index = _scriptWriterElements.Count - 1; - // Skip alignment points (they sit logically at position 0 width) - while (index >= 0 && _scriptWriterElements[index] is AlignmentPointData) - { - index--; - } - if (index < 0) return false; - var last = _scriptWriterElements[index]; - if (last is NewLineElement) - { - _scriptWriterElements.RemoveAt(index); - AddToken(ScriptGeneratorSupporter.CreateWhitespaceToken(1)); - return true; - } - if (last is TokenWrapper tw && (tw.Token.Text == "\n" || tw.Token.Text == "\r\n")) - { - _scriptWriterElements.RemoveAt(index); - AddToken(ScriptGeneratorSupporter.CreateWhitespaceToken(1)); - return true; - } - return false; - } - // Helper: ensure exactly one space at the end (unless last element already whitespace or newline). public void EnsureSingleTrailingSpace() { From 48d09e075185ed5015ca13fbf3cfa60a4a62ed0a Mon Sep 17 00:00:00 2001 From: Drew Skwiers-Koballa Date: Thu, 2 Oct 2025 11:47:38 -0700 Subject: [PATCH 4/5] fix test and related from clause generation change --- .../SqlScriptGeneratorVisitor.FromClause.cs | 14 +++----------- Test/SqlDom/ScriptGeneratorTests.cs | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs index 9f66241..dc48225 100644 --- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs @@ -28,17 +28,9 @@ protected void GenerateFromClause(FromClause fromClause, AlignmentPoint clauseBo MarkAndPushAlignmentPoint(start); GenerateKeyword(TSqlTokenType.From); - if (_suppressNextClauseAlignment) - { - // Simple single space after FROM when coming directly after an inline trailing comment - _suppressNextClauseAlignment = false; - GenerateSpace(); - } - else - { - MarkClauseBodyAlignmentWhenNecessary(_options.NewLineBeforeFromClause, clauseBody); - GenerateSpace(); - } + + MarkClauseBodyAlignmentWhenNecessary(_options.NewLineBeforeFromClause, clauseBody); + GenerateSpace(); AlignmentPoint fromItems = new AlignmentPoint(); MarkAndPushAlignmentPoint(fromItems); diff --git a/Test/SqlDom/ScriptGeneratorTests.cs b/Test/SqlDom/ScriptGeneratorTests.cs index 9430a02..e738adf 100644 --- a/Test/SqlDom/ScriptGeneratorTests.cs +++ b/Test/SqlDom/ScriptGeneratorTests.cs @@ -524,7 +524,7 @@ @EndDate DATETIME -- End of reporting range SELECT Col1, /* inline comment */ Col2, * -- all the columns -FROM MyTable;"; +FROM MyTable;"; var formattedSqlWithoutComments = @"SELECT Col1, Col2, * From 9e14a3b3304ac3e2a6d1c045c480074437f0c64a Mon Sep 17 00:00:00 2001 From: Drew Skwiers-Koballa Date: Thu, 2 Oct 2025 12:03:30 -0700 Subject: [PATCH 5/5] add space back --- .../ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs index dc48225..41d4dae 100644 --- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs @@ -30,6 +30,7 @@ protected void GenerateFromClause(FromClause fromClause, AlignmentPoint clauseBo GenerateKeyword(TSqlTokenType.From); MarkClauseBodyAlignmentWhenNecessary(_options.NewLineBeforeFromClause, clauseBody); + GenerateSpace(); AlignmentPoint fromItems = new AlignmentPoint();