From 7ca7dc5102f0be2ffac577cb0488eb2a8108953a Mon Sep 17 00:00:00 2001
From: Allan Cascante <120929896+acascante_microsoft@users.noreply.github.com>
Date: Mon, 12 Oct 2026 15:58:23 -0600
Subject: [PATCH 01/13] Fix preserved comment ordering around comma-separated
lists
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../SqlScriptGeneratorVisitor.Comments.cs | 139 ++++++++++-
...SqlScriptGeneratorVisitor.CommonPhrases.cs | 49 +++-
.../SqlScriptGeneratorVisitor.FromClause.cs | 10 +-
.../SqlScriptGeneratorVisitor.Utils.cs | 37 +--
.../ScriptGenerator/CommaPlacementTests.cs | 231 +++++++++++++++++-
5 files changed, 445 insertions(+), 21 deletions(-)
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.Comments.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.Comments.cs
index ef0e005..6f95d44 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.Comments.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.Comments.cs
@@ -103,6 +103,16 @@ protected void EmitLeadingComments(TSqlFragment fragment)
///
/// The fragment about to be generated.
protected void EmitGapComments(TSqlFragment fragment)
+ {
+ EmitGapComments(fragment, null);
+ }
+
+ ///
+ /// Emits comments that appear before the current fragment, aligned at the specified point.
+ ///
+ /// The fragment about to be generated.
+ /// The alignment point for each emitted comment.
+ protected void EmitGapComments(TSqlFragment fragment, AlignmentPoint commentAlignmentPoint)
{
if (!_options.PreserveComments || _currentTokenStream == null || fragment == null)
{
@@ -122,6 +132,11 @@ protected void EmitGapComments(TSqlFragment fragment)
var token = _currentTokenStream[i];
if (IsCommentToken(token) && !_emittedComments.Contains(token))
{
+ if (commentAlignmentPoint != null)
+ {
+ Mark(commentAlignmentPoint);
+ }
+
EmitCommentToken(token, isLeading: true);
_emittedComments.Add(token);
_lastProcessedTokenIndex = i;
@@ -162,6 +177,49 @@ protected void EmitPendingGapComments()
}
}
+ ///
+ /// Indicates whether a comment appears next in the original script, before the next SQL
+ /// keyword or expression. Spaces and line breaks between the current position and the
+ /// comment are ignored.
+ ///
+ ///
+ /// After the select list in:
+ ///
+ /// SELECT a, b
+ /// -- comment
+ /// FROM t
+ ///
+ /// this method returns , allowing the caller to put the comment on
+ /// its own line before emitting the FROM keyword.
+ ///
+ ///
+ /// when the next meaningful content to preserve is a comment;
+ /// otherwise, .
+ ///
+ protected bool HasPendingGapComments()
+ {
+ if (!_options.PreserveComments || _currentTokenStream == null)
+ {
+ return false;
+ }
+
+ for (int i = _lastProcessedTokenIndex + 1; i < _currentTokenStream.Count; i++)
+ {
+ var token = _currentTokenStream[i];
+ if (IsCommentToken(token) && !_emittedComments.Contains(token))
+ {
+ return true;
+ }
+
+ if (token.TokenType != TSqlTokenType.WhiteSpace)
+ {
+ return false;
+ }
+ }
+
+ return false;
+ }
+
///
/// Emits trailing comments after the fragment, ONLY on the same line.
/// Comments on their own line are left as gap comments for the next fragment.
@@ -457,6 +515,16 @@ protected void UpdateLastProcessedIndex(TSqlFragment fragment)
///
/// The fragment about to be generated.
protected void HandleCommentsBeforeFragment(TSqlFragment fragment)
+ {
+ HandleCommentsBeforeFragment(fragment, null);
+ }
+
+ ///
+ /// Handles comments before a fragment and aligns gap comments at the specified point.
+ ///
+ /// The fragment about to be generated.
+ /// The alignment point for gap comments.
+ protected void HandleCommentsBeforeFragment(TSqlFragment fragment, AlignmentPoint commentAlignmentPoint)
{
if (!_options.PreserveComments || _currentTokenStream == null || fragment == null)
{
@@ -471,7 +539,76 @@ protected void HandleCommentsBeforeFragment(TSqlFragment fragment)
}
// Emit any comments in the gap between last processed token and this fragment
- EmitGapComments(fragment);
+ EmitGapComments(fragment, commentAlignmentPoint);
+ }
+
+ ///
+ /// Emits comments before a list item, including comments that the parser records at the
+ /// beginning of the item's own source range.
+ ///
+ /// The list item about to be generated.
+ /// The alignment point for emitted comments.
+ protected bool HandleCommentsBeforeListItem(TSqlFragment fragment, AlignmentPoint commentAlignmentPoint)
+ {
+ if (!_options.PreserveComments || _currentTokenStream == null || fragment == null)
+ {
+ return false;
+ }
+
+ int emittedCommentCount = _emittedComments.Count;
+ HandleCommentsBeforeFragment(fragment, commentAlignmentPoint);
+
+ int firstTokenIndex = fragment.FirstTokenIndex;
+ if (firstTokenIndex < 0 && fragment is ProcedureParameter parameter && parameter.VariableName != null)
+ {
+ firstTokenIndex = parameter.VariableName.FirstTokenIndex;
+ }
+
+ if (firstTokenIndex < 0 || firstTokenIndex >= _currentTokenStream.Count)
+ {
+ return _emittedComments.Count > emittedCommentCount;
+ }
+
+ int firstGapTokenIndex = firstTokenIndex;
+ while (firstGapTokenIndex > 0)
+ {
+ var precedingToken = _currentTokenStream[firstGapTokenIndex - 1];
+ if (precedingToken.TokenType != TSqlTokenType.WhiteSpace &&
+ precedingToken.TokenType != TSqlTokenType.Comma &&
+ !IsCommentToken(precedingToken))
+ {
+ break;
+ }
+
+ firstGapTokenIndex--;
+ }
+
+ for (int i = firstGapTokenIndex; i < firstTokenIndex; i++)
+ {
+ var token = _currentTokenStream[i];
+ if (token.TokenType == TSqlTokenType.WhiteSpace || token.TokenType == TSqlTokenType.Comma)
+ {
+ continue;
+ }
+
+ if (!IsCommentToken(token))
+ {
+ break;
+ }
+
+ if (!_emittedComments.Contains(token))
+ {
+ Mark(commentAlignmentPoint);
+ EmitCommentToken(token, isLeading: true);
+ _emittedComments.Add(token);
+ if (i > _lastProcessedTokenIndex)
+ {
+ _lastProcessedTokenIndex = i;
+ }
+ }
+ }
+
+ return _emittedComments.Count > emittedCommentCount;
}
///
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CommonPhrases.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CommonPhrases.cs
index 16bbaec..e2a8711 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CommonPhrases.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CommonPhrases.cs
@@ -485,7 +485,54 @@ protected void GenerateProcedureOrFunctionParameters(IList p
else
{
// The option produces its own leading new line before the first parameter.
- GenerateFragmentList(parameters, ListGenerationOption.MultipleLineProcedureParameterOption);
+ if (_options.PreserveComments && _options.CommaPlacement == CommaPlacement.Leading)
+ {
+ GenerateMultilineProcedureParametersWithLeadingCommas(parameters);
+ }
+ else
+ {
+ GenerateFragmentList(parameters, ListGenerationOption.MultipleLineProcedureParameterOption);
+ }
+ }
+ }
+
+ private void GenerateMultilineProcedureParametersWithLeadingCommas(
+ IList parameters)
+ {
+ PushNamedAlignmentScope();
+ try
+ {
+ AlignmentPoint items = new AlignmentPoint();
+
+ for (int index = 0; index < parameters.Count; index++)
+ {
+ ProcedureParameter parameter = parameters[index];
+ NewLine();
+
+ if (index == 0)
+ {
+ Indent();
+ if (HandleCommentsBeforeListItem(parameter, items))
+ {
+ Indent();
+ }
+ Mark(items);
+ }
+ else
+ {
+ HandleCommentsBeforeListItem(parameter, items);
+ int indentColumns = _options.IndentationSize;
+ Indent(indentColumns >= LeadingCommaWidth ? indentColumns - LeadingCommaWidth : 0);
+ GenerateSymbol(TSqlTokenType.Comma);
+ GenerateLeadingCommaSpace();
+ }
+
+ GenerateFragmentIfNotNull(parameter);
+ }
+ }
+ finally
+ {
+ PopNamedAlignmentScope();
}
}
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs
index c7c2fc6..7e005bd 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FromClause.cs
@@ -22,7 +22,15 @@ protected void GenerateFromClause(FromClause fromClause, AlignmentPoint clauseBo
IList tableSources = fromClause.TableReferences;
if (tableSources.Count > 0)
{
- GenerateSeparatorForFromClause();
+ if (HasPendingGapComments())
+ {
+ NewLine();
+ }
+ else
+ {
+ GenerateSeparatorForFromClause();
+ }
+ EmitPendingGapComments();
AlignmentPoint start = new AlignmentPoint();
MarkAndPushAlignmentPoint(start);
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.Utils.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.Utils.cs
index cdefa26..b9d79dd 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.Utils.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.Utils.cs
@@ -460,24 +460,29 @@ protected void GenerateFragmentList(IList list, ListGenerationOption optio
for (int indentIterator = 0; indentIterator < option.MultipleIndentItems; indentIterator++)
Indent();
}
- else if (option.MultipleIndentItems >= 1)
- {
- // Leading comma placement in an indented list: reserve the comma width inside
- // the indentation so the item stays at the normal list indentation column and
- // the comma is indented LeadingCommaWidth characters fewer.
- int listIndentColumns = option.MultipleIndentItems * _options.IndentationSize;
- Indent(listIndentColumns >= LeadingCommaWidth ? listIndentColumns - LeadingCommaWidth : 0);
-
- GenerateSymbol(TSqlTokenType.Comma);
- GenerateLeadingCommaSpace();
- }
else
{
- // Leading comma placement in a keyword-aligned list (no fixed indentation, e.g.
- // the SELECT column list): the comma is emitted as a right-aligned separator so
- // it ends exactly before the aligned item column, keeping items aligned with
- // the first element and the comma just before each of them.
- GenerateRightAlignedCommaSeparator();
+ HandleCommentsBeforeListItem(item, items);
+
+ if (option.MultipleIndentItems >= 1)
+ {
+ // Leading comma placement in an indented list: reserve the comma width inside
+ // the indentation so the item stays at the normal list indentation column and
+ // the comma is indented LeadingCommaWidth characters fewer.
+ int listIndentColumns = option.MultipleIndentItems * _options.IndentationSize;
+ Indent(listIndentColumns >= LeadingCommaWidth ? listIndentColumns - LeadingCommaWidth : 0);
+
+ GenerateSymbol(TSqlTokenType.Comma);
+ GenerateLeadingCommaSpace();
+ }
+ else
+ {
+ // Leading comma placement in a keyword-aligned list (no fixed indentation, e.g.
+ // the SELECT column list): the comma is emitted as a right-aligned separator so
+ // it ends exactly before the aligned item column, keeping items aligned with
+ // the first element and the comma just before each of them.
+ GenerateRightAlignedCommaSeparator();
+ }
}
if (option.NewLineBeforeItems)
diff --git a/Test/SqlDom/ScriptGenerator/CommaPlacementTests.cs b/Test/SqlDom/ScriptGenerator/CommaPlacementTests.cs
index c48dc77..2e24769 100644
--- a/Test/SqlDom/ScriptGenerator/CommaPlacementTests.cs
+++ b/Test/SqlDom/ScriptGenerator/CommaPlacementTests.cs
@@ -274,7 +274,8 @@ SELECT a
public void TestCommaPlacementLeadingWithPreserveCommentsDoesNotAbsorbComma()
{
// A line comment after an element must not absorb the following leading comma.
- const string input = @"SELECT col1, -- first column
+ const string input = @"
+SELECT col1, -- first column
col2, -- second column
col3
FROM t;";
@@ -296,7 +297,8 @@ SELECT col1 -- first column
[SqlStudioTestCategory(Category.UnitTest)]
public void TestCommaPlacementTrailingWithPreserveCommentsKeepsCommentsAfterComma()
{
- const string input = @"SELECT col1, -- first column
+ const string input = @"
+SELECT col1, -- first column
col2, -- second column
col3
FROM t;";
@@ -333,6 +335,126 @@ public void TestCommaPlacementTrailingWithCommentBeforeCommaForcesNewLine()
AssertGenerated(input, options, expected);
}
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCommaPlacementLeadingPreservesCommentBeforeFromClause()
+ {
+ const string input = @"
+SELECT a
+, b
+-- comment
+FROM t;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ AlignClauseBodies = false,
+ CommaPlacement = CommaPlacement.Leading,
+ LeadingCommaSpaceCount = 0,
+ NewLineBeforeFromClause = false,
+ PreserveComments = true,
+ };
+ const string expected = @"
+SELECT a
+ ,b
+-- comment
+FROM t;";
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCommaPlacementTrailingPreservesCommentBeforeFromClause()
+ {
+ const string input = @"
+SELECT a,
+b
+-- comment
+FROM t;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ AlignClauseBodies = false,
+ CommaPlacement = CommaPlacement.Trailing,
+ NewLineBeforeFromClause = false,
+ PreserveComments = true,
+ };
+ const string expected = @"
+SELECT a,
+ b
+-- comment
+FROM t;";
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCommaPlacementLeadingPreservesCommentsAroundSelectElements()
+ {
+ const string input = @"
+-- before select
+SELECT
+-- before first column
+a -- after first column
+-- between first and second column
+,b -- after second column
+,c -- after third column
+-- before from
+FROM t;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ AlignClauseBodies = false,
+ CommaPlacement = CommaPlacement.Leading,
+ LeadingCommaSpaceCount = 0,
+ NewLineBeforeFromClause = true,
+ PreserveComments = true,
+ };
+ const string expected = @"
+-- before select
+SELECT -- before first column
+ a -- after first column
+ -- between first and second column
+ ,b -- after second column
+ ,c -- after third column
+-- before from
+FROM t;";
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCommaPlacementTrailingPreservesCommentsAroundSelectElements()
+ {
+ const string input = @"
+-- before select
+SELECT
+-- before first column
+a, -- after first column
+-- between first and second column
+b, -- after second column
+c -- after third column
+-- before from
+FROM t;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ AlignClauseBodies = false,
+ CommaPlacement = CommaPlacement.Trailing,
+ NewLineBeforeFromClause = true,
+ PreserveComments = true,
+ };
+ const string expected = @"
+-- before select
+SELECT -- before first column
+ a, -- after first column
+ -- between first and second column
+ b, -- after second column
+ c -- after third column
+-- before from
+FROM t;";
+ AssertGenerated(input, options, expected);
+ }
+
[TestMethod]
[Priority(0)]
[SqlStudioTestCategory(Category.UnitTest)]
@@ -500,6 +622,111 @@ a INT -- first
AssertGenerated(input, options, expected);
}
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCommaPlacementLeadingProcedureParametersPreservesCommentBeforeParameter()
+ {
+ const string input = @"CREATE PROCEDURE p @a INT,
+-- before second parameter
+@b INT AS SELECT 1;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ CommaPlacement = CommaPlacement.Leading,
+ MultilineProcedureParametersList = true,
+ PreserveComments = true,
+ };
+ const string expected = @"
+CREATE PROCEDURE p
+ @a INT
+ -- before second parameter
+ , @b INT
+AS
+SELECT 1;";
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCommaPlacementLeadingProcedureParametersPreservesCommentsAroundParameters()
+ {
+ const string input = @"CREATE PROCEDURE p
+-- before first parameter
+@a INT, -- after first parameter
+-- before second parameter
+@b INT -- after second parameter
+AS SELECT 1;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ CommaPlacement = CommaPlacement.Leading,
+ MultilineProcedureParametersList = true,
+ PreserveComments = true,
+ };
+ const string expected = @"
+CREATE PROCEDURE p
+ -- before first parameter
+ @a INT -- after first parameter
+ -- before second parameter
+ , @b INT -- after second parameter
+AS
+SELECT 1;";
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCommaPlacementLeadingProcedureParametersWithSmallIndentation()
+ {
+ const string input = @"CREATE PROCEDURE p @a INT,
+-- before second parameter
+@b INT AS SELECT 1;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ CommaPlacement = CommaPlacement.Leading,
+ IndentationSize = 1,
+ MultilineProcedureParametersList = true,
+ PreserveComments = true,
+ };
+ const string expected = @"
+CREATE PROCEDURE p
+ @a INT
+ -- before second parameter
+, @b INT
+AS
+SELECT 1;";
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCommaPlacementTrailingProcedureParametersPreservesCommentsAroundParameters()
+ {
+ const string input = @"CREATE PROCEDURE p
+-- before first parameter
+@a INT, -- after first parameter
+-- before second parameter
+@b INT -- after second parameter
+AS SELECT 1;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ CommaPlacement = CommaPlacement.Trailing,
+ MultilineProcedureParametersList = true,
+ PreserveComments = true,
+ };
+ const string expected = @"
+CREATE PROCEDURE p
+ -- before first parameter
+ @a INT, -- after first parameter
+ -- before second parameter
+ @b INT -- after second parameter
+AS
+SELECT 1;";
+ AssertGenerated(input, options, expected);
+ }
+
[TestMethod]
[Priority(0)]
[SqlStudioTestCategory(Category.UnitTest)]
From fec290bd343fe72c12f64058f9d7eeaff11b20c9 Mon Sep 17 00:00:00 2001
From: Allan Cascante <120929896+acascante_microsoft@users.noreply.github.com>
Date: Mon, 12 Oct 2026 15:58:25 -0600
Subject: [PATCH 02/13] Fix preserved column alias style
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
...GeneratorVisitor.SelectScalarExpression.cs | 36 +++++++++-
.../ScriptGenerator/ColumnAliasStyleTests.cs | 72 +++++++++++++++++++
2 files changed, 107 insertions(+), 1 deletion(-)
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.SelectScalarExpression.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.SelectScalarExpression.cs
index 7797f9c..8666c42 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.SelectScalarExpression.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.SelectScalarExpression.cs
@@ -24,12 +24,46 @@ public override void ExplicitVisit(SelectScalarExpression node)
if (node.ColumnName != null)
{
- GenerateSpaceAndKeyword(TSqlTokenType.As);
+ if (ShouldGenerateAsKeywordForColumnAlias(node))
+ {
+ GenerateSpaceAndKeyword(TSqlTokenType.As);
+ }
GenerateSpaceAndFragmentIfNotNull(node.ColumnName);
}
}
}
+ private bool ShouldGenerateAsKeywordForColumnAlias(SelectScalarExpression node)
+ {
+ if (_options.ColumnAliasStyle != ColumnAliasStyle.Preserve)
+ {
+ return true;
+ }
+
+ if (node?.Expression == null || node.ColumnName == null || node.ScriptTokenStream == null)
+ {
+ return true;
+ }
+
+ int expressionIndex = node.Expression.LastTokenIndex;
+ int aliasIndex = node.ColumnName.FirstTokenIndex;
+ if (expressionIndex < 0 || aliasIndex < 0 || expressionIndex >= aliasIndex ||
+ aliasIndex >= node.ScriptTokenStream.Count)
+ {
+ return true;
+ }
+
+ for (int tokenIndex = expressionIndex + 1; tokenIndex < aliasIndex; tokenIndex++)
+ {
+ if (node.ScriptTokenStream[tokenIndex].TokenType == TSqlTokenType.As)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
// Determines whether a column alias should be rendered as "alias = expression"
// (as opposed to "expression AS alias") based on the ColumnAliasStyle option.
private bool UseEqualsSignForColumnAlias(SelectScalarExpression node)
diff --git a/Test/SqlDom/ScriptGenerator/ColumnAliasStyleTests.cs b/Test/SqlDom/ScriptGenerator/ColumnAliasStyleTests.cs
index 158ca0a..67b5448 100644
--- a/Test/SqlDom/ScriptGenerator/ColumnAliasStyleTests.cs
+++ b/Test/SqlDom/ScriptGenerator/ColumnAliasStyleTests.cs
@@ -314,6 +314,25 @@ public void TestColumnAliasStylePreserveMixedForms()
Assert.AreEqual(expected, generated);
}
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestColumnAliasStylePreserveKeepsAliasesWithoutAsKeyword()
+ {
+ string generated = Generate(
+ "SELECT col1 = a, bb AS c, d col3, ee [col 4] FROM t;",
+ new SqlScriptGeneratorOptions { ColumnAliasStyle = ColumnAliasStyle.Preserve });
+
+ string expected =
+ "SELECT col1 = a," + Environment.NewLine +
+ " bb AS c," + Environment.NewLine +
+ " d col3," + Environment.NewLine +
+ " ee [col 4]" + Environment.NewLine +
+ "FROM t;" + Environment.NewLine + Environment.NewLine;
+
+ Assert.AreEqual(expected, generated);
+ }
+
[TestMethod]
[Priority(0)]
[SqlStudioTestCategory(Category.UnitTest)]
@@ -406,6 +425,43 @@ public void TestColumnAliasStylePreserveOnProgrammaticallyBuiltFragmentRendersAs
Assert.AreEqual(expected, generated);
}
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestColumnAliasStylePreserveWithAliasIndexOutsideTokenStreamRendersAsKeyword()
+ {
+ var tokenStream = new List
+ {
+ new TSqlParserToken(TSqlTokenType.Identifier, "a")
+ };
+ var expression = new ColumnReferenceExpression
+ {
+ FirstTokenIndex = 0,
+ LastTokenIndex = 0,
+ MultiPartIdentifier = new MultiPartIdentifier { Identifiers = { new Identifier { Value = "a" } } }
+ };
+ var columnName = new IdentifierOrValueExpression
+ {
+ FirstTokenIndex = tokenStream.Count,
+ Identifier = new Identifier { Value = "x" }
+ };
+ var scalar = new SelectScalarExpression
+ {
+ ScriptTokenStream = tokenStream,
+ Expression = expression,
+ ColumnName = columnName
+ };
+ var query = new QuerySpecification();
+ query.SelectElements.Add(scalar);
+ var select = new SelectStatement { QueryExpression = query };
+
+ var generator = new Sql170ScriptGenerator(
+ new SqlScriptGeneratorOptions { ColumnAliasStyle = ColumnAliasStyle.Preserve });
+ generator.GenerateScript(select, out string generated);
+
+ Assert.AreEqual("SELECT a AS x", generated);
+ }
+
// ---------------------------------------------------------------------------------------
// Context-scoping tests: the "alias = expression" form is valid ONLY in a SELECT
// projection list. OUTPUT, OUTPUT INTO and RECEIVE reuse SelectScalarExpression but their
@@ -573,5 +629,21 @@ public void TestColumnAliasStylePreserveWithCommentsKeepsCommentInPlace()
Assert.AreEqual(expected, generated);
}
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestColumnAliasStylePreserveKeepsCommentBeforeAliasWithoutAsKeyword()
+ {
+ string generated = Generate(
+ "SELECT CAST(a AS int) /* mid */ result FROM t;",
+ new SqlScriptGeneratorOptions { ColumnAliasStyle = ColumnAliasStyle.Preserve, PreserveComments = true });
+
+ string expected =
+ "SELECT CAST (a AS INT) /* mid */ result" + Environment.NewLine +
+ "FROM t;" + Environment.NewLine + Environment.NewLine;
+
+ Assert.AreEqual(expected, generated);
+ }
}
}
From 6343919809e2384862fe2c22df266f4dad06b334 Mon Sep 17 00:00:00 2001
From: Allan Cascante <120929896+acascante_microsoft@users.noreply.github.com>
Date: Mon, 12 Oct 2026 15:58:27 -0600
Subject: [PATCH 03/13] Fix GROUP BY ALL source ranges in the Fabric DW parser
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
SqlScriptDom/Parser/TSql/TSqlFabricDW.g | 3 ++-
Test/SqlDom/OnlyFabricDWSyntaxTests.cs | 35 ++++++++++++++++++++++++-
2 files changed, 36 insertions(+), 2 deletions(-)
diff --git a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
index 5f7808d..e244298 100644
--- a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
+++ b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
@@ -21417,9 +21417,10 @@ groupByClause returns [GroupByClause vResult = FragmentFactory.CreateFragment
Date: Mon, 12 Oct 2026 15:59:13 -0600
Subject: [PATCH 04/13] Add Fabric DW join and query hints
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
SqlScriptDom/Parser/TSql/Ast.xml | 1 +
.../Parser/TSql/CodeGenerationSupporter.cs | 1 +
SqlScriptDom/Parser/TSql/JoinHint.cs | 10 +-
SqlScriptDom/Parser/TSql/TSqlFabricDW.g | 122 +++++++++---------
...Visitor.AlterTableAddClusterByStatement.cs | 2 +-
...SqlScriptGeneratorVisitor.OptimizerHint.cs | 14 ++
...SqlScriptGeneratorVisitor.QualifiedJoin.cs | 14 +-
...CreateAlterTableClusterByTestsFabricDW.sql | 13 +-
.../QueryHintsTestsFabricDW.sql | 41 ++++++
Test/SqlDom/CommonSyntaxTests.cs | 3 +-
Test/SqlDom/OnlyFabricDWSyntaxTests.cs | 3 +-
Test/SqlDom/ParserErrorsTests.cs | 50 +++++++
...CreateAlterTableClusterByTestsFabricDW.sql | 13 +-
.../TestScripts/QueryHintsTestsFabricDW.sql | 41 ++++++
14 files changed, 258 insertions(+), 70 deletions(-)
create mode 100644 Test/SqlDom/BaselinesFabricDW/QueryHintsTestsFabricDW.sql
create mode 100644 Test/SqlDom/TestScripts/QueryHintsTestsFabricDW.sql
diff --git a/SqlScriptDom/Parser/TSql/Ast.xml b/SqlScriptDom/Parser/TSql/Ast.xml
index e9c2d9e..95e2762 100644
--- a/SqlScriptDom/Parser/TSql/Ast.xml
+++ b/SqlScriptDom/Parser/TSql/Ast.xml
@@ -3562,6 +3562,7 @@
+
diff --git a/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs b/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs
index fff679b..35093c7 100644
--- a/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs
+++ b/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs
@@ -870,6 +870,7 @@ internal static class CodeGenerationSupporter
internal const string RemoteServiceName = "REMOTE_SERVICE_NAME";
internal const string Remove = "REMOVE";
internal const string Reorganize = "REORGANIZE";
+ internal const string Redistribute = "REDISTRIBUTE";
internal const string Repeatable = "REPEATABLE";
internal const string RepeatableRead = "REPEATABLEREAD";
internal const string Replace = "REPLACE";
diff --git a/SqlScriptDom/Parser/TSql/JoinHint.cs b/SqlScriptDom/Parser/TSql/JoinHint.cs
index be17136..fde9a4b 100644
--- a/SqlScriptDom/Parser/TSql/JoinHint.cs
+++ b/SqlScriptDom/Parser/TSql/JoinHint.cs
@@ -30,6 +30,14 @@ public enum JoinHint
///
/// REMOTE was used.
///
- Remote = 4
+ Remote = 4,
+ ///
+ /// REPLICATE was used.
+ ///
+ Replicate = 5,
+ ///
+ /// REDISTRIBUTE was used.
+ ///
+ Redistribute = 6
}
}
diff --git a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
index e244298..d18c51a 100644
--- a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
+++ b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
@@ -535,9 +535,6 @@ ctasCreateTableStatement [CreateTableStatement vParent]
{
vParent.SelectStatement = vSelectStatement;
}
- {
- CheckCtasStatementHasDistributionOption(vParent);
- }
;
/////////////////////////////////////////////////
@@ -19536,28 +19533,37 @@ qualifiedJoin[SubDmlFlags subDmlFlags, ref TableReference vResult]
;
joinHint[QualifiedJoin vParent]
+{
+ IntegerLiteral vColumnsCount;
+}
: (
tId1:Identifier
(
- tId2:Identifier
+ LeftParenthesis vColumnsCount = integer RightParenthesis
{
- Match(tId1,CodeGenerationSupporter.Local);
- vParent.JoinHint = JoinHintHelper.Instance.ParseOption(tId2);
- if (vParent.JoinHint == JoinHint.Remote)
- {
- ThrowIncorrectSyntaxErrorException(tId2);
- }
- }
- |
- tMerge:Merge
- {
- Match(tId1,CodeGenerationSupporter.Local);
- vParent.JoinHint = JoinHint.Merge;
+ Match(tId1,CodeGenerationSupporter.Redistribute);
+ vParent.JoinHint = JoinHint.Redistribute;
+ vParent.JoinHintColumnsCount = vColumnsCount;
}
|
/* empty */
{
- vParent.JoinHint = JoinHintHelper.Instance.ParseOption(tId1);
+ if (TryMatch(tId1, CodeGenerationSupporter.Replicate))
+ {
+ vParent.JoinHint = JoinHint.Replicate;
+ }
+ else if (TryMatch(tId1, CodeGenerationSupporter.Redistribute))
+ {
+ vParent.JoinHint = JoinHint.Redistribute;
+ }
+ else
+ {
+ vParent.JoinHint = JoinHintHelper.Instance.ParseOption(tId1);
+ if (vParent.JoinHint == JoinHint.Remote)
+ {
+ ThrowIncorrectSyntaxErrorException(tId1);
+ }
+ }
}
)
)
@@ -21960,6 +21966,19 @@ hint returns [OptimizerHint vResult]
| vResult = forTimestampAsOfOptimizerHint
;
+forTimestampAsOfOptimizerHint returns [LiteralOptimizerHint vResult = FragmentFactory.CreateFragment()]
+{
+ Literal vValue;
+}
+ : tFor:For tTimestamp:Identifier As Of vValue = stringLiteral
+ {
+ Match(tTimestamp, CodeGenerationSupporter.TimeStamp);
+ vResult.HintKind = OptimizerHintKind.ForTimestampAsOf;
+ UpdateTokenInfo(vResult, tFor);
+ vResult.Value = vValue;
+ }
+ ;
+
tableHintsOptimizerHint returns [TableHintsOptimizerHint vResult = FragmentFactory.CreateFragment()]
{
SchemaObjectName vSchemaObjectName;
@@ -21999,31 +22018,19 @@ simpleOptimizerHint returns [OptimizerHint vResult = FragmentFactory.CreateFragm
vResult.HintKind = OptimizerHintKind.MergeUnion;
UpdateTokenInfo(vResult, tMergeUnion);
}
- | {NextTokenMatches(CodeGenerationSupporter.Force)}?
- tForce:Identifier (
- Order
- {
- Match(tForce, CodeGenerationSupporter.Force);
- vResult.HintKind = OptimizerHintKind.ForceOrder;
- UpdateTokenInfo(vResult, tForce);
- }
- |
- Distributed tPlan1:Plan
- {
- Match(tForce, CodeGenerationSupporter.Force);
- vResult.HintKind = OptimizerHintKind.ForceDistributedPlan;
- UpdateTokenInfo(vResult, tForce);
- }
- |
- tSingle:Identifier tNode:Identifier tPlan2:Plan
- {
- Match(tForce, CodeGenerationSupporter.Force);
- Match(tSingle, CodeGenerationSupporter.Single);
- Match(tNode, CodeGenerationSupporter.GraphNode); // NODE
- vResult.HintKind = OptimizerHintKind.ForceSingleNodePlan;
- UpdateTokenInfo(vResult, tForce);
- }
- )
+ | tForce:Identifier Order
+ {
+ Match(tForce, CodeGenerationSupporter.Force);
+ vResult.HintKind = OptimizerHintKind.ForceOrder;
+ UpdateTokenInfo(vResult, tForce);
+ }
+ | tForceDistributed:Identifier Distributed tPlanDistributed:Plan
+ {
+ Match(tForceDistributed, CodeGenerationSupporter.Force);
+ vResult.HintKind = OptimizerHintKind.ForceDistributedPlan;
+ UpdateTokenInfo(vResult, tForceDistributed);
+ UpdateTokenInfo(vResult, tPlanDistributed);
+ }
| tHash:Identifier Group
{
Match(tHash, CodeGenerationSupporter.Hash);
@@ -22072,7 +22079,16 @@ simpleOptimizerHint returns [OptimizerHint vResult = FragmentFactory.CreateFragm
Match(tFirstWord, CodeGenerationSupporter.Optimize);
Match(tSecondWord, CodeGenerationSupporter.Correlated);
vResult.HintKind = OptimizerHintKind.OptimizeCorrelatedUnionAll;
- UpdateTokenInfo(vResult, tUnion2);
+ UpdateTokenInfo(vResult, tUnion2);
+ }
+ | tNodeWord:Identifier tPlanSingleNode:Plan
+ {
+ Match(tFirstWord, CodeGenerationSupporter.Force);
+ Match(tSecondWord, CodeGenerationSupporter.Single);
+ Match(tNodeWord, CodeGenerationSupporter.GraphNode);
+ vResult.HintKind = OptimizerHintKind.ForceSingleNodePlan;
+ UpdateTokenInfo(vResult, tFirstWord);
+ UpdateTokenInfo(vResult, tPlanSingleNode);
}
)
| tMonoHint:Identifier
@@ -22200,19 +22216,6 @@ useHintClause returns [UseHintList vResult = FragmentFactory.CreateFragment()]
-{
- Literal vValue;
-}
- : tFor:For tTimestamp:Identifier tAs:As tOf:Of vValue = stringLiteral
- {
- Match(tTimestamp, CodeGenerationSupporter.TimeStamp);
- vResult.HintKind = OptimizerHintKind.ForTimestampAsOf;
- vResult.Value = vValue;
- UpdateTokenInfo(vResult, tFor);
- }
- ;
-
createRuleStatement returns [CreateRuleStatement vResult = this.FragmentFactory.CreateFragment()]
{
SchemaObjectName vSchemaObjectName;
@@ -28028,6 +28031,8 @@ alterTableStatement returns [AlterTableStatement vResult = null]
| vResult=alterTableDropTableElementStatement
| {NextTokenMatches(CodeGenerationSupporter.Switch)}?
vResult=alterTableSwitchStatement
+ | {NextTokenMatches(CodeGenerationSupporter.Cluster, 3)}?
+ vResult=alterTableAddClusterByStatement
| (
(With vExistingRowsCheck = constraintEnforcement)?
(
@@ -28048,7 +28053,6 @@ alterTableStatement returns [AlterTableStatement vResult = null]
{NextTokenMatches(CodeGenerationSupporter.FileTableNamespace, 2)}?
vResult=alterTableFileTableNamespaceStatement
| vResult=alterTableSetStatement
- | vResult=alterTableAddClusterByStatement
)
{
// Update position later, because instantiation is lazy
@@ -28195,10 +28199,10 @@ alterTableAddClusterByStatement returns [AlterTableAddClusterByStatement vResult
{
ClusterByTableOption vClusterByOption;
}
- : tAdd:Add LeftParenthesis vClusterByOption = clusterByTableOption
+ : tWith:With LeftParenthesis vClusterByOption = clusterByTableOption
{
vResult.ClusterByOption = vClusterByOption;
- UpdateTokenInfo(vResult, tAdd);
+ UpdateTokenInfo(vResult, tWith);
}
tRParen:RightParenthesis
{
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AlterTableAddClusterByStatement.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AlterTableAddClusterByStatement.cs
index b335635..1f9beb0 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AlterTableAddClusterByStatement.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AlterTableAddClusterByStatement.cs
@@ -18,7 +18,7 @@ public override void ExplicitVisit(AlterTableAddClusterByStatement node)
NewLineAndIndent();
- GenerateKeyword(TSqlTokenType.Add);
+ GenerateKeyword(TSqlTokenType.With);
GenerateSpace();
if (node.ClusterByOption != null)
{
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OptimizerHint.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OptimizerHint.cs
index 92068d1..8181667 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OptimizerHint.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OptimizerHint.cs
@@ -31,6 +31,20 @@ partial class SqlScriptGeneratorVisitor
{ OptimizerHintKind.ForceOrder, new List( ) {
new IdentifierGenerator(CodeGenerationSupporter.Force, true),
new KeywordGenerator(TSqlTokenType.Order), }},
+ { OptimizerHintKind.ForceSingleNodePlan, new List( ) {
+ new IdentifierGenerator(CodeGenerationSupporter.Force, true),
+ new IdentifierGenerator(CodeGenerationSupporter.Single, true),
+ new IdentifierGenerator(CodeGenerationSupporter.GraphNode, true),
+ new KeywordGenerator(TSqlTokenType.Plan), }},
+ { OptimizerHintKind.ForceDistributedPlan, new List( ) {
+ new IdentifierGenerator(CodeGenerationSupporter.Force, true),
+ new KeywordGenerator(TSqlTokenType.Distributed, true),
+ new KeywordGenerator(TSqlTokenType.Plan), }},
+ { OptimizerHintKind.ForTimestampAsOf, new List( ) {
+ new KeywordGenerator(TSqlTokenType.For, true),
+ new IdentifierGenerator(CodeGenerationSupporter.TimeStamp, true),
+ new KeywordGenerator(TSqlTokenType.As, true),
+ new KeywordGenerator(TSqlTokenType.Of), }},
{ OptimizerHintKind.HashGroup, new List( ) {
new IdentifierGenerator(CodeGenerationSupporter.Hash, true),
new KeywordGenerator(TSqlTokenType.Group), }},
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.QualifiedJoin.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.QualifiedJoin.cs
index 19de775..19b2421 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.QualifiedJoin.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.QualifiedJoin.cs
@@ -44,7 +44,19 @@ public override void ExplicitVisit(QualifiedJoin node)
if (node.JoinHint != JoinHint.None)
{
GenerateSpace();
- JoinHintHelper.Instance.GenerateSourceForOption(_writer, node.JoinHint);
+ if (node.JoinHint == JoinHint.Replicate)
+ {
+ GenerateIdentifierWithoutCasing(CodeGenerationSupporter.Replicate);
+ }
+ else if (node.JoinHint == JoinHint.Redistribute)
+ {
+ GenerateIdentifierWithoutCasing(CodeGenerationSupporter.Redistribute);
+ GenerateParenthesisedFragmentIfNotNull(node.JoinHintColumnsCount);
+ }
+ else
+ {
+ JoinHintHelper.Instance.GenerateSourceForOption(_writer, node.JoinHint);
+ }
}
GenerateSpaceAndKeyword(TSqlTokenType.Join);
diff --git a/Test/SqlDom/BaselinesFabricDW/CreateAlterTableClusterByTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/CreateAlterTableClusterByTestsFabricDW.sql
index 05f8e02..b210224 100644
--- a/Test/SqlDom/BaselinesFabricDW/CreateAlterTableClusterByTestsFabricDW.sql
+++ b/Test/SqlDom/BaselinesFabricDW/CreateAlterTableClusterByTestsFabricDW.sql
@@ -25,14 +25,21 @@ WITH (CLUSTER BY (ProductID));
GO
ALTER TABLE Customers
- ADD (CLUSTER BY (CustomerID));
+ WITH (CLUSTER BY (CustomerID));
GO
ALTER TABLE sales.Orders
- ADD (CLUSTER BY (CustomerID, OrderDate));
+ WITH (CLUSTER BY (CustomerID, OrderDate));
GO
ALTER TABLE Inventory
- ADD (CLUSTER BY (ProductID));
\ No newline at end of file
+ WITH (CLUSTER BY (ProductID));
+
+
+GO
+CREATE TABLE CustomersClustered
+WITH (CLUSTER BY (CustomerID)) AS
+SELECT *
+FROM Customers;
\ No newline at end of file
diff --git a/Test/SqlDom/BaselinesFabricDW/QueryHintsTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/QueryHintsTestsFabricDW.sql
new file mode 100644
index 0000000..4b2e7d8
--- /dev/null
+++ b/Test/SqlDom/BaselinesFabricDW/QueryHintsTestsFabricDW.sql
@@ -0,0 +1,41 @@
+SELECT *
+FROM t1
+ INNER HASH JOIN
+ t2
+ ON t1.c1 = t2.c1;
+GO
+
+SELECT *
+FROM t1
+ INNER REPLICATE JOIN
+ t2
+ ON t1.c1 = t2.c1;
+GO
+
+SELECT *
+FROM t1
+ INNER REDISTRIBUTE JOIN
+ t2
+ ON t1.c1 = t2.c1;
+GO
+
+SELECT *
+FROM t1
+ INNER REDISTRIBUTE(2) JOIN
+ t2
+ ON t1.c1 = t2.c1;
+GO
+
+SELECT c1
+FROM t1
+OPTION (FORCE SINGLE NODE PLAN);
+GO
+
+SELECT c1
+FROM t1
+OPTION (FORCE DISTRIBUTED PLAN);
+GO
+
+SELECT c1
+FROM t1
+OPTION (FOR TIMESTAMP AS OF '2024-05-01T10:00:00');
\ No newline at end of file
diff --git a/Test/SqlDom/CommonSyntaxTests.cs b/Test/SqlDom/CommonSyntaxTests.cs
index 8b280db..b00a005 100644
--- a/Test/SqlDom/CommonSyntaxTests.cs
+++ b/Test/SqlDom/CommonSyntaxTests.cs
@@ -247,7 +247,8 @@ public void CommonSyntaxInFabricDWParserTest()
"IntegerTests.sql",
"TSqlParserTestScript1.sql",
"TSqlParserTestScript2.sql",
- "ExpressionTests.sql"
+ "ExpressionTests.sql",
+ "FromClauseTests.sql"
};
foreach (ParserTest ti in CommonTestInfos)
diff --git a/Test/SqlDom/OnlyFabricDWSyntaxTests.cs b/Test/SqlDom/OnlyFabricDWSyntaxTests.cs
index 36134aa..285e0df 100644
--- a/Test/SqlDom/OnlyFabricDWSyntaxTests.cs
+++ b/Test/SqlDom/OnlyFabricDWSyntaxTests.cs
@@ -12,7 +12,7 @@ public partial class SqlDomTests
{
new ParserTestFabricDW("CloneTableTestsFabricDW.sql", nErrors80: 4, nErrors90: 4, nErrors100: 4, nErrors110: 4, nErrors120: 4, nErrors130: 4, nErrors140: 4, nErrors150: 4, nErrors160: 4, nErrors170: 4, nErrors180: 4),
new ParserTestFabricDW("CreateAlterExternalDataSourceTestsFabricDW.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0),
- new ParserTestFabricDW("CreateAlterTableClusterByTestsFabricDW.sql", nErrors80: 6, nErrors90: 6, nErrors100: 6, nErrors110: 6, nErrors120: 6, nErrors130: 6, nErrors140: 6, nErrors150: 6, nErrors160: 6, nErrors170: 6, nErrors180: 6),
+ new ParserTestFabricDW("CreateAlterTableClusterByTestsFabricDW.sql", nErrors80: 7, nErrors90: 7, nErrors100: 7, nErrors110: 7, nErrors120: 7, nErrors130: 7, nErrors140: 7, nErrors150: 7, nErrors160: 7, nErrors170: 7, nErrors180: 7),
new ParserTestFabricDW("CreateExternalTableStatementTestsFabricDW.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 2, nErrors140: 2, nErrors150: 2, nErrors160: 0, nErrors170: 0, nErrors180: 0),
new ParserTestFabricDW("CreateProcedureCloneTableTestsFabricDW.sql", nErrors80: 4, nErrors90: 4, nErrors100: 4, nErrors110: 4, nErrors120: 4, nErrors130: 4, nErrors140: 4, nErrors150: 4, nErrors160: 4, nErrors170: 4, nErrors180: 4),
new ParserTestFabricDW("IdentityColumnTestsFabricDW.sql", nErrors80: 0, nErrors90: 0, nErrors100: 0, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0),
@@ -31,6 +31,7 @@ public partial class SqlDomTests
new ParserTestFabricDW("CreateDatabaseCollateBeforeEditionTestsFabricDW.sql", nErrors80: 3, nErrors90: 3, nErrors100: 3, nErrors110: 3, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0),
new ParserTestFabricDW("ModernGroupByAllTestsFabricDW.sql", nErrors80: 9, nErrors90: 9, nErrors100: 9, nErrors110: 9, nErrors120: 9, nErrors130: 9, nErrors140: 9, nErrors150: 9, nErrors160: 9, nErrors170: 9, nErrors180: 9),
new ParserTestFabricDW("OrderByAllTestsFabricDW.sql", nErrors80: 13, nErrors90: 13, nErrors100: 13, nErrors110: 13, nErrors120: 13, nErrors130: 13, nErrors140: 13, nErrors150: 13, nErrors160: 13, nErrors170: 13, nErrors180: 13),
+ new ParserTestFabricDW("QueryHintsTestsFabricDW.sql", nErrors80: 6, nErrors90: 6, nErrors100: 6, nErrors110: 6, nErrors120: 6, nErrors130: 6, nErrors140: 6, nErrors150: 6, nErrors160: 6, nErrors170: 6, nErrors180: 6),
};
diff --git a/Test/SqlDom/ParserErrorsTests.cs b/Test/SqlDom/ParserErrorsTests.cs
index 00db16f..0529a84 100644
--- a/Test/SqlDom/ParserErrorsTests.cs
+++ b/Test/SqlDom/ParserErrorsTests.cs
@@ -7258,6 +7258,56 @@ FROM @SalesData
ParserTestUtils.ErrorTestFabricDW(scalarFunctionSyntax4, new ParserErrorInfo(scalarFunctionSyntax4.IndexOf("NULL"), "SQL46010", "NULL"));
}
+ ///
+ /// Negative tests for join hints and query hints in Fabric DW.
+ ///
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void QueryHintsNegativeTestsFabricDW()
+ {
+ // REMOTE join hint is not supported in Fabric DW
+ //
+ string remoteJoinHint = "SELECT * FROM t1 INNER REMOTE JOIN t2 ON t1.c1 = t2.c1";
+ ParserTestUtils.ErrorTestFabricDW(remoteJoinHint, new ParserErrorInfo(remoteJoinHint.IndexOf("REMOTE"), "SQL46010", "REMOTE"));
+
+ // LOCAL-prefixed join hints are not supported in Fabric DW
+ //
+ string localHashJoinHint = "SELECT * FROM t1 INNER LOCAL HASH JOIN t2 ON t1.c1 = t2.c1";
+ ParserTestUtils.ErrorTestFabricDW(localHashJoinHint, new ParserErrorInfo(localHashJoinHint.IndexOf("HASH"), "SQL46010", "HASH"));
+
+ string localMergeJoinHint = "SELECT * FROM t1 INNER LOCAL MERGE JOIN t2 ON t1.c1 = t2.c1";
+ ParserTestUtils.ErrorTestFabricDW(localMergeJoinHint, new ParserErrorInfo(localMergeJoinHint.IndexOf("MERGE"), "SQL46010", "MERGE"));
+
+ // Only REDISTRIBUTE accepts a (columns_count) argument
+ //
+ string replicateWithCount = "SELECT * FROM t1 INNER REPLICATE (2) JOIN t2 ON t1.c1 = t2.c1";
+ ParserTestUtils.ErrorTestFabricDW(replicateWithCount, new ParserErrorInfo(replicateWithCount.IndexOf("REPLICATE"), "SQL46005", "REDISTRIBUTE", "REPLICATE"));
+
+ // REDISTRIBUTE columns count must be an integer literal
+ //
+ string redistributeNonInteger = "SELECT * FROM t1 INNER REDISTRIBUTE ('a') JOIN t2 ON t1.c1 = t2.c1";
+ ParserTestUtils.ErrorTestFabricDW(redistributeNonInteger, new ParserErrorInfo(redistributeNonInteger.IndexOf("'a'"), "SQL46010", "'a'"));
+
+ // FORCE DISTRIBUTED PLAN must start with FORCE
+ //
+ string keepDistributedPlan = "SELECT c1 FROM t1 OPTION (KEEP DISTRIBUTED PLAN)";
+ ParserTestUtils.ErrorTestFabricDW(keepDistributedPlan, new ParserErrorInfo(keepDistributedPlan.IndexOf("KEEP"), "SQL46005", "FORCE", "KEEP"));
+
+ // FORCE SINGLE NODE PLAN requires the NODE keyword
+ //
+ string forceSingleNodesPlan = "SELECT c1 FROM t1 OPTION (FORCE SINGLE NODES PLAN)";
+ ParserTestUtils.ErrorTestFabricDW(forceSingleNodesPlan, new ParserErrorInfo(forceSingleNodesPlan.IndexOf("NODES"), "SQL46005", "NODE", "NODES"));
+
+ // FOR TIMESTAMP AS OF requires the TIMESTAMP keyword and a string literal
+ //
+ string forTimestamps = "SELECT c1 FROM t1 OPTION (FOR TIMESTAMPS AS OF '2024-05-01T10:00:00')";
+ ParserTestUtils.ErrorTestFabricDW(forTimestamps, new ParserErrorInfo(forTimestamps.IndexOf("TIMESTAMPS"), "SQL46005", "TIMESTAMP", "TIMESTAMPS"));
+
+ string forTimestampNonString = "SELECT c1 FROM t1 OPTION (FOR TIMESTAMP AS OF 123)";
+ ParserTestUtils.ErrorTestFabricDW(forTimestampNonString, new ParserErrorInfo(forTimestampNonString.IndexOf("123"), "SQL46010", "123"));
+ }
+
///
/// Negative tests for Scalar Functions in Fabric DW.
///
diff --git a/Test/SqlDom/TestScripts/CreateAlterTableClusterByTestsFabricDW.sql b/Test/SqlDom/TestScripts/CreateAlterTableClusterByTestsFabricDW.sql
index 05f8e02..b210224 100644
--- a/Test/SqlDom/TestScripts/CreateAlterTableClusterByTestsFabricDW.sql
+++ b/Test/SqlDom/TestScripts/CreateAlterTableClusterByTestsFabricDW.sql
@@ -25,14 +25,21 @@ WITH (CLUSTER BY (ProductID));
GO
ALTER TABLE Customers
- ADD (CLUSTER BY (CustomerID));
+ WITH (CLUSTER BY (CustomerID));
GO
ALTER TABLE sales.Orders
- ADD (CLUSTER BY (CustomerID, OrderDate));
+ WITH (CLUSTER BY (CustomerID, OrderDate));
GO
ALTER TABLE Inventory
- ADD (CLUSTER BY (ProductID));
\ No newline at end of file
+ WITH (CLUSTER BY (ProductID));
+
+
+GO
+CREATE TABLE CustomersClustered
+WITH (CLUSTER BY (CustomerID)) AS
+SELECT *
+FROM Customers;
\ No newline at end of file
diff --git a/Test/SqlDom/TestScripts/QueryHintsTestsFabricDW.sql b/Test/SqlDom/TestScripts/QueryHintsTestsFabricDW.sql
new file mode 100644
index 0000000..4b2e7d8
--- /dev/null
+++ b/Test/SqlDom/TestScripts/QueryHintsTestsFabricDW.sql
@@ -0,0 +1,41 @@
+SELECT *
+FROM t1
+ INNER HASH JOIN
+ t2
+ ON t1.c1 = t2.c1;
+GO
+
+SELECT *
+FROM t1
+ INNER REPLICATE JOIN
+ t2
+ ON t1.c1 = t2.c1;
+GO
+
+SELECT *
+FROM t1
+ INNER REDISTRIBUTE JOIN
+ t2
+ ON t1.c1 = t2.c1;
+GO
+
+SELECT *
+FROM t1
+ INNER REDISTRIBUTE(2) JOIN
+ t2
+ ON t1.c1 = t2.c1;
+GO
+
+SELECT c1
+FROM t1
+OPTION (FORCE SINGLE NODE PLAN);
+GO
+
+SELECT c1
+FROM t1
+OPTION (FORCE DISTRIBUTED PLAN);
+GO
+
+SELECT c1
+FROM t1
+OPTION (FOR TIMESTAMP AS OF '2024-05-01T10:00:00');
\ No newline at end of file
From 435def8d4ad11f90e28138df8a95892305b5edc3 Mon Sep 17 00:00:00 2001
From: Allan Cascante <120929896+acascante_microsoft@users.noreply.github.com>
Date: Mon, 12 Oct 2026 15:59:54 -0600
Subject: [PATCH 05/13] Fix lexer handling of the U+FFFF noncharacter
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Parser/TSql/TSql80ParserBaseInternal.cs | 5 +-
.../Parser/TSql/TSqlLexerBaseInternal.cs | 59 +++-
SqlScriptDom/antlr/antlr/CharScanner.cs | 50 ++-
SqlScriptDom/antlr/antlr/InputBuffer.cs | 9 +-
.../antlr/antlr/MismatchedCharException.cs | 33 +-
.../antlr/NoViableAltForCharException.cs | 26 +-
Test/SqlDom/UnicodeNonCharacterLexerTests.cs | 319 ++++++++++++++++++
7 files changed, 479 insertions(+), 22 deletions(-)
create mode 100644 Test/SqlDom/UnicodeNonCharacterLexerTests.cs
diff --git a/SqlScriptDom/Parser/TSql/TSql80ParserBaseInternal.cs b/SqlScriptDom/Parser/TSql/TSql80ParserBaseInternal.cs
index 5607c18..4c2c626 100644
--- a/SqlScriptDom/Parser/TSql/TSql80ParserBaseInternal.cs
+++ b/SqlScriptDom/Parser/TSql/TSql80ParserBaseInternal.cs
@@ -2365,12 +2365,15 @@ internal static ParseError ProcessTokenStreamRecognitionException(antlr.TokenStr
antlr.NoViableAltForCharException notViableAltForCharException = exception.recog as antlr.NoViableAltForCharException;
if (notViableAltForCharException != null)
{
+ string foundChar = notViableAltForCharException.foundChar == antlr.CharScanner.EOF_CHAR
+ ? ""
+ : Convert.ToChar(notViableAltForCharException.foundChar).ToString();
return TSql80ParserBaseInternal.CreateParseError("SQL46010",
lastOffset,
notViableAltForCharException.getLine(),
notViableAltForCharException.getColumn(),
TSqlParserResource.SQL46010Message,
- notViableAltForCharException.foundChar.ToString());
+ foundChar);
}
// TODO, sacaglar: This is translated into an internal error. Is there a runtime log for this?
diff --git a/SqlScriptDom/Parser/TSql/TSqlLexerBaseInternal.cs b/SqlScriptDom/Parser/TSql/TSqlLexerBaseInternal.cs
index 2835996..76d5c3f 100644
--- a/SqlScriptDom/Parser/TSql/TSqlLexerBaseInternal.cs
+++ b/SqlScriptDom/Parser/TSql/TSqlLexerBaseInternal.cs
@@ -34,6 +34,13 @@ public void InitializeForNewInput(int startOffset, int startLine, int startColum
private int _complexTokenStartOffset = 0;
protected int _currentLineStartOffset = 0;
+ ///
+ /// The kind of complex token currently being scanned. Recorded while scanning so
+ /// that, if end-of-input is reached before the token is closed, the correct
+ /// unterminated-token error can be raised (see ).
+ ///
+ private TokenKind _complexTokenKind = TokenKind.Common;
+
///
/// Used to be able to handle GO statement.
///
@@ -55,6 +62,8 @@ public override void newline()
protected internal override IToken makeToken(int t)
{
+ // A token was produced successfully, so we are no longer inside a complex token.
+ _complexTokenKind = TokenKind.Common;
return new TSqlParserToken(
(TSqlTokenType)t,
CurrentOffset - text.Length,
@@ -80,6 +89,9 @@ protected enum TokenKind
///
protected void checkEOF(TokenKind currentToken)
{
+ // Remember which complex token we are scanning so handlePrematureEof can raise
+ // the right error if the closing delimiter is never found.
+ _complexTokenKind = currentToken;
if (LA(1) == EOF_CHAR)
{
uponEOF();
@@ -94,17 +106,17 @@ protected void checkEOF(TokenKind currentToken)
case TokenKind.QuotedIdentifier:
error = TSql80ParserBaseInternal.CreateParseError("SQL46031",
_complexTokenStartOffset, inputState.tokenStartLine, inputState.tokenStartColumn,
- TSqlParserResource.SQL46031Message, getText().TrimEnd(EOF_CHAR));
+ TSqlParserResource.SQL46031Message, getText());
break;
case TokenKind.SqlCommandIdentifier:
error = TSql80ParserBaseInternal.CreateParseError("SQL46033",
_complexTokenStartOffset, inputState.tokenStartLine, inputState.tokenStartColumn,
- TSqlParserResource.SQL46033Message, getText().TrimEnd(EOF_CHAR));
+ TSqlParserResource.SQL46033Message, getText());
break;
case TokenKind.String:
error = TSql80ParserBaseInternal.CreateParseError("SQL46030",
_complexTokenStartOffset, inputState.tokenStartLine, inputState.tokenStartColumn,
- TSqlParserResource.SQL46030Message, getText().TrimEnd(EOF_CHAR));
+ TSqlParserResource.SQL46030Message, getText());
break;
case TokenKind.Common:
default:
@@ -121,6 +133,47 @@ protected void checkEOF(TokenKind currentToken)
protected void beginComplexToken()
{
_complexTokenStartOffset = CurrentOffset;
+ // Record the kind of complex token from its opening delimiter so the correct
+ // unterminated-token error can still be raised when the token has no content
+ // before end-of-input (e.g. a lone quote at the end of the script). The
+ // content-loop checkEOF call is not reached at genuine EOF because the EOF
+ // sentinel is no longer a member of the content token sets.
+ switch (cached_LA1)
+ {
+ case '\'': // ascii string literal opens with '
+ case 'n': // unicode string literal opens with n' (lexer is case-insensitive)
+ _complexTokenKind = TokenKind.String;
+ break;
+ case '[':
+ case '"': // delimited identifier opens with [ or "
+ _complexTokenKind = TokenKind.QuotedIdentifier;
+ break;
+ case '/': // multi-line comment opens with /*
+ _complexTokenKind = TokenKind.MultiLineComment;
+ break;
+ case '$': // sql command identifier opens with $(
+ _complexTokenKind = TokenKind.SqlCommandIdentifier;
+ break;
+ default:
+ _complexTokenKind = TokenKind.Common;
+ break;
+ }
+ }
+
+ ///
+ /// Called by the lexer base when a character match fails at end-of-input. If we
+ /// are scanning a complex token (string literal, delimited identifier, or
+ /// multi-line comment), raise the specific unterminated-token error rather than a
+ /// generic mismatch. The EOF sentinel is no longer a member of the content token
+ /// sets, so the in-loop checkEOF call is not reached at genuine end-of-input; this
+ /// hook fires when the closing delimiter match fails instead.
+ ///
+ protected override void handlePrematureEof()
+ {
+ if (inputState.guessing == 0)
+ {
+ checkEOF(_complexTokenKind);
+ }
}
internal static bool IsValueTooLargeForTokenInteger(string source)
diff --git a/SqlScriptDom/antlr/antlr/CharScanner.cs b/SqlScriptDom/antlr/antlr/CharScanner.cs
index 004c5e3..4132950 100644
--- a/SqlScriptDom/antlr/antlr/CharScanner.cs
+++ b/SqlScriptDom/antlr/antlr/CharScanner.cs
@@ -28,7 +28,11 @@ namespace antlr
internal abstract class CharScanner : TokenStream
{
internal const char NO_CHAR = (char) (0);
- public static readonly char EOF_CHAR = Char.MaxValue;
+ // EOF is represented by a value OUTSIDE the 16-bit char range so that a real
+ // character equal to U+FFFF (char.MaxValue) in the input is not mistaken for
+ // end-of-input. Real EOF is produced only by the input buffer once the
+ // underlying reader is exhausted (detected by stream position, not by value).
+ public const int EOF_CHAR = Char.MaxValue + 1;
// NOTE, olegr: Appears to be not used...
//// Used to store event delegates
@@ -64,10 +68,12 @@ internal abstract class CharScanner : TokenStream
/// Used for creating Token instances.
protected TokenCreator tokenCreator;
- /// Used for caching lookahead characters.
- protected char cached_LA1;
- protected char cached_LA2;
- protected char cached_LA3;
+ /// Used for caching lookahead characters. Typed as int so the
+ /// out-of-char-range EOF_CHAR sentinel can be represented distinctly from a
+ /// real U+FFFF character.
+ protected int cached_LA1;
+ protected int cached_LA2;
+ protected int cached_LA3;
protected bool caseSensitive = true;
protected bool caseSensitiveLiterals = true;
@@ -149,13 +155,13 @@ public virtual void consume()
{
if (caseSensitive)
{
- append(cached_LA1);
+ append((char) cached_LA1);
}
else
{
// use input.LA(), not LA(), to get original case
// CharScanner.LA() would toLower it.
- append(inputState.input.LA(1));
+ append((char) inputState.input.LA(1));
}
if (cached_LA1 == '\t')
{
@@ -259,7 +265,7 @@ public virtual IToken getTokenObject()
return returnToken_;
}
- public virtual char LA(int i)
+ public virtual int LA(int i)
{
if (i == 1)
{
@@ -328,6 +334,10 @@ public virtual void match(int c)
{
if (cached_LA1 != c)
{
+ if (cached_LA1 == EOF_CHAR)
+ {
+ handlePrematureEof();
+ }
throw new MismatchedCharException(cached_LA1, Convert.ToChar(c), false, this);
}
consume();
@@ -337,6 +347,10 @@ public virtual void match(BitSet b)
{
if (!b.member(cached_LA1))
{
+ if (cached_LA1 == EOF_CHAR)
+ {
+ handlePrematureEof();
+ }
throw new MismatchedCharException(cached_LA1, b, false, this);
}
consume();
@@ -349,6 +363,10 @@ public virtual void match(string s)
{
if (cached_LA1 != s[i])
{
+ if (cached_LA1 == EOF_CHAR)
+ {
+ handlePrematureEof();
+ }
throw new MismatchedCharException(cached_LA1, s[i], false, this);
}
consume();
@@ -383,6 +401,15 @@ public virtual void matchRange(char c1, char c2)
matchRange((int) c1, (int) c2);
}
+ ///
+ /// Hook invoked when a character match fails because the next lookahead is the
+ /// EOF sentinel. Subclasses can override to raise a context-specific error
+ /// (e.g. an unterminated string literal, delimited identifier, or comment).
+ ///
+ protected virtual void handlePrematureEof()
+ {
+ }
+
public virtual void newline()
{
inputState.line++;
@@ -594,8 +621,13 @@ public virtual int testLiteralsTable(string someText, int ttype)
}
// Override this method to get more specific case handling
- public virtual char toLower(int c)
+ public virtual int toLower(int c)
{
+ // Keep any out-of-range sentinel unchanged before converting to char.
+ // EOF_CHAR is above the range; the lower-bound check also prevents a
+ // negative sentinel from being narrowed.
+ if (c < 0 || c > Char.MaxValue)
+ return c;
return Char.ToLower(Convert.ToChar(c), System.Globalization.CultureInfo.InvariantCulture);
}
diff --git a/SqlScriptDom/antlr/antlr/InputBuffer.cs b/SqlScriptDom/antlr/antlr/InputBuffer.cs
index 1417db5..5f3eb42 100644
--- a/SqlScriptDom/antlr/antlr/InputBuffer.cs
+++ b/SqlScriptDom/antlr/antlr/InputBuffer.cs
@@ -68,7 +68,7 @@ public virtual void commit()
}
/*Mark another character for deferred consumption */
- public virtual char consume()
+ public virtual int consume()
{
numToConsume++;
return LA(1);
@@ -107,10 +107,13 @@ public virtual bool isMarked()
}
/*Get a lookahead character */
- public virtual char LA(int i)
+ public virtual int LA(int i)
{
fill(i);
- return (char) queue[markerOffset + i - 1];
+ // Entries are boxed char values for real input and a boxed int sentinel
+ // (CharScanner.EOF_CHAR) for end-of-input; Convert.ToInt32 handles both so a
+ // real U+FFFF (65535) stays distinct from the EOF sentinel (65536).
+ return Convert.ToInt32(queue[markerOffset + i - 1]);
}
/*Return an integer marker that can be used to rewind the buffer to
diff --git a/SqlScriptDom/antlr/antlr/MismatchedCharException.cs b/SqlScriptDom/antlr/antlr/MismatchedCharException.cs
index 691fd27..0874bd3 100644
--- a/SqlScriptDom/antlr/antlr/MismatchedCharException.cs
+++ b/SqlScriptDom/antlr/antlr/MismatchedCharException.cs
@@ -152,6 +152,36 @@ public MismatchedCharException(char c, BitSet set_, bool matchNot, CharScanner s
scanner = scanner_;
}
+ // int overloads so the lexer can pass a lookahead that may be the
+ // out-of-char-range EOF sentinel without a cast/overflow at the call site.
+ public MismatchedCharException(int c, char lower, char upper_, bool matchNot, CharScanner scanner_) :
+ base("Mismatched char", scanner_.getFilename(), scanner_.getLine(), scanner_.getColumn())
+ {
+ mismatchType = matchNot ? CharTypeEnum.NotRangeType : CharTypeEnum.RangeType;
+ foundChar = c;
+ expecting = lower;
+ upper = upper_;
+ scanner = scanner_;
+ }
+
+ public MismatchedCharException(int c, char expecting_, bool matchNot, CharScanner scanner_) :
+ base("Mismatched char", scanner_.getFilename(), scanner_.getLine(), scanner_.getColumn())
+ {
+ mismatchType = matchNot ? CharTypeEnum.NotCharType : CharTypeEnum.CharType;
+ foundChar = c;
+ expecting = expecting_;
+ scanner = scanner_;
+ }
+
+ public MismatchedCharException(int c, BitSet set_, bool matchNot, CharScanner scanner_) :
+ base("Mismatched char", scanner_.getFilename(), scanner_.getLine(), scanner_.getColumn())
+ {
+ mismatchType = matchNot ? CharTypeEnum.NotSetType : CharTypeEnum.SetType;
+ foundChar = c;
+ bset = set_;
+ scanner = scanner_;
+ }
+
protected MismatchedCharException(SerializationInfo info, StreamingContext context)
: base(info, context)
{ }
@@ -165,8 +195,7 @@ private static void appendCharName(StringBuilder sb, int c)
{
switch (c)
{
- case 65535 :
- // 65535 = (char) -1 = EOF
+ case CharScanner.EOF_CHAR :
sb.Append("''");
break;
case '\n' :
diff --git a/SqlScriptDom/antlr/antlr/NoViableAltForCharException.cs b/SqlScriptDom/antlr/antlr/NoViableAltForCharException.cs
index 7f452c7..d50e943 100644
--- a/SqlScriptDom/antlr/antlr/NoViableAltForCharException.cs
+++ b/SqlScriptDom/antlr/antlr/NoViableAltForCharException.cs
@@ -23,7 +23,7 @@ namespace antlr
[Serializable]
internal class NoViableAltForCharException : RecognitionException
{
- public char foundChar;
+ public int foundChar;
public NoViableAltForCharException(char c, CharScanner scanner) :
base("NoViableAlt", scanner.getFilename(), scanner.getLine(), scanner.getColumn())
@@ -37,6 +37,20 @@ public NoViableAltForCharException(char c, string fileName, int line, int column
foundChar = c;
}
+ // int overloads so the generated lexer can pass a lookahead that may be the
+ // out-of-char-range EOF sentinel without a cast/overflow at the call site.
+ public NoViableAltForCharException(int c, CharScanner scanner) :
+ base("NoViableAlt", scanner.getFilename(), scanner.getLine(), scanner.getColumn())
+ {
+ foundChar = c;
+ }
+
+ public NoViableAltForCharException(int c, string fileName, int line, int column) :
+ base("NoViableAlt", fileName, line, column)
+ {
+ foundChar = c;
+ }
+
protected NoViableAltForCharException(SerializationInfo info, StreamingContext context)
: base(info, context)
{ }
@@ -54,16 +68,20 @@ override public string Message
// But java seems to lack something isprint-ish..
// so we do it manually. This is probably too restrictive.
- if ((foundChar >= ' ') && (foundChar <= '~'))
+ if (foundChar == CharScanner.EOF_CHAR)
+ {
+ mesg.Append("");
+ }
+ else if ((foundChar >= ' ') && (foundChar <= '~'))
{
mesg.Append('\'');
- mesg.Append(foundChar);
+ mesg.Append((char) foundChar);
mesg.Append('\'');
}
else
{
mesg.Append("0x");
- mesg.Append(((int)foundChar).ToString("X", CultureInfo.InvariantCulture));
+ mesg.Append(foundChar.ToString("X", CultureInfo.InvariantCulture));
}
return mesg.ToString();
}
diff --git a/Test/SqlDom/UnicodeNonCharacterLexerTests.cs b/Test/SqlDom/UnicodeNonCharacterLexerTests.cs
new file mode 100644
index 0000000..ad504b8
--- /dev/null
+++ b/Test/SqlDom/UnicodeNonCharacterLexerTests.cs
@@ -0,0 +1,319 @@
+//------------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+//------------------------------------------------------------------------------
+
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.SqlServer.TransactSql.ScriptDom;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using SqlStudio.Tests.AssemblyTools.TestCategory;
+
+namespace SqlStudio.Tests.UTSqlScriptDom
+{
+ ///
+ /// Regression tests for the lexer treating the Unicode non-character U+FFFF
+ /// (char.MaxValue) as an end-of-input sentinel. A literal U+FFFF appearing inside
+ /// a string literal, delimited identifier, or comment must be scanned as a normal
+ /// character; real end-of-input must be detected by stream position only.
+ ///
+ /// Downstream: DacFx GitHub issue #796 - stored procedures
+ /// whose bodies contain NCHAR(0xFFFE)/NCHAR(0xFFFF) caused continuous schema drift
+ /// because ScriptDom stopped scanning at the literal U+FFFF.
+ ///
+ public partial class SqlDomTests
+ {
+ // Unicode non-characters. U+FFFE is handled fine today; only U+FFFF
+ // (== char.MaxValue == CharScanner.EOF_CHAR) triggers the failure.
+ private const string UFFFE = "\uFFFE";
+ private const string UFFFF = "\uFFFF";
+
+ private static TSqlScript ParseScript(TSqlParser parser, string source, out IList errors)
+ {
+ using (StringReader reader = new StringReader(source))
+ {
+ return (TSqlScript)parser.Parse(reader, out errors);
+ }
+ }
+
+ ///
+ /// Lexer-level guarantee across every parser version: an N'...' string literal
+ /// containing both U+FFFE and U+FFFF parses cleanly and the literal value
+ /// round-trips with the U+FFFF preserved (proving the scanner consumed past it).
+ ///
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void LexerTreatsUFFFFAsLiteralInStringLiteral()
+ {
+ string source = "SELECT N'hi" + UFFFE + UFFFF + "lo' AS c;";
+
+ ParserTestUtils.ExecuteTestForAllParsers(parser =>
+ {
+ TSqlScript script = ParseScript(parser, source, out IList errors);
+
+ ParserTestUtils.LogErrors(errors, source);
+ Assert.AreEqual(0, errors.Count, "U+FFFF inside a string literal must not terminate scanning.");
+
+ SelectStatement select = (SelectStatement)script.Batches[0].Statements[0];
+ QuerySpecification query = (QuerySpecification)select.QueryExpression;
+ SelectScalarExpression element = (SelectScalarExpression)query.SelectElements[0];
+ StringLiteral literal = (StringLiteral)element.Expression;
+
+ Assert.AreEqual("hi" + UFFFE + UFFFF + "lo", literal.Value,
+ "The U+FFFF must round-trip as a literal character inside the string literal.");
+ }, true);
+ }
+
+ ///
+ /// The verbatim DacFx minimal repro: a CREATE PROCEDURE whose body contains a
+ /// string literal with U+FFFE + U+FFFF must parse to a single complete statement
+ /// that spans the entire input.
+ ///
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void ParseProcedureWithUnicodeNonCharactersIsComplete()
+ {
+ string body =
+ "CREATE PROCEDURE dbo.p AS BEGIN " +
+ "DECLARE @s NVARCHAR(50) = N'hi" + UFFFE + UFFFF + "lo'; " +
+ "SELECT @s; END";
+
+ ParserTestUtils.ExecuteTestForParsers(parser =>
+ {
+ TSqlScript script = ParseScript(parser, body, out IList errors);
+
+ ParserTestUtils.LogErrors(errors, body);
+ Assert.AreEqual(0, errors.Count, "U+FFFF in the procedure body must not terminate scanning.");
+
+ IList statements = script.Batches[0].Statements;
+ Assert.AreEqual(1, statements.Count, "Exactly one statement should be produced.");
+
+ CreateProcedureStatement statement = (CreateProcedureStatement)statements[0];
+ Assert.AreEqual(body.Length, statement.StartOffset + statement.FragmentLength,
+ "The statement must cover the whole input, including everything after U+FFFF.");
+ }, new TSql160Parser(true), new TSql170Parser(true), new TSql180Parser(true));
+ }
+
+ ///
+ /// U+FFFF inside the bodies of CREATE VIEW / FUNCTION / TRIGGER must not stop
+ /// scanning; each produces a single statement covering the whole input.
+ ///
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void LexerHandlesUFFFFInViewFunctionTriggerBodies()
+ {
+ string literal = "N'hi" + UFFFE + UFFFF + "lo'";
+ string view = "CREATE VIEW dbo.v AS SELECT " + literal + " AS c";
+ string function =
+ "CREATE FUNCTION dbo.f() RETURNS NVARCHAR(50) AS BEGIN RETURN " + literal + "; END";
+ string trigger =
+ "CREATE TRIGGER dbo.tr ON dbo.t AFTER INSERT AS SELECT " + literal + " AS c";
+
+ ParserTestUtils.ExecuteTestForParsers(parser =>
+ {
+ foreach (string body in new[] { view, function, trigger })
+ {
+ TSqlScript script = ParseScript(parser, body, out IList errors);
+
+ ParserTestUtils.LogErrors(errors, body);
+ Assert.AreEqual(0, errors.Count, "U+FFFF must not terminate scanning for: " + body);
+
+ IList statements = script.Batches[0].Statements;
+ Assert.AreEqual(1, statements.Count, "Exactly one statement should be produced for: " + body);
+ Assert.AreEqual(body.Length,
+ statements[0].StartOffset + statements[0].FragmentLength,
+ "The statement must span the whole input for: " + body);
+ }
+ }, new TSql160Parser(true), new TSql170Parser(true), new TSql180Parser(true));
+ }
+
+ ///
+ /// A delimited identifier containing U+FFFF must parse and preserve the character.
+ ///
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void LexerHandlesUFFFFInDelimitedIdentifier()
+ {
+ string source = "SELECT 1 AS [col" + UFFFF + "];";
+
+ ParserTestUtils.ExecuteTestForAllParsers(parser =>
+ {
+ TSqlScript script = ParseScript(parser, source, out IList errors);
+
+ ParserTestUtils.LogErrors(errors, source);
+ Assert.AreEqual(0, errors.Count, "U+FFFF inside a delimited identifier must not terminate scanning.");
+
+ SelectStatement select = (SelectStatement)script.Batches[0].Statements[0];
+ QuerySpecification query = (QuerySpecification)select.QueryExpression;
+ SelectScalarExpression element = (SelectScalarExpression)query.SelectElements[0];
+
+ Assert.AreEqual("col" + UFFFF, element.ColumnName.Identifier.Value,
+ "The U+FFFF must round-trip inside the delimited identifier.");
+ }, true);
+ }
+
+ ///
+ /// U+FFFF inside a block comment and a line comment must not terminate scanning;
+ /// the following SELECT must still be parsed.
+ ///
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void LexerHandlesUFFFFInComments()
+ {
+ string blockComment = "/* c" + UFFFF + " */ SELECT 1 AS c;";
+ string lineComment = "-- c" + UFFFF + "\r\nSELECT 1 AS c;";
+
+ ParserTestUtils.ExecuteTestForAllParsers(parser =>
+ {
+ foreach (string source in new[] { blockComment, lineComment })
+ {
+ TSqlScript script = ParseScript(parser, source, out IList errors);
+
+ ParserTestUtils.LogErrors(errors, source);
+ Assert.AreEqual(0, errors.Count, "U+FFFF inside a comment must not terminate scanning for: " + source);
+
+ IList statements = script.Batches[0].Statements;
+ Assert.AreEqual(1, statements.Count,
+ "The SELECT after the comment must still be parsed for: " + source);
+ Assert.IsInstanceOfType(statements[0], typeof(SelectStatement));
+ }
+ }, true);
+ }
+
+ ///
+ /// Sanity guard: a string literal that genuinely runs to end-of-stream must still
+ /// report the unterminated-literal error. The fix must not mask real EOF errors.
+ ///
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void LexerStillReportsUnterminatedLiteralAtEndOfStream()
+ {
+ string source = "SELECT N'abc";
+
+ ParserTestUtils.ExecuteTestForAllParsers(parser =>
+ {
+ ParseScript(parser, source, out IList errors);
+ Assert.AreEqual(1, errors.Count,
+ "A genuinely unterminated literal at end-of-stream must still produce an error.");
+ }, true);
+ }
+
+ ///
+ /// The integer lookahead contract must retain every char value while keeping
+ /// end-of-input outside that range, without changing ordinary case folding.
+ ///
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void LexerIntegerLookaheadPreservesCharacterAndSentinelValues()
+ {
+ antlr.CharBuffer inputBuffer = new antlr.CharBuffer(new StringReader(UFFFF));
+
+ Assert.AreEqual((int)char.MaxValue, inputBuffer.LA(1));
+ Assert.AreEqual(antlr.CharScanner.EOF_CHAR, inputBuffer.LA(2));
+
+ DiagnosticCharScanner scanner = new DiagnosticCharScanner();
+ Assert.AreEqual((int)'a', scanner.toLower('A'));
+ Assert.AreEqual(antlr.CharScanner.EOF_CHAR, scanner.toLower(antlr.CharScanner.EOF_CHAR));
+ Assert.AreEqual(-1, scanner.toLower(-1));
+ }
+
+ ///
+ /// Lexer exceptions must retain the out-of-range EOF sentinel so diagnostics do
+ /// not confuse end-of-input with a real U+0000 or U+FFFF character.
+ ///
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void LexerExceptionDiagnosticsPreserveEndOfInput()
+ {
+ antlr.NoViableAltForCharException eofAlternative = new antlr.NoViableAltForCharException(
+ antlr.CharScanner.EOF_CHAR, string.Empty, 1, 1);
+ antlr.NoViableAltForCharException nullAlternative =
+ new antlr.NoViableAltForCharException('\0', string.Empty, 1, 1);
+ antlr.NoViableAltForCharException uffffAlternative =
+ new antlr.NoViableAltForCharException(char.MaxValue, string.Empty, 1, 1);
+
+ Assert.AreEqual(antlr.CharScanner.EOF_CHAR, eofAlternative.foundChar);
+ Assert.AreEqual("unexpected char: ", eofAlternative.Message);
+ Assert.AreEqual("unexpected char: 0x0", nullAlternative.Message);
+ Assert.AreEqual("unexpected char: 0xFFFF", uffffAlternative.Message);
+
+ antlr.TokenStreamRecognitionException eofTokenStreamException =
+ new antlr.TokenStreamRecognitionException(eofAlternative);
+ ParseError eofParseError = TSql80ParserBaseInternal.ProcessTokenStreamRecognitionException(
+ eofTokenStreamException, 0);
+ antlr.NoViableAltForCharException printableAlternative =
+ new antlr.NoViableAltForCharException('@', string.Empty, 1, 1);
+ ParseError printableParseError = TSql80ParserBaseInternal.ProcessTokenStreamRecognitionException(
+ new antlr.TokenStreamRecognitionException(printableAlternative), 0);
+
+ Assert.AreEqual("Incorrect syntax near ''.", eofParseError.Message);
+ Assert.AreEqual("Incorrect syntax near '@'.", printableParseError.Message);
+
+ DiagnosticCharScanner scanner = new DiagnosticCharScanner();
+ antlr.MismatchedCharException eofMismatch = new antlr.MismatchedCharException(
+ antlr.CharScanner.EOF_CHAR, 'x', false, scanner);
+ antlr.MismatchedCharException uffffMismatch = new antlr.MismatchedCharException(
+ char.MaxValue, 'x', false, scanner);
+
+ Assert.AreEqual(antlr.CharScanner.EOF_CHAR, eofMismatch.foundChar);
+ StringAssert.Contains(eofMismatch.Message, "''");
+ Assert.AreEqual(char.MaxValue, uffffMismatch.foundChar);
+ Assert.IsFalse(uffffMismatch.Message.Contains(""));
+ }
+
+ ///
+ /// Generator round-trip: parse -> SqlScriptGenerator -> string must preserve U+FFFF,
+ /// and the generated script must re-parse with zero errors.
+ ///
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void GeneratorRoundTripsUFFFFInStringLiteral()
+ {
+ string body =
+ "CREATE PROCEDURE dbo.p AS BEGIN " +
+ "DECLARE @s NVARCHAR(50) = N'hi" + UFFFE + UFFFF + "lo'; " +
+ "SELECT @s; END";
+
+ RoundTrip(new TSql160Parser(true), SqlVersion.Sql160, body);
+ RoundTrip(new TSql170Parser(true), SqlVersion.Sql170, body);
+ RoundTrip(new TSql180Parser(true), SqlVersion.Sql180, body);
+ }
+
+ private static void RoundTrip(TSqlParser parser, SqlVersion version, string body)
+ {
+ TSqlScript script = ParseScript(parser, body, out IList errors);
+ ParserTestUtils.LogErrors(errors, body);
+ Assert.AreEqual(0, errors.Count, "Source must parse cleanly before generation.");
+
+ SqlScriptGenerator generator = ParserTestUtils.CreateScriptGen(version);
+ StringWriter writer = new StringWriter();
+ generator.GenerateScript(script, writer);
+ string generated = writer.ToString();
+
+ Assert.IsTrue(generated.Contains(UFFFF), "Generated script must preserve U+FFFF.");
+ Assert.IsTrue(generated.Contains(UFFFE), "Generated script must preserve U+FFFE.");
+
+ ParseScript(parser, generated, out IList reparseErrors);
+ ParserTestUtils.LogErrors(reparseErrors, generated);
+ Assert.AreEqual(0, reparseErrors.Count, "Generated script must re-parse with zero errors.");
+ }
+
+ private sealed class DiagnosticCharScanner : antlr.CharScanner
+ {
+ public DiagnosticCharScanner()
+ : base(new antlr.CharBuffer(new StringReader(string.Empty)))
+ {
+ }
+ }
+ }
+}
From 2156c6af83a85a29c1af8fa8dbe85c7956b30fbf Mon Sep 17 00:00:00 2001
From: Allan Cascante <120929896+acascante_microsoft@users.noreply.github.com>
Date: Mon, 12 Oct 2026 15:59:59 -0600
Subject: [PATCH 06/13] Add shared ON ERROR clauses for Fabric DW AI functions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
SqlScriptDom/Parser/TSql/Ast.xml | 20 ++-
SqlScriptDom/Parser/TSql/TSqlFabricDW.g | 130 +++++++++++++++---
...ratorVisitor.AiAnalyzeSentimentFunction.cs | 1 +
...riptGeneratorVisitor.AiClassifyFunction.cs | 1 +
...criptGeneratorVisitor.AiExtractFunction.cs | 1 +
...ptGeneratorVisitor.AiFixGrammarFunction.cs | 1 +
...ratorVisitor.AiGenerateResponseFunction.cs | 1 +
...iptGeneratorVisitor.AiSummarizeFunction.cs | 1 +
...iptGeneratorVisitor.AiTranslateFunction.cs | 41 ++++++
.../AiAnalyzeSentimentTestsFabricDW.sql | 8 +-
.../AiClassifyTestsFabricDW.sql | 8 +-
.../AiExtractTestsFabricDW.sql | 8 +-
.../AiFixGrammarTestsFabricDW.sql | 8 +-
.../AiGenerateResponseTestsFabricDW.sql | 8 +-
.../AiSummarizeTestsFabricDW.sql | 8 +-
.../AiTranslateTestsFabricDW.sql | 10 +-
Test/SqlDom/OnlyFabricDWSyntaxTests.cs | 91 ++++++++++--
Test/SqlDom/ParserErrorsTests.cs | 74 +++++++++-
.../AiAnalyzeSentimentTestsFabricDW.sql | 8 +-
.../TestScripts/AiClassifyTestsFabricDW.sql | 7 +-
.../TestScripts/AiExtractTestsFabricDW.sql | 7 +-
.../TestScripts/AiFixGrammarTestsFabricDW.sql | 8 +-
.../AiGenerateResponseTestsFabricDW.sql | 5 +-
.../TestScripts/AiSummarizeTestsFabricDW.sql | 8 +-
.../TestScripts/AiTranslateTestsFabricDW.sql | 6 +-
25 files changed, 426 insertions(+), 43 deletions(-)
diff --git a/SqlScriptDom/Parser/TSql/Ast.xml b/SqlScriptDom/Parser/TSql/Ast.xml
index 95e2762..3e2f628 100644
--- a/SqlScriptDom/Parser/TSql/Ast.xml
+++ b/SqlScriptDom/Parser/TSql/Ast.xml
@@ -4828,34 +4828,52 @@
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
index d18c51a..4347f5e 100644
--- a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
+++ b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
@@ -32301,10 +32301,11 @@ withinGroupClause returns [WithinGroupClause vResult = FragmentFactory.CreateFra
}
;
-// AI_ANALYZE_SENTIMENT()
+// AI_ANALYZE_SENTIMENT( [(ERROR | NULL | DEFAULT ) ON ERROR])
aiAnalyzeSentimentFunctionCall returns [AIAnalyzeSentimentFunctionCall vResult = this.FragmentFactory.CreateFragment()]
{
ScalarExpression vInput;
+ AIOnErrorClause vOnErrorClause;
}
: tFunc:Identifier LeftParenthesis
{
@@ -32315,15 +32316,25 @@ aiAnalyzeSentimentFunctionCall returns [AIAnalyzeSentimentFunctionCall vResult =
{
vResult.Input = vInput;
}
- RightParenthesis
+ (
+ vOnErrorClause = aiOnErrorClause
+ {
+ vResult.OnErrorClause = vOnErrorClause;
+ }
+ )?
+ tRParen:RightParenthesis
+ {
+ UpdateTokenInfo(vResult, tRParen);
+ }
;
-// AI_CLASSIFY(, [, ... up to 21])
+// AI_CLASSIFY(, [, ... up to 20] [(ERROR | NULL | DEFAULT ) ON ERROR])
// Input and labels accept any expression; type constraints are deferred to semantic/type phase.
aiClassifyFunctionCall returns [AIClassifyFunctionCall vResult = this.FragmentFactory.CreateFragment()]
{
ScalarExpression vExpr;
int labelsCount = 0;
+ AIOnErrorClause vOnErrorClause;
}
: tFunc:Identifier LeftParenthesis
{
@@ -32341,29 +32352,36 @@ aiClassifyFunctionCall returns [AIClassifyFunctionCall vResult = this.FragmentFa
AddAndUpdateTokenInfo(vResult, vResult.Labels, vExpr);
labelsCount = 1;
}
- // additional labels up to 21 total
+ // additional labels up to 20 total
(
Comma vExpr = expression
{
++labelsCount;
- if (labelsCount > 21)
+ if (labelsCount > 20)
{ThrowParseErrorException("SQL46010", vExpr, TSqlParserResource.SQL46010Message, CodeGenerationSupporter.AIClassify);
}
AddAndUpdateTokenInfo(vResult, vResult.Labels, vExpr);
}
)*
+ (
+ vOnErrorClause = aiOnErrorClause
+ {
+ vResult.OnErrorClause = vOnErrorClause;
+ }
+ )?
tRParen:RightParenthesis
{
UpdateTokenInfo(vResult, tRParen);
}
;
-// AI_EXTRACT(, [, ... up to 21])
+// AI_EXTRACT(, [, ... up to 20] [(ERROR | NULL | DEFAULT ) ON ERROR])
// Input and labels accept any expression; type constraints are deferred to semantic/type phase.
aiExtractFunctionCall returns [AIExtractFunctionCall vResult = this.FragmentFactory.CreateFragment()]
{
ScalarExpression vExpr;
int labelsCount = 0;
+ AIOnErrorClause vOnErrorClause;
}
: tFunc:Identifier LeftParenthesis
{
@@ -32381,27 +32399,34 @@ aiExtractFunctionCall returns [AIExtractFunctionCall vResult = this.FragmentFact
AddAndUpdateTokenInfo(vResult, vResult.Labels, vExpr);
labelsCount = 1;
}
- // additional labels up to 21 total
+ // additional labels up to 20 total
(
Comma vExpr = expression
{
++labelsCount;
- if (labelsCount > 21)
+ if (labelsCount > 20)
{ThrowParseErrorException("SQL46010", vExpr, TSqlParserResource.SQL46010Message, CodeGenerationSupporter.AIExtract);
}
AddAndUpdateTokenInfo(vResult, vResult.Labels, vExpr);
}
)*
+ (
+ vOnErrorClause = aiOnErrorClause
+ {
+ vResult.OnErrorClause = vOnErrorClause;
+ }
+ )?
tRParen:RightParenthesis
{
UpdateTokenInfo(vResult, tRParen);
}
;
-// AI_FIX_GRAMMAR()
+// AI_FIX_GRAMMAR( [(ERROR | NULL | DEFAULT ) ON ERROR])
aiFixGrammarFunctionCall returns [AIFixGrammarFunctionCall vResult = this.FragmentFactory.CreateFragment()]
{
ScalarExpression vInput;
+ AIOnErrorClause vOnErrorClause;
}
: tFunc:Identifier LeftParenthesis
{
@@ -32412,14 +32437,24 @@ aiFixGrammarFunctionCall returns [AIFixGrammarFunctionCall vResult = this.Fragme
{
vResult.Input = vInput;
}
- RightParenthesis
+ (
+ vOnErrorClause = aiOnErrorClause
+ {
+ vResult.OnErrorClause = vOnErrorClause;
+ }
+ )?
+ tRParen:RightParenthesis
+ {
+ UpdateTokenInfo(vResult, tRParen);
+ }
;
-// AI_GENERATE_RESPONSE([, ])
+// AI_GENERATE_RESPONSE([, ] [(ERROR | NULL | DEFAULT ) ON ERROR])
aiGenerateResponseFunctionCall returns [AIGenerateResponseFunctionCall vResult = this.FragmentFactory.CreateFragment()]
{
ScalarExpression vPart1;
ScalarExpression vPart2;
+ AIOnErrorClause vOnErrorClause;
}
: tFunc:Identifier LeftParenthesis
{
@@ -32436,13 +32471,23 @@ aiGenerateResponseFunctionCall returns [AIGenerateResponseFunctionCall vResult =
vResult.PromptPart2 = vPart2;
}
)?
- RightParenthesis
+ (
+ vOnErrorClause = aiOnErrorClause
+ {
+ vResult.OnErrorClause = vOnErrorClause;
+ }
+ )?
+ tRParen:RightParenthesis
+ {
+ UpdateTokenInfo(vResult, tRParen);
+ }
;
-// AI_SUMMARIZE()
+// AI_SUMMARIZE( [(ERROR | NULL | DEFAULT ) ON ERROR])
aiSummarizeFunctionCall returns [AISummarizeFunctionCall vResult = this.FragmentFactory.CreateFragment()]
{
ScalarExpression vInput;
+ AIOnErrorClause vOnErrorClause;
}
: tFunc:Identifier LeftParenthesis
{
@@ -32453,14 +32498,24 @@ aiSummarizeFunctionCall returns [AISummarizeFunctionCall vResult = this.Fragment
{
vResult.Input = vInput;
}
- RightParenthesis
+ (
+ vOnErrorClause = aiOnErrorClause
+ {
+ vResult.OnErrorClause = vOnErrorClause;
+ }
+ )?
+ tRParen:RightParenthesis
+ {
+ UpdateTokenInfo(vResult, tRParen);
+ }
;
-// AI_TRANSLATE(, )
+// AI_TRANSLATE(, [(ERROR | NULL | DEFAULT ) ON ERROR])
aiTranslateFunctionCall returns [AITranslateFunctionCall vResult = this.FragmentFactory.CreateFragment()]
{
ScalarExpression vInput;
ScalarExpression vLang;
+ AIOnErrorClause vOnErrorClause;
}
: tFunc:Identifier LeftParenthesis
{
@@ -32475,7 +32530,50 @@ aiTranslateFunctionCall returns [AITranslateFunctionCall vResult = this.Fragment
{
vResult.Language = vLang;
}
- RightParenthesis
+ (
+ vOnErrorClause = aiOnErrorClause
+ {
+ vResult.OnErrorClause = vOnErrorClause;
+ }
+ )?
+ tRParen:RightParenthesis
+ {
+ UpdateTokenInfo(vResult, tRParen);
+ }
+ ;
+
+aiOnErrorClause returns [AIOnErrorClause vResult = null]
+{
+ StringLiteral vDefaultValue;
+}
+ :
+ (
+ tErrorAction:Identifier
+ {
+ Match(tErrorAction, CodeGenerationSupporter.Error);
+ vResult = this.FragmentFactory.CreateFragment();
+ UpdateTokenInfo(vResult, tErrorAction);
+ }
+ |
+ tNull:Null
+ {
+ vResult = this.FragmentFactory.CreateFragment();
+ UpdateTokenInfo(vResult, tNull);
+ }
+ |
+ tDefault:Default vDefaultValue=stringLiteral
+ {
+ AIDefaultOnErrorClause vDefaultClause = this.FragmentFactory.CreateFragment();
+ vDefaultClause.Value = vDefaultValue;
+ vResult = vDefaultClause;
+ UpdateTokenInfo(vResult, tDefault);
+ }
+ )
+ On tError:Identifier
+ {
+ Match(tError, CodeGenerationSupporter.Error);
+ UpdateTokenInfo(vResult, tError);
+ }
;
// INVOKE_EXTERNAL_API('', '' [, [, ... ]])
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiAnalyzeSentimentFunction.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiAnalyzeSentimentFunction.cs
index e584657..553b246 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiAnalyzeSentimentFunction.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiAnalyzeSentimentFunction.cs
@@ -22,6 +22,7 @@ public override void ExplicitVisit(AIAnalyzeSentimentFunctionCall node)
}
GenerateSymbol(TSqlTokenType.LeftParenthesis);
GenerateFragmentIfNotNull(node.Input);
+ GenerateSpaceAndFragmentIfNotNull(node.OnErrorClause);
GenerateSymbol(TSqlTokenType.RightParenthesis);
}
}
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiClassifyFunction.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiClassifyFunction.cs
index d6997cb..702f6b6 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiClassifyFunction.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiClassifyFunction.cs
@@ -38,6 +38,7 @@ public override void ExplicitVisit(AIClassifyFunctionCall node)
}
}
+ GenerateSpaceAndFragmentIfNotNull(node.OnErrorClause);
GenerateSymbol(TSqlTokenType.RightParenthesis);
}
}
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiExtractFunction.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiExtractFunction.cs
index 7ba23ad..d0508e6 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiExtractFunction.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiExtractFunction.cs
@@ -39,6 +39,7 @@ public override void ExplicitVisit(AIExtractFunctionCall node)
}
}
+ GenerateSpaceAndFragmentIfNotNull(node.OnErrorClause);
GenerateSymbol(TSqlTokenType.RightParenthesis);
}
}
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiFixGrammarFunction.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiFixGrammarFunction.cs
index 3d9f8b3..909f938 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiFixGrammarFunction.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiFixGrammarFunction.cs
@@ -22,6 +22,7 @@ public override void ExplicitVisit(AIFixGrammarFunctionCall node)
}
GenerateSymbol(TSqlTokenType.LeftParenthesis);
GenerateFragmentIfNotNull(node.Input);
+ GenerateSpaceAndFragmentIfNotNull(node.OnErrorClause);
GenerateSymbol(TSqlTokenType.RightParenthesis);
}
}
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiGenerateResponseFunction.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiGenerateResponseFunction.cs
index b9affe8..f101d74 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiGenerateResponseFunction.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiGenerateResponseFunction.cs
@@ -33,6 +33,7 @@ public override void ExplicitVisit(AIGenerateResponseFunctionCall node)
GenerateFragmentIfNotNull(node.PromptPart2);
}
+ GenerateSpaceAndFragmentIfNotNull(node.OnErrorClause);
GenerateSymbol(TSqlTokenType.RightParenthesis);
}
}
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiSummarizeFunction.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiSummarizeFunction.cs
index 3d73ae5..5f5a3af 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiSummarizeFunction.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiSummarizeFunction.cs
@@ -23,6 +23,7 @@ public override void ExplicitVisit(AISummarizeFunctionCall node)
}
GenerateSymbol(TSqlTokenType.LeftParenthesis);
GenerateFragmentIfNotNull(node.Input);
+ GenerateSpaceAndFragmentIfNotNull(node.OnErrorClause);
GenerateSymbol(TSqlTokenType.RightParenthesis);
}
}
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiTranslateFunction.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiTranslateFunction.cs
index 10c56ed..f623917 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiTranslateFunction.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiTranslateFunction.cs
@@ -3,6 +3,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//------------------------------------------------------------------------------
+using System;
using Microsoft.SqlServer.TransactSql.ScriptDom;
namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator
@@ -28,7 +29,47 @@ public override void ExplicitVisit(AITranslateFunctionCall node)
GenerateSymbol(TSqlTokenType.Comma);
GenerateSpace();
GenerateFragmentIfNotNull(node.Language);
+ GenerateSpaceAndFragmentIfNotNull(node.OnErrorClause);
GenerateSymbol(TSqlTokenType.RightParenthesis);
}
+
+ ///
+ /// Emits ERROR ON ERROR behavior for AI functions.
+ ///
+ /// The error behavior clause to generate.
+ public override void ExplicitVisit(AIErrorOnErrorClause node)
+ {
+ GenerateIdentifier(CodeGenerationSupporter.Error);
+ GenerateSpaceAndKeyword(TSqlTokenType.On);
+ GenerateSpaceAndIdentifier(CodeGenerationSupporter.Error);
+ }
+
+ ///
+ /// Emits NULL ON ERROR behavior for AI functions.
+ ///
+ /// The null behavior clause to generate.
+ public override void ExplicitVisit(AINullOnErrorClause node)
+ {
+ GenerateKeyword(TSqlTokenType.Null);
+ GenerateSpaceAndKeyword(TSqlTokenType.On);
+ GenerateSpaceAndIdentifier(CodeGenerationSupporter.Error);
+ }
+
+ ///
+ /// Emits DEFAULT 'literal' ON ERROR behavior for AI functions.
+ ///
+ /// The default behavior clause to generate.
+ public override void ExplicitVisit(AIDefaultOnErrorClause node)
+ {
+ if (node.Value == null)
+ {
+ throw new InvalidOperationException("AIDefaultOnErrorClause.Value is required.");
+ }
+
+ GenerateKeyword(TSqlTokenType.Default);
+ GenerateSpaceAndFragmentIfNotNull(node.Value);
+ GenerateSpaceAndKeyword(TSqlTokenType.On);
+ GenerateSpaceAndIdentifier(CodeGenerationSupporter.Error);
+ }
}
}
diff --git a/Test/SqlDom/BaselinesFabricDW/AiAnalyzeSentimentTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/AiAnalyzeSentimentTestsFabricDW.sql
index 7c2656f..2b577a6 100644
--- a/Test/SqlDom/BaselinesFabricDW/AiAnalyzeSentimentTestsFabricDW.sql
+++ b/Test/SqlDom/BaselinesFabricDW/AiAnalyzeSentimentTestsFabricDW.sql
@@ -12,6 +12,12 @@ SELECT AI_ANALYZE_SENTIMENT(@s0 + 'a');
SELECT AI_ANALYZE_SENTIMENT('b' + 'a');
+SELECT AI_ANALYZE_SENTIMENT('text' ERROR ON ERROR);
+
+SELECT AI_ANALYZE_SENTIMENT('text' NULL ON ERROR);
+
+SELECT AI_ANALYZE_SENTIMENT('text' DEFAULT 'Sentiment unavailable' ON ERROR);
+
SELECT *
FROM AI_ANALYZE_SENTIMENT((SELECT t.text_col
FROM dbo.Texts AS t));
@@ -23,5 +29,5 @@ CREATE OR ALTER FUNCTION dbo.fx
RETURNS NVARCHAR (MAX)
AS
BEGIN
- RETURN (AI_ANALYZE_SENTIMENT(@s));
+ RETURN (AI_ANALYZE_SENTIMENT(@s DEFAULT N'Sentiment unavailable' ON ERROR));
END
diff --git a/Test/SqlDom/BaselinesFabricDW/AiClassifyTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/AiClassifyTestsFabricDW.sql
index 4703721..9728202 100644
--- a/Test/SqlDom/BaselinesFabricDW/AiClassifyTestsFabricDW.sql
+++ b/Test/SqlDom/BaselinesFabricDW/AiClassifyTestsFabricDW.sql
@@ -15,6 +15,12 @@ SELECT *
FROM AI_CLASSIFY((SELECT t.text_col
FROM dbo.Texts AS t), 'topic', 'sentiment', 'intent');
+SELECT AI_CLASSIFY('text', 'spam' ERROR ON ERROR);
+
+SELECT AI_CLASSIFY('text', 'spam', 'ham' NULL ON ERROR);
+
+SELECT AI_CLASSIFY(@s, 'topic', 'sentiment', 'intent' DEFAULT 'Classification unavailable' ON ERROR);
+
GO
CREATE OR ALTER FUNCTION dbo.fx
@@ -22,5 +28,5 @@ CREATE OR ALTER FUNCTION dbo.fx
RETURNS NVARCHAR (MAX)
AS
BEGIN
- RETURN (AI_CLASSIFY(@s, 'labelA', 'labelB'));
+ RETURN (AI_CLASSIFY(@s, 'labelA', 'labelB' DEFAULT N'Classification unavailable' ON ERROR));
END
diff --git a/Test/SqlDom/BaselinesFabricDW/AiExtractTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/AiExtractTestsFabricDW.sql
index 4af178b..ce3b3ab 100644
--- a/Test/SqlDom/BaselinesFabricDW/AiExtractTestsFabricDW.sql
+++ b/Test/SqlDom/BaselinesFabricDW/AiExtractTestsFabricDW.sql
@@ -15,6 +15,12 @@ SELECT *
FROM AI_EXTRACT((SELECT t.text_col
FROM dbo.Texts AS t), 'topic', 'sentiment', 'intent');
+SELECT AI_EXTRACT('text', 'spam' ERROR ON ERROR);
+
+SELECT AI_EXTRACT('text', 'spam', 'ham' NULL ON ERROR);
+
+SELECT AI_EXTRACT(@s, 'topic', 'sentiment', 'intent' DEFAULT 'Extraction unavailable' ON ERROR);
+
GO
CREATE OR ALTER FUNCTION dbo.fx
@@ -22,5 +28,5 @@ CREATE OR ALTER FUNCTION dbo.fx
RETURNS NVARCHAR (MAX)
AS
BEGIN
- RETURN (AI_EXTRACT(@s, 'labelA', 'labelB'));
+ RETURN (AI_EXTRACT(@s, 'labelA', 'labelB' DEFAULT N'Extraction unavailable' ON ERROR));
END
diff --git a/Test/SqlDom/BaselinesFabricDW/AiFixGrammarTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/AiFixGrammarTestsFabricDW.sql
index 7e6225a..8bfef16 100644
--- a/Test/SqlDom/BaselinesFabricDW/AiFixGrammarTestsFabricDW.sql
+++ b/Test/SqlDom/BaselinesFabricDW/AiFixGrammarTestsFabricDW.sql
@@ -12,6 +12,12 @@ SELECT AI_FIX_GRAMMAR(@s0 + 'a');
SELECT AI_FIX_GRAMMAR('b' + 'a');
+SELECT AI_FIX_GRAMMAR('text' ERROR ON ERROR);
+
+SELECT AI_FIX_GRAMMAR('text' NULL ON ERROR);
+
+SELECT AI_FIX_GRAMMAR('text' DEFAULT 'Correction unavailable' ON ERROR);
+
SELECT *
FROM AI_FIX_GRAMMAR((SELECT t.text_col
FROM dbo.Texts AS t));
@@ -23,5 +29,5 @@ CREATE OR ALTER FUNCTION dbo.fx
RETURNS NVARCHAR (MAX)
AS
BEGIN
- RETURN (AI_FIX_GRAMMAR(@s));
+ RETURN (AI_FIX_GRAMMAR(@s DEFAULT N'Correction unavailable' ON ERROR));
END
diff --git a/Test/SqlDom/BaselinesFabricDW/AiGenerateResponseTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/AiGenerateResponseTestsFabricDW.sql
index 5a0a6b5..34b1355 100644
--- a/Test/SqlDom/BaselinesFabricDW/AiGenerateResponseTestsFabricDW.sql
+++ b/Test/SqlDom/BaselinesFabricDW/AiGenerateResponseTestsFabricDW.sql
@@ -10,6 +10,12 @@ SELECT AI_GENERATE_RESPONSE(@p1 + 'part1', @p2);
SELECT AI_GENERATE_RESPONSE('b' + 'a');
+SELECT AI_GENERATE_RESPONSE('Hello' ERROR ON ERROR);
+
+SELECT AI_GENERATE_RESPONSE('Question:', ' What is the time?' NULL ON ERROR);
+
+SELECT AI_GENERATE_RESPONSE(@p1, @p2 DEFAULT 'Response unavailable' ON ERROR);
+
SELECT *
FROM AI_GENERATE_RESPONSE((SELECT t.text_col
FROM dbo.Texts AS t));
@@ -21,5 +27,5 @@ CREATE OR ALTER FUNCTION dbo.fx
RETURNS NVARCHAR (MAX)
AS
BEGIN
- RETURN (AI_GENERATE_RESPONSE(@p));
+ RETURN (AI_GENERATE_RESPONSE(@p DEFAULT N'Response unavailable' ON ERROR));
END
diff --git a/Test/SqlDom/BaselinesFabricDW/AiSummarizeTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/AiSummarizeTestsFabricDW.sql
index fbb972d..8991134 100644
--- a/Test/SqlDom/BaselinesFabricDW/AiSummarizeTestsFabricDW.sql
+++ b/Test/SqlDom/BaselinesFabricDW/AiSummarizeTestsFabricDW.sql
@@ -12,6 +12,12 @@ SELECT AI_SUMMARIZE(@s0 + 'a');
SELECT AI_SUMMARIZE('b' + 'a');
+SELECT AI_SUMMARIZE('text' ERROR ON ERROR);
+
+SELECT AI_SUMMARIZE('text' NULL ON ERROR);
+
+SELECT AI_SUMMARIZE('text' DEFAULT 'Summary unavailable' ON ERROR);
+
SELECT *
FROM AI_SUMMARIZE((SELECT t.text_col
FROM dbo.Texts AS t));
@@ -23,5 +29,5 @@ CREATE OR ALTER FUNCTION dbo.fx
RETURNS NVARCHAR (MAX)
AS
BEGIN
- RETURN (AI_SUMMARIZE(@s));
+ RETURN (AI_SUMMARIZE(@s DEFAULT N'Summary unavailable' ON ERROR));
END
diff --git a/Test/SqlDom/BaselinesFabricDW/AiTranslateTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/AiTranslateTestsFabricDW.sql
index 10b497d..e8c16a4 100644
--- a/Test/SqlDom/BaselinesFabricDW/AiTranslateTestsFabricDW.sql
+++ b/Test/SqlDom/BaselinesFabricDW/AiTranslateTestsFabricDW.sql
@@ -13,6 +13,14 @@ SELECT AI_TRANSLATE(@s, @lang);
SELECT AI_TRANSLATE(@s + 'a', @lang + 'a');
+SELECT AI_TRANSLATE('Hello world', 'es' ERROR ON ERROR);
+
+SELECT AI_TRANSLATE('Hello world', 'es' NULL ON ERROR);
+
+SELECT AI_TRANSLATE('Hello world', 'es' DEFAULT 'Translation unavailable' ON ERROR);
+
+SELECT AI_TRANSLATE(@s, @lang DEFAULT N'Translation unavailable' ON ERROR);
+
SELECT *
FROM AI_TRANSLATE((SELECT t.text_col
FROM dbo.Texts AS t), 'e' + 's');
@@ -24,7 +32,7 @@ CREATE OR ALTER FUNCTION dbo.fx
RETURNS NVARCHAR (MAX)
AS
BEGIN
- RETURN (AI_TRANSLATE(@s, 'fr'));
+ RETURN (AI_TRANSLATE(@s, 'fr' DEFAULT N'Translation unavailable' ON ERROR));
END
diff --git a/Test/SqlDom/OnlyFabricDWSyntaxTests.cs b/Test/SqlDom/OnlyFabricDWSyntaxTests.cs
index 285e0df..26cf684 100644
--- a/Test/SqlDom/OnlyFabricDWSyntaxTests.cs
+++ b/Test/SqlDom/OnlyFabricDWSyntaxTests.cs
@@ -1,4 +1,5 @@
-using System.IO;
+using System;
+using System.IO;
using Microsoft.SqlServer.TransactSql.ScriptDom;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SqlStudio.Tests.AssemblyTools.TestCategory;
@@ -19,13 +20,13 @@ public partial class SqlDomTests
new ParserTestFabricDW("NestedCTETestsFabricDW.sql", nErrors80: 1, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 2, nErrors140: 2, nErrors150: 2, nErrors160: 2, nErrors170: 2, nErrors180: 2),
new ParserTestFabricDW("ScalarFunctionTestsFabricDW.sql", nErrors80: 3, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0),
new ParserTestFabricDW("CreateEventSessionNotLikePredicate.sql", nErrors80: 2, nErrors90: 1, nErrors100: 1, nErrors110: 1, nErrors120: 1, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0),
- new ParserTestFabricDW("AiAnalyzeSentimentTestsFabricDW.sql", nErrors80: 2, nErrors90: 1, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0),
- new ParserTestFabricDW("AiClassifyTestsFabricDW.sql", nErrors80: 2, nErrors90: 1, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0),
- new ParserTestFabricDW("AiExtractTestsFabricDW.sql", nErrors80: 2, nErrors90: 1, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0),
- new ParserTestFabricDW("AiFixGrammarTestsFabricDW.sql", nErrors80: 2, nErrors90: 1, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0),
- new ParserTestFabricDW("AiGenerateResponseTestsFabricDW.sql", nErrors80: 3, nErrors90: 1, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0),
- new ParserTestFabricDW("AiSummarizeTestsFabricDW.sql", nErrors80: 2, nErrors90: 1, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0),
- new ParserTestFabricDW("AiTranslateTestsFabricDW.sql", nErrors80: 3, nErrors90: 1, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0),
+ new ParserTestFabricDW("AiAnalyzeSentimentTestsFabricDW.sql", nErrors80: 5, nErrors90: 1, nErrors100: 5, nErrors110: 5, nErrors120: 5, nErrors130: 5, nErrors140: 5, nErrors150: 5, nErrors160: 5, nErrors170: 5, nErrors180: 5),
+ new ParserTestFabricDW("AiClassifyTestsFabricDW.sql", nErrors80: 5, nErrors90: 1, nErrors100: 5, nErrors110: 5, nErrors120: 5, nErrors130: 5, nErrors140: 5, nErrors150: 5, nErrors160: 5, nErrors170: 5, nErrors180: 5),
+ new ParserTestFabricDW("AiExtractTestsFabricDW.sql", nErrors80: 5, nErrors90: 1, nErrors100: 5, nErrors110: 5, nErrors120: 5, nErrors130: 5, nErrors140: 5, nErrors150: 5, nErrors160: 5, nErrors170: 5, nErrors180: 5),
+ new ParserTestFabricDW("AiFixGrammarTestsFabricDW.sql", nErrors80: 5, nErrors90: 1, nErrors100: 5, nErrors110: 5, nErrors120: 5, nErrors130: 5, nErrors140: 5, nErrors150: 5, nErrors160: 5, nErrors170: 5, nErrors180: 5),
+ new ParserTestFabricDW("AiGenerateResponseTestsFabricDW.sql", nErrors80: 6, nErrors90: 1, nErrors100: 5, nErrors110: 5, nErrors120: 5, nErrors130: 5, nErrors140: 5, nErrors150: 5, nErrors160: 5, nErrors170: 5, nErrors180: 5),
+ new ParserTestFabricDW("AiSummarizeTestsFabricDW.sql", nErrors80: 5, nErrors90: 1, nErrors100: 5, nErrors110: 5, nErrors120: 5, nErrors130: 5, nErrors140: 5, nErrors150: 5, nErrors160: 5, nErrors170: 5, nErrors180: 5),
+ new ParserTestFabricDW("AiTranslateTestsFabricDW.sql", nErrors80: 3, nErrors90: 1, nErrors100: 6, nErrors110: 6, nErrors120: 6, nErrors130: 6, nErrors140: 6, nErrors150: 6, nErrors160: 6, nErrors170: 6, nErrors180: 6),
new ParserTestFabricDW("InvokeExternalApiTestsFabricDW.sql", nErrors80: 2, nErrors90: 1, nErrors100: 0, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0),
new ParserTestFabricDW("ExternalFunctionTestsFabricDW.sql", nErrors80: 12, nErrors90: 4, nErrors100: 4, nErrors110: 4, nErrors120: 4, nErrors130: 11, nErrors140: 11, nErrors150: 11, nErrors160: 11, nErrors170: 11, nErrors180: 0),
new ParserTestFabricDW("CreateDatabaseCollateBeforeEditionTestsFabricDW.sql", nErrors80: 3, nErrors90: 3, nErrors100: 3, nErrors110: 3, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0),
@@ -67,6 +68,80 @@ public void FabricDWGroupingAndOrderingTokenRanges(string clauseText)
}
}
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void AiTranslateOnErrorClauseAstShape()
+ {
+ const string sql =
+ "SELECT AI_TRANSLATE('text', 'es' ERROR ON ERROR);" +
+ "SELECT AI_TRANSLATE('text', 'es' NULL ON ERROR);" +
+ "SELECT AI_TRANSLATE('text', 'es' DEFAULT 'fallback' ON ERROR);";
+
+ var parser = new TSqlFabricDWParser(true);
+ var script = (TSqlScript)parser.Parse(new StringReader(sql), out var errors);
+
+ Assert.AreEqual(0, errors.Count);
+
+ var errorFunction = GetAiTranslateFunction(script, 0);
+ Assert.IsInstanceOfType(errorFunction.OnErrorClause, typeof(AIErrorOnErrorClause));
+
+ var nullFunction = GetAiTranslateFunction(script, 1);
+ Assert.IsInstanceOfType(nullFunction.OnErrorClause, typeof(AINullOnErrorClause));
+
+ var defaultFunction = GetAiTranslateFunction(script, 2);
+ Assert.IsInstanceOfType(defaultFunction.OnErrorClause, typeof(AIDefaultOnErrorClause));
+ Assert.AreEqual(
+ "fallback",
+ ((AIDefaultOnErrorClause)defaultFunction.OnErrorClause).Value.Value);
+ }
+
+ private static AITranslateFunctionCall GetAiTranslateFunction(TSqlScript script, int statementIndex)
+ {
+ var selectStatement = (SelectStatement)script.Batches[0].Statements[statementIndex];
+ var query = (QuerySpecification)selectStatement.QueryExpression;
+ return (AITranslateFunctionCall)((SelectScalarExpression)query.SelectElements[0]).Expression;
+ }
+
+ [DataTestMethod]
+ [DataRow("AI_ANALYZE_SENTIMENT('text')")]
+ [DataRow("AI_FIX_GRAMMAR('text' ERROR ON ERROR)")]
+ [DataRow("AI_GENERATE_RESPONSE('prompt', 'data' NULL ON ERROR)")]
+ [DataRow("AI_SUMMARIZE('text' DEFAULT 'fallback' ON ERROR)")]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void FabricDWAiFunctionTokenRanges(string functionText)
+ {
+ string sql = "SELECT " + functionText;
+ var parser = new TSqlFabricDWParser(true);
+ using (var reader = new StringReader(sql))
+ {
+ var script = (TSqlScript)parser.Parse(reader, out var errors);
+ Assert.AreEqual(0, errors.Count);
+ var select = (SelectStatement)script.Batches[0].Statements[0];
+ var function = ((SelectScalarExpression)((QuerySpecification)select.QueryExpression).SelectElements[0]).Expression;
+
+ Assert.AreEqual(functionText, sql.Substring(function.StartOffset, function.FragmentLength));
+ Assert.AreEqual(sql, sql.Substring(select.StartOffset, select.FragmentLength));
+ Assert.AreEqual(function.LastTokenIndex, select.LastTokenIndex);
+ }
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void AiDefaultOnErrorClauseRequiresValue()
+ {
+ var function = new AISummarizeFunctionCall
+ {
+ Input = new StringLiteral { Value = "text" },
+ OnErrorClause = new AIDefaultOnErrorClause(),
+ };
+
+ SqlScriptGenerator scriptGen = ParserTestUtils.CreateScriptGen(SqlVersion.SqlFabricDW);
+ Assert.ThrowsException(() => scriptGen.GenerateScript(function, out string _));
+ }
+
[TestMethod]
[Priority(0)]
[SqlStudioTestCategory(Category.UnitTest)]
diff --git a/Test/SqlDom/ParserErrorsTests.cs b/Test/SqlDom/ParserErrorsTests.cs
index 0529a84..b4d35f6 100644
--- a/Test/SqlDom/ParserErrorsTests.cs
+++ b/Test/SqlDom/ParserErrorsTests.cs
@@ -7952,6 +7952,11 @@ public void AiAnalyzeSentimentNegativeTestsFabricDw()
ParserTestUtils.ErrorTestFabricDW(
"SELECT AI_ANALYZE_SENTIMENT('1', '2')",
new ParserErrorInfo(31, "SQL46010", ","));
+
+ // ON ERROR clause must follow the last argument
+ ParserTestUtils.ErrorTestFabricDW(
+ "SELECT AI_ANALYZE_SENTIMENT('1' NULL ON ERROR, '2')",
+ new ParserErrorInfo(45, "SQL46010", ","));
}
///
@@ -7972,10 +7977,19 @@ public void AiClassifyNegativeTestsFabricDw()
"SELECT AI_CLASSIFY('1')",
new ParserErrorInfo(22, "SQL46010", ")"));
+ // Up to 20 labels are allowed
+ ParserTestUtils.ErrorTestFabricDW(
+ $"SELECT AI_CLASSIFY({string.Join(", ", Enumerable.Repeat("'1'", 21))} NULL ON ERROR)");
+
// Too many arguments
ParserTestUtils.ErrorTestFabricDW(
- $"SELECT AI_CLASSIFY({string.Join(", ", Enumerable.Repeat("'1'", 23))})",
- new ParserErrorInfo(129, "SQL46010", "AI_CLASSIFY"));
+ $"SELECT AI_CLASSIFY({string.Join(", ", Enumerable.Repeat("'1'", 22))})",
+ new ParserErrorInfo(124, "SQL46010", "AI_CLASSIFY"));
+
+ // ON ERROR clause must follow the last argument
+ ParserTestUtils.ErrorTestFabricDW(
+ "SELECT AI_CLASSIFY('1', '2', '3' NULL ON ERROR, '4')",
+ new ParserErrorInfo(46, "SQL46010", ","));
}
///
@@ -7996,10 +8010,19 @@ public void AiExtractNegativeTestsFabricDw()
"SELECT AI_EXTRACT('1')",
new ParserErrorInfo(21, "SQL46010", ")"));
+ // Up to 20 labels are allowed
+ ParserTestUtils.ErrorTestFabricDW(
+ $"SELECT AI_EXTRACT({string.Join(", ", Enumerable.Repeat("'1'", 21))} NULL ON ERROR)");
+
// Too many arguments
ParserTestUtils.ErrorTestFabricDW(
- $"SELECT AI_EXTRACT({string.Join(", ", Enumerable.Repeat("'1'", 23))})",
- new ParserErrorInfo(128, "SQL46010", "AI_EXTRACT"));
+ $"SELECT AI_EXTRACT({string.Join(", ", Enumerable.Repeat("'1'", 22))})",
+ new ParserErrorInfo(123, "SQL46010", "AI_EXTRACT"));
+
+ // ON ERROR clause must follow the last argument
+ ParserTestUtils.ErrorTestFabricDW(
+ "SELECT AI_EXTRACT('1', '2', '3' NULL ON ERROR, '4')",
+ new ParserErrorInfo(45, "SQL46010", ","));
}
///
@@ -8019,6 +8042,11 @@ public void AiFixGrammarNegativeTestsFabricDw()
ParserTestUtils.ErrorTestFabricDW(
"SELECT AI_FIX_GRAMMAR('1', '2')",
new ParserErrorInfo(25, "SQL46010", ","));
+
+ // ON ERROR clause must follow the last argument
+ ParserTestUtils.ErrorTestFabricDW(
+ "SELECT AI_FIX_GRAMMAR('1' NULL ON ERROR, '2')",
+ new ParserErrorInfo(39, "SQL46010", ","));
}
///
@@ -8038,6 +8066,11 @@ public void AiGenerateResponseNegativeTestsFabricDw()
ParserTestUtils.ErrorTestFabricDW(
"SELECT AI_GENERATE_RESPONSE('1', '2', '3')",
new ParserErrorInfo(36, "SQL46010", ","));
+
+ // ON ERROR clause must follow the last argument
+ ParserTestUtils.ErrorTestFabricDW(
+ "SELECT AI_GENERATE_RESPONSE('1' NULL ON ERROR, '2')",
+ new ParserErrorInfo(45, "SQL46010", ","));
}
///
@@ -8057,6 +8090,11 @@ public void AiSummarizeNegativeTestsFabricDw()
ParserTestUtils.ErrorTestFabricDW(
"SELECT AI_SUMMARIZE('1', '2')",
new ParserErrorInfo(23, "SQL46010", ","));
+
+ // ON ERROR clause must follow the last argument
+ ParserTestUtils.ErrorTestFabricDW(
+ "SELECT AI_SUMMARIZE('1' NULL ON ERROR, '2')",
+ new ParserErrorInfo(37, "SQL46010", ","));
}
///
@@ -8081,6 +8119,34 @@ public void AiTranslateNegativeTestsFabricDw()
ParserTestUtils.ErrorTestFabricDW(
"SELECT AI_TRANSLATE('1', '2', '3')",
new ParserErrorInfo(28, "SQL46010", ","));
+
+ // DEFAULT requires a string literal
+ ParserTestUtils.ErrorTestFabricDW(
+ "SELECT AI_TRANSLATE('hello', 'es' DEFAULT ON ERROR)",
+ new ParserErrorInfo(42, "SQL46010", "ON"));
+
+ ParserTestUtils.ErrorTestFabricDW(
+ "SELECT AI_TRANSLATE('hello', 'es' DEFAULT @fallback ON ERROR)",
+ new ParserErrorInfo(42, "SQL46010", "@fallback"));
+
+ // The behavior must be followed by ON ERROR
+ ParserTestUtils.ErrorTestFabricDW(
+ "SELECT AI_TRANSLATE('hello', 'es' NULL)",
+ new ParserErrorInfo(38, "SQL46010", ")"));
+
+ ParserTestUtils.ErrorTestFabricDW(
+ "SELECT AI_TRANSLATE('hello', 'es' NULL ON NULL)",
+ new ParserErrorInfo(42, "SQL46010", "NULL"));
+
+ // Only ERROR, NULL, or DEFAULT are valid behaviors
+ ParserTestUtils.ErrorTestFabricDW(
+ "SELECT AI_TRANSLATE('hello', 'es' IGNORE ON ERROR)",
+ new ParserErrorInfo(34, "SQL46005", "ERROR", "IGNORE"));
+
+ // Only one ON ERROR clause is allowed
+ ParserTestUtils.ErrorTestFabricDW(
+ "SELECT AI_TRANSLATE('hello', 'es' NULL ON ERROR NULL ON ERROR)",
+ new ParserErrorInfo(48, "SQL46010", "NULL"));
}
///
diff --git a/Test/SqlDom/TestScripts/AiAnalyzeSentimentTestsFabricDW.sql b/Test/SqlDom/TestScripts/AiAnalyzeSentimentTestsFabricDW.sql
index 7c2656f..2b577a6 100644
--- a/Test/SqlDom/TestScripts/AiAnalyzeSentimentTestsFabricDW.sql
+++ b/Test/SqlDom/TestScripts/AiAnalyzeSentimentTestsFabricDW.sql
@@ -12,6 +12,12 @@ SELECT AI_ANALYZE_SENTIMENT(@s0 + 'a');
SELECT AI_ANALYZE_SENTIMENT('b' + 'a');
+SELECT AI_ANALYZE_SENTIMENT('text' ERROR ON ERROR);
+
+SELECT AI_ANALYZE_SENTIMENT('text' NULL ON ERROR);
+
+SELECT AI_ANALYZE_SENTIMENT('text' DEFAULT 'Sentiment unavailable' ON ERROR);
+
SELECT *
FROM AI_ANALYZE_SENTIMENT((SELECT t.text_col
FROM dbo.Texts AS t));
@@ -23,5 +29,5 @@ CREATE OR ALTER FUNCTION dbo.fx
RETURNS NVARCHAR (MAX)
AS
BEGIN
- RETURN (AI_ANALYZE_SENTIMENT(@s));
+ RETURN (AI_ANALYZE_SENTIMENT(@s DEFAULT N'Sentiment unavailable' ON ERROR));
END
diff --git a/Test/SqlDom/TestScripts/AiClassifyTestsFabricDW.sql b/Test/SqlDom/TestScripts/AiClassifyTestsFabricDW.sql
index 2a623c0..8322a2d 100644
--- a/Test/SqlDom/TestScripts/AiClassifyTestsFabricDW.sql
+++ b/Test/SqlDom/TestScripts/AiClassifyTestsFabricDW.sql
@@ -11,6 +11,11 @@ SELECT AI_CLASSIFY(@s, 'topic', 'sentiment', 'intent');
SELECT AI_CLASSIFY('b' + 'a', 'topic', 'sentiment', 'intent');
SELECT AI_CLASSIFY(@s + 'a', 'topic', 'sentiment', 'intent');
SELECT * FROM AI_CLASSIFY((SELECT t.text_col FROM dbo.Texts AS t), 'topic', 'sentiment', 'intent');
+
+-- ON ERROR behaviors
+SELECT AI_CLASSIFY('text', 'spam' ERROR ON ERROR);
+SELECT AI_CLASSIFY('text', 'spam', 'ham' NULL ON ERROR);
+SELECT AI_CLASSIFY(@s, 'topic', 'sentiment', 'intent' DEFAULT 'Classification unavailable' ON ERROR);
GO
-- RETURN context
@@ -18,6 +23,6 @@ CREATE OR ALTER FUNCTION dbo.fx(@s NVARCHAR(MAX))
RETURNS NVARCHAR(MAX)
AS
BEGIN
- RETURN (AI_CLASSIFY(@s, 'labelA', 'labelB'));
+ RETURN (AI_CLASSIFY(@s, 'labelA', 'labelB' DEFAULT N'Classification unavailable' ON ERROR));
END;
GO
diff --git a/Test/SqlDom/TestScripts/AiExtractTestsFabricDW.sql b/Test/SqlDom/TestScripts/AiExtractTestsFabricDW.sql
index cf2c82e..fa3c247 100644
--- a/Test/SqlDom/TestScripts/AiExtractTestsFabricDW.sql
+++ b/Test/SqlDom/TestScripts/AiExtractTestsFabricDW.sql
@@ -11,6 +11,11 @@ SELECT AI_EXTRACT(@s, 'topic', 'sentiment', 'intent');
SELECT AI_EXTRACT('b' + 'a', 'topic', 'sentiment', 'intent');
SELECT AI_EXTRACT(@s + 'a', 'topic', 'sentiment', 'intent');
SELECT * FROM AI_EXTRACT((SELECT t.text_col FROM dbo.Texts AS t), 'topic', 'sentiment', 'intent');
+
+-- ON ERROR behaviors
+SELECT AI_EXTRACT('text', 'spam' ERROR ON ERROR);
+SELECT AI_EXTRACT('text', 'spam', 'ham' NULL ON ERROR);
+SELECT AI_EXTRACT(@s, 'topic', 'sentiment', 'intent' DEFAULT 'Extraction unavailable' ON ERROR);
GO
-- RETURN context
@@ -18,6 +23,6 @@ CREATE OR ALTER FUNCTION dbo.fx(@s NVARCHAR(MAX))
RETURNS NVARCHAR(MAX)
AS
BEGIN
- RETURN (AI_EXTRACT(@s, 'labelA', 'labelB'));
+ RETURN (AI_EXTRACT(@s, 'labelA', 'labelB' DEFAULT N'Extraction unavailable' ON ERROR));
END;
GO
diff --git a/Test/SqlDom/TestScripts/AiFixGrammarTestsFabricDW.sql b/Test/SqlDom/TestScripts/AiFixGrammarTestsFabricDW.sql
index 7e6225a..8bfef16 100644
--- a/Test/SqlDom/TestScripts/AiFixGrammarTestsFabricDW.sql
+++ b/Test/SqlDom/TestScripts/AiFixGrammarTestsFabricDW.sql
@@ -12,6 +12,12 @@ SELECT AI_FIX_GRAMMAR(@s0 + 'a');
SELECT AI_FIX_GRAMMAR('b' + 'a');
+SELECT AI_FIX_GRAMMAR('text' ERROR ON ERROR);
+
+SELECT AI_FIX_GRAMMAR('text' NULL ON ERROR);
+
+SELECT AI_FIX_GRAMMAR('text' DEFAULT 'Correction unavailable' ON ERROR);
+
SELECT *
FROM AI_FIX_GRAMMAR((SELECT t.text_col
FROM dbo.Texts AS t));
@@ -23,5 +29,5 @@ CREATE OR ALTER FUNCTION dbo.fx
RETURNS NVARCHAR (MAX)
AS
BEGIN
- RETURN (AI_FIX_GRAMMAR(@s));
+ RETURN (AI_FIX_GRAMMAR(@s DEFAULT N'Correction unavailable' ON ERROR));
END
diff --git a/Test/SqlDom/TestScripts/AiGenerateResponseTestsFabricDW.sql b/Test/SqlDom/TestScripts/AiGenerateResponseTestsFabricDW.sql
index 55cec5a..d25347f 100644
--- a/Test/SqlDom/TestScripts/AiGenerateResponseTestsFabricDW.sql
+++ b/Test/SqlDom/TestScripts/AiGenerateResponseTestsFabricDW.sql
@@ -6,6 +6,9 @@ DECLARE @p1 NVARCHAR(MAX) = N'Prompt ';
DECLARE @p2 NVARCHAR(MAX) = N'Continuation';
SELECT AI_GENERATE_RESPONSE(@p1 + 'part1', @p2);
SELECT AI_GENERATE_RESPONSE('b' + 'a');
+SELECT AI_GENERATE_RESPONSE('Hello' ERROR ON ERROR);
+SELECT AI_GENERATE_RESPONSE('Question:', ' What is the time?' NULL ON ERROR);
+SELECT AI_GENERATE_RESPONSE(@p1, @p2 DEFAULT 'Response unavailable' ON ERROR);
SELECT * FROM AI_GENERATE_RESPONSE((SELECT t.text_col
FROM dbo.Texts AS t));
GO
@@ -14,6 +17,6 @@ CREATE OR ALTER FUNCTION dbo.fx(@p NVARCHAR(MAX))
RETURNS NVARCHAR(MAX)
AS
BEGIN
- RETURN (AI_GENERATE_RESPONSE(@p));
+ RETURN (AI_GENERATE_RESPONSE(@p DEFAULT N'Response unavailable' ON ERROR));
END;
GO
diff --git a/Test/SqlDom/TestScripts/AiSummarizeTestsFabricDW.sql b/Test/SqlDom/TestScripts/AiSummarizeTestsFabricDW.sql
index fbb972d..8991134 100644
--- a/Test/SqlDom/TestScripts/AiSummarizeTestsFabricDW.sql
+++ b/Test/SqlDom/TestScripts/AiSummarizeTestsFabricDW.sql
@@ -12,6 +12,12 @@ SELECT AI_SUMMARIZE(@s0 + 'a');
SELECT AI_SUMMARIZE('b' + 'a');
+SELECT AI_SUMMARIZE('text' ERROR ON ERROR);
+
+SELECT AI_SUMMARIZE('text' NULL ON ERROR);
+
+SELECT AI_SUMMARIZE('text' DEFAULT 'Summary unavailable' ON ERROR);
+
SELECT *
FROM AI_SUMMARIZE((SELECT t.text_col
FROM dbo.Texts AS t));
@@ -23,5 +29,5 @@ CREATE OR ALTER FUNCTION dbo.fx
RETURNS NVARCHAR (MAX)
AS
BEGIN
- RETURN (AI_SUMMARIZE(@s));
+ RETURN (AI_SUMMARIZE(@s DEFAULT N'Summary unavailable' ON ERROR));
END
diff --git a/Test/SqlDom/TestScripts/AiTranslateTestsFabricDW.sql b/Test/SqlDom/TestScripts/AiTranslateTestsFabricDW.sql
index bcda9fb..4a9893c 100644
--- a/Test/SqlDom/TestScripts/AiTranslateTestsFabricDW.sql
+++ b/Test/SqlDom/TestScripts/AiTranslateTestsFabricDW.sql
@@ -8,6 +8,10 @@ DECLARE @s AS NVARCHAR (MAX) = N'Hello world';
DECLARE @lang AS NVARCHAR (5) = N'en';
SELECT AI_TRANSLATE (@s, @lang);
SELECT AI_TRANSLATE (@s + 'a', @lang + 'a');
+SELECT AI_TRANSLATE ('Hello world', 'es' ERROR ON ERROR);
+SELECT AI_TRANSLATE ('Hello world', 'es' NULL ON ERROR);
+SELECT AI_TRANSLATE ('Hello world', 'es' DEFAULT 'Translation unavailable' ON ERROR);
+SELECT AI_TRANSLATE (@s, @lang DEFAULT N'Translation unavailable' ON ERROR);
SELECT * FROM AI_TRANSLATE((SELECT t.text_col
FROM dbo.Texts AS t), 'e' + 's');
GO
@@ -17,7 +21,7 @@ CREATE OR ALTER FUNCTION dbo.fx
RETURNS NVARCHAR (MAX)
AS
BEGIN
- RETURN (AI_TRANSLATE (@s, 'fr'));
+ RETURN (AI_TRANSLATE (@s, 'fr' DEFAULT N'Translation unavailable' ON ERROR));
END
GO
From 92d5e11d474a5c685cf7d3d9c444f1349cc3626c Mon Sep 17 00:00:00 2001
From: Allan Cascante <120929896+acascante_microsoft@users.noreply.github.com>
Date: Mon, 12 Oct 2026 16:00:18 -0600
Subject: [PATCH 07/13] Fix quoted named WINDOW references
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
SqlScriptDom/Parser/TSql/TSql160.g | 4 +-
SqlScriptDom/Parser/TSql/TSql170.g | 4 +-
SqlScriptDom/Parser/TSql/TSql180.g | 4 +-
SqlScriptDom/Parser/TSql/TSqlFabricDW.g | 4 +-
.../SqlScriptGeneratorVisitor.OverClause.cs | 4 +-
.../WindowClauseQuotedNameTests160.sql | 46 ++++++++++++++++++
.../WindowClauseQuotedNameTests160.sql | 46 ++++++++++++++++++
.../WindowClauseQuotedNameTests160.sql | 46 ++++++++++++++++++
.../WindowClauseQuotedNameTests160.sql | 46 ++++++++++++++++++
Test/SqlDom/Only160SyntaxTests.cs | 1 +
Test/SqlDom/Only170SyntaxTests.cs | 1 +
Test/SqlDom/Only180SyntaxTests.cs | 4 +-
Test/SqlDom/OnlyFabricDWSyntaxTests.cs | 1 +
.../IdentifierFormattingTests.cs | 48 ++++++++++++-------
.../WindowClauseQuotedNameTests160.sql | 46 ++++++++++++++++++
15 files changed, 277 insertions(+), 28 deletions(-)
create mode 100644 Test/SqlDom/Baselines160/WindowClauseQuotedNameTests160.sql
create mode 100644 Test/SqlDom/Baselines170/WindowClauseQuotedNameTests160.sql
create mode 100644 Test/SqlDom/Baselines180/WindowClauseQuotedNameTests160.sql
create mode 100644 Test/SqlDom/BaselinesFabricDW/WindowClauseQuotedNameTests160.sql
create mode 100644 Test/SqlDom/TestScripts/WindowClauseQuotedNameTests160.sql
diff --git a/SqlScriptDom/Parser/TSql/TSql160.g b/SqlScriptDom/Parser/TSql/TSql160.g
index 32b8d2c..b3c9079 100644
--- a/SqlScriptDom/Parser/TSql/TSql160.g
+++ b/SqlScriptDom/Parser/TSql/TSql160.g
@@ -32292,7 +32292,7 @@ regularBuiltInFunctionCall [FunctionCall vParent]
UpdateTokenInfo(vParent,tRParen);
}
(
- {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier)) ||
+ {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier || LA(2)==QuotedIdentifier)) ||
(NextTokenMatches(CodeGenerationSupporter.Within) && LA(2)==Group && LA(3)==LeftParenthesis) }?
withinGroupOrOverClause [vParent]
)?
@@ -32323,7 +32323,7 @@ ignoreRespectNullsRegularBuiltInFunctionCall [FunctionCall vParent]
ignoreRespectNulls [vParent]
)?
(
- {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier)) ||
+ {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier || LA(2)==QuotedIdentifier)) ||
(NextTokenMatches(CodeGenerationSupporter.Within) && LA(2)==Group && LA(3)==LeftParenthesis) }?
withinGroupOrOverClause [vParent]
)?
diff --git a/SqlScriptDom/Parser/TSql/TSql170.g b/SqlScriptDom/Parser/TSql/TSql170.g
index 48cf386..eb00d94 100644
--- a/SqlScriptDom/Parser/TSql/TSql170.g
+++ b/SqlScriptDom/Parser/TSql/TSql170.g
@@ -33460,7 +33460,7 @@ regularBuiltInFunctionCall [FunctionCall vParent]
UpdateTokenInfo(vParent,tRParen);
}
(
- {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier)) ||
+ {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier || LA(2)==QuotedIdentifier)) ||
(NextTokenMatches(CodeGenerationSupporter.Within) && LA(2)==Group && LA(3)==LeftParenthesis) }?
withinGroupOrOverClause [vParent]
)?
@@ -33491,7 +33491,7 @@ ignoreRespectNullsRegularBuiltInFunctionCall [FunctionCall vParent]
ignoreRespectNulls [vParent]
)?
(
- {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier)) ||
+ {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier || LA(2)==QuotedIdentifier)) ||
(NextTokenMatches(CodeGenerationSupporter.Within) && LA(2)==Group && LA(3)==LeftParenthesis) }?
withinGroupOrOverClause [vParent]
)?
diff --git a/SqlScriptDom/Parser/TSql/TSql180.g b/SqlScriptDom/Parser/TSql/TSql180.g
index df96536..147abda 100644
--- a/SqlScriptDom/Parser/TSql/TSql180.g
+++ b/SqlScriptDom/Parser/TSql/TSql180.g
@@ -33698,7 +33698,7 @@ regularBuiltInFunctionCall [FunctionCall vParent]
UpdateTokenInfo(vParent,tRParen);
}
(
- {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier)) ||
+ {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier || LA(2)==QuotedIdentifier)) ||
(NextTokenMatches(CodeGenerationSupporter.Within) && LA(2)==Group && LA(3)==LeftParenthesis) }?
withinGroupOrOverClause [vParent]
)?
@@ -33729,7 +33729,7 @@ ignoreRespectNullsRegularBuiltInFunctionCall [FunctionCall vParent]
ignoreRespectNulls [vParent]
)?
(
- {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier)) ||
+ {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier || LA(2)==QuotedIdentifier)) ||
(NextTokenMatches(CodeGenerationSupporter.Within) && LA(2)==Group && LA(3)==LeftParenthesis) }?
withinGroupOrOverClause [vParent]
)?
diff --git a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
index 4347f5e..98b7989 100644
--- a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
+++ b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
@@ -32707,7 +32707,7 @@ regularBuiltInFunctionCall [FunctionCall vParent]
UpdateTokenInfo(vParent,tRParen);
}
(
- {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier)) ||
+ {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier || LA(2)==QuotedIdentifier)) ||
(NextTokenMatches(CodeGenerationSupporter.Within) && LA(2)==Group && LA(3)==LeftParenthesis) }?
withinGroupOrOverClause [vParent]
)?
@@ -32738,7 +32738,7 @@ ignoreRespectNullsRegularBuiltInFunctionCall [FunctionCall vParent]
ignoreRespectNulls [vParent]
)?
(
- {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier)) ||
+ {(LA(1)==Over && (LA(2)==LeftParenthesis || LA(2)==Identifier || LA(2)==QuotedIdentifier)) ||
(NextTokenMatches(CodeGenerationSupporter.Within) && LA(2)==Group && LA(3)==LeftParenthesis) }?
withinGroupOrOverClause [vParent]
)?
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OverClause.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OverClause.cs
index a9d8a42..a52d748 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OverClause.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OverClause.cs
@@ -18,9 +18,7 @@ public override void ExplicitVisit(OverClause node)
if (windowNameExists && !windowSpecExist)
{
- // An unparenthesized "OVER window_name" reference must be a plain identifier; it
- // cannot be bracketed or recased. (Inside "OVER (window_name ...)" it may be.)
- GenerateWithoutIdentifierFormatting(() => GenerateSpaceAndFragmentIfNotNull(node.WindowName));
+ GenerateSpaceAndFragmentIfNotNull(node.WindowName);
}
else
{
diff --git a/Test/SqlDom/Baselines160/WindowClauseQuotedNameTests160.sql b/Test/SqlDom/Baselines160/WindowClauseQuotedNameTests160.sql
new file mode 100644
index 0000000..8a5ee8a
--- /dev/null
+++ b/Test/SqlDom/Baselines160/WindowClauseQuotedNameTests160.sql
@@ -0,0 +1,46 @@
+CREATE PROCEDURE [dbo].[ExampleProc]
+AS
+BEGIN
+ SELECT ROW_NUMBER() OVER [win] AS [RowNum],
+ SUM([Amount]) OVER [win] AS [RunningTotal]
+ FROM [dbo].[Transactions]
+ WINDOW [win] AS (PARTITION BY [AccountID] ORDER BY [TransactionDate]);
+END
+
+
+GO
+SELECT COUNT(*) OVER [win]
+FROM t1
+WINDOW [win] AS (PARTITION BY c1);
+
+
+GO
+SELECT LAST_VALUE(c1) IGNORE NULLS OVER [win]
+FROM t1
+WINDOW [win] AS (ORDER BY c2);
+
+
+GO
+SELECT LAG(c1) OVER [my win]
+FROM t1
+WINDOW [my win] AS (ORDER BY c2);
+
+
+GO
+SELECT SUM(c1) OVER [select]
+FROM t1
+WINDOW [select] AS (PARTITION BY c2);
+
+
+GO
+SELECT SUM(c1) OVER "win"
+FROM t1
+WINDOW "win" AS (PARTITION BY c2);
+
+
+GO
+CREATE VIEW v1
+AS
+SELECT AVG(c1) OVER [win] AS a
+FROM t1
+WINDOW [win] AS ([base] ORDER BY c2), [base] AS (PARTITION BY c3);
diff --git a/Test/SqlDom/Baselines170/WindowClauseQuotedNameTests160.sql b/Test/SqlDom/Baselines170/WindowClauseQuotedNameTests160.sql
new file mode 100644
index 0000000..8a5ee8a
--- /dev/null
+++ b/Test/SqlDom/Baselines170/WindowClauseQuotedNameTests160.sql
@@ -0,0 +1,46 @@
+CREATE PROCEDURE [dbo].[ExampleProc]
+AS
+BEGIN
+ SELECT ROW_NUMBER() OVER [win] AS [RowNum],
+ SUM([Amount]) OVER [win] AS [RunningTotal]
+ FROM [dbo].[Transactions]
+ WINDOW [win] AS (PARTITION BY [AccountID] ORDER BY [TransactionDate]);
+END
+
+
+GO
+SELECT COUNT(*) OVER [win]
+FROM t1
+WINDOW [win] AS (PARTITION BY c1);
+
+
+GO
+SELECT LAST_VALUE(c1) IGNORE NULLS OVER [win]
+FROM t1
+WINDOW [win] AS (ORDER BY c2);
+
+
+GO
+SELECT LAG(c1) OVER [my win]
+FROM t1
+WINDOW [my win] AS (ORDER BY c2);
+
+
+GO
+SELECT SUM(c1) OVER [select]
+FROM t1
+WINDOW [select] AS (PARTITION BY c2);
+
+
+GO
+SELECT SUM(c1) OVER "win"
+FROM t1
+WINDOW "win" AS (PARTITION BY c2);
+
+
+GO
+CREATE VIEW v1
+AS
+SELECT AVG(c1) OVER [win] AS a
+FROM t1
+WINDOW [win] AS ([base] ORDER BY c2), [base] AS (PARTITION BY c3);
diff --git a/Test/SqlDom/Baselines180/WindowClauseQuotedNameTests160.sql b/Test/SqlDom/Baselines180/WindowClauseQuotedNameTests160.sql
new file mode 100644
index 0000000..8a5ee8a
--- /dev/null
+++ b/Test/SqlDom/Baselines180/WindowClauseQuotedNameTests160.sql
@@ -0,0 +1,46 @@
+CREATE PROCEDURE [dbo].[ExampleProc]
+AS
+BEGIN
+ SELECT ROW_NUMBER() OVER [win] AS [RowNum],
+ SUM([Amount]) OVER [win] AS [RunningTotal]
+ FROM [dbo].[Transactions]
+ WINDOW [win] AS (PARTITION BY [AccountID] ORDER BY [TransactionDate]);
+END
+
+
+GO
+SELECT COUNT(*) OVER [win]
+FROM t1
+WINDOW [win] AS (PARTITION BY c1);
+
+
+GO
+SELECT LAST_VALUE(c1) IGNORE NULLS OVER [win]
+FROM t1
+WINDOW [win] AS (ORDER BY c2);
+
+
+GO
+SELECT LAG(c1) OVER [my win]
+FROM t1
+WINDOW [my win] AS (ORDER BY c2);
+
+
+GO
+SELECT SUM(c1) OVER [select]
+FROM t1
+WINDOW [select] AS (PARTITION BY c2);
+
+
+GO
+SELECT SUM(c1) OVER "win"
+FROM t1
+WINDOW "win" AS (PARTITION BY c2);
+
+
+GO
+CREATE VIEW v1
+AS
+SELECT AVG(c1) OVER [win] AS a
+FROM t1
+WINDOW [win] AS ([base] ORDER BY c2), [base] AS (PARTITION BY c3);
diff --git a/Test/SqlDom/BaselinesFabricDW/WindowClauseQuotedNameTests160.sql b/Test/SqlDom/BaselinesFabricDW/WindowClauseQuotedNameTests160.sql
new file mode 100644
index 0000000..8a5ee8a
--- /dev/null
+++ b/Test/SqlDom/BaselinesFabricDW/WindowClauseQuotedNameTests160.sql
@@ -0,0 +1,46 @@
+CREATE PROCEDURE [dbo].[ExampleProc]
+AS
+BEGIN
+ SELECT ROW_NUMBER() OVER [win] AS [RowNum],
+ SUM([Amount]) OVER [win] AS [RunningTotal]
+ FROM [dbo].[Transactions]
+ WINDOW [win] AS (PARTITION BY [AccountID] ORDER BY [TransactionDate]);
+END
+
+
+GO
+SELECT COUNT(*) OVER [win]
+FROM t1
+WINDOW [win] AS (PARTITION BY c1);
+
+
+GO
+SELECT LAST_VALUE(c1) IGNORE NULLS OVER [win]
+FROM t1
+WINDOW [win] AS (ORDER BY c2);
+
+
+GO
+SELECT LAG(c1) OVER [my win]
+FROM t1
+WINDOW [my win] AS (ORDER BY c2);
+
+
+GO
+SELECT SUM(c1) OVER [select]
+FROM t1
+WINDOW [select] AS (PARTITION BY c2);
+
+
+GO
+SELECT SUM(c1) OVER "win"
+FROM t1
+WINDOW "win" AS (PARTITION BY c2);
+
+
+GO
+CREATE VIEW v1
+AS
+SELECT AVG(c1) OVER [win] AS a
+FROM t1
+WINDOW [win] AS ([base] ORDER BY c2), [base] AS (PARTITION BY c3);
diff --git a/Test/SqlDom/Only160SyntaxTests.cs b/Test/SqlDom/Only160SyntaxTests.cs
index 9feb868..2fa962d 100644
--- a/Test/SqlDom/Only160SyntaxTests.cs
+++ b/Test/SqlDom/Only160SyntaxTests.cs
@@ -21,6 +21,7 @@ public partial class SqlDomTests
new ParserTest160("CreateUserFromExternalProvider160.sql", nErrors80: 2, nErrors90: 1, nErrors100: 1, nErrors110: 1, nErrors120: 1, nErrors130: 1, nErrors140: 1, nErrors150: 1),
new ParserTest160("CreateExternalTableStatementTests160.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 2, nErrors140: 2, nErrors150: 2),
new ParserTest160("WindowClauseTests160.sql", nErrors80: 16, nErrors90: 15, nErrors100: 15, nErrors110: 15, nErrors120: 15, nErrors130: 15, nErrors140: 15, nErrors150: 15),
+ new ParserTest160("WindowClauseQuotedNameTests160.sql", nErrors80: 7, nErrors90: 7, nErrors100: 7, nErrors110: 7, nErrors120: 7, nErrors130: 7, nErrors140: 7, nErrors150: 7),
new ParserTest160("CreateExternalDataSourceStatementTests160.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0),
new ParserTest160("CreateDatabaseTests160.sql", nErrors80: 4, nErrors90: 4, nErrors100: 4, nErrors110: 4, nErrors120: 4, nErrors130: 4, nErrors140: 4, nErrors150: 4),
new ParserTest160("CreateLedgerTableTests160.sql", nErrors80: 14, nErrors90: 14, nErrors100: 14, nErrors110: 14, nErrors120: 14, nErrors130: 14, nErrors140: 14, nErrors150: 14),
diff --git a/Test/SqlDom/Only170SyntaxTests.cs b/Test/SqlDom/Only170SyntaxTests.cs
index 6c6aa48..147d80f 100644
--- a/Test/SqlDom/Only170SyntaxTests.cs
+++ b/Test/SqlDom/Only170SyntaxTests.cs
@@ -25,6 +25,7 @@ public partial class SqlDomTests
new ParserTest170("JsonObjectAggOverClause170.sql", nErrors80: 4, nErrors90: 4, nErrors100: 4, nErrors110: 4, nErrors120: 4, nErrors130: 4, nErrors140: 4, nErrors150: 4, nErrors160: 4),
new ParserTest170("ComplexJsonObjectFunctionTests170.sql"),
new ParserTest170("TrimFromReturnTests160.sql"),
+ new ParserTest170("WindowClauseQuotedNameTests160.sql"),
new ParserTest170("AiGenerateEmbeddingsTests170.sql", nErrors80: 14, nErrors90: 11, nErrors100: 11, nErrors110: 11, nErrors120: 11, nErrors130: 11, nErrors140: 11, nErrors150: 11, nErrors160: 11),
new ParserTest170("CreateExternalModelStatementTests170.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 4, nErrors140: 4, nErrors150: 4, nErrors160: 4),
new ParserTest170("AlterExternalModelStatementTests170.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 5, nErrors140: 5, nErrors150: 5, nErrors160: 5),
diff --git a/Test/SqlDom/Only180SyntaxTests.cs b/Test/SqlDom/Only180SyntaxTests.cs
index e26be85..d8a2644 100644
--- a/Test/SqlDom/Only180SyntaxTests.cs
+++ b/Test/SqlDom/Only180SyntaxTests.cs
@@ -14,8 +14,10 @@ public partial class SqlDomTests
{
new ParserTest180("AutomaticIndexCompactionTests180.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 2, nErrors140: 2, nErrors150: 2, nErrors160: 2, nErrors170: 2),
new ParserTest180("TrimFromReturnTests160.sql"),
- new ParserTest180("PersistSamplePercentStatisticsTests130.sql"),
+ new ParserTest180("WindowClauseQuotedNameTests160.sql"),
new ParserTest180("CreateSemanticIndexTests180.sql"),
+ new ParserTest180("AlterDatabaseScopedConfigTxnPriorityTests180.sql", nErrors80: 7, nErrors90: 7, nErrors100: 7, nErrors110: 7, nErrors120: 7, nErrors130: 1, nErrors140: 1, nErrors150: 1, nErrors160: 1, nErrors170: 1),
+ new ParserTest180("PersistSamplePercentStatisticsTests130.sql"),
new ParserTest180("CreateDatabaseCollateBeforeEditionTests120.sql", nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0)
};
diff --git a/Test/SqlDom/OnlyFabricDWSyntaxTests.cs b/Test/SqlDom/OnlyFabricDWSyntaxTests.cs
index 26cf684..5b4d722 100644
--- a/Test/SqlDom/OnlyFabricDWSyntaxTests.cs
+++ b/Test/SqlDom/OnlyFabricDWSyntaxTests.cs
@@ -33,6 +33,7 @@ public partial class SqlDomTests
new ParserTestFabricDW("ModernGroupByAllTestsFabricDW.sql", nErrors80: 9, nErrors90: 9, nErrors100: 9, nErrors110: 9, nErrors120: 9, nErrors130: 9, nErrors140: 9, nErrors150: 9, nErrors160: 9, nErrors170: 9, nErrors180: 9),
new ParserTestFabricDW("OrderByAllTestsFabricDW.sql", nErrors80: 13, nErrors90: 13, nErrors100: 13, nErrors110: 13, nErrors120: 13, nErrors130: 13, nErrors140: 13, nErrors150: 13, nErrors160: 13, nErrors170: 13, nErrors180: 13),
new ParserTestFabricDW("QueryHintsTestsFabricDW.sql", nErrors80: 6, nErrors90: 6, nErrors100: 6, nErrors110: 6, nErrors120: 6, nErrors130: 6, nErrors140: 6, nErrors150: 6, nErrors160: 6, nErrors170: 6, nErrors180: 6),
+ new ParserTestFabricDW("WindowClauseQuotedNameTests160.sql", nErrors80: 7, nErrors90: 7, nErrors100: 7, nErrors110: 7, nErrors120: 7, nErrors130: 7, nErrors140: 7, nErrors150: 7, nErrors160: 0, nErrors170: 0, nErrors180: 0),
};
diff --git a/Test/SqlDom/ScriptGenerator/IdentifierFormattingTests.cs b/Test/SqlDom/ScriptGenerator/IdentifierFormattingTests.cs
index 61865b6..95edba3 100644
--- a/Test/SqlDom/ScriptGenerator/IdentifierFormattingTests.cs
+++ b/Test/SqlDom/ScriptGenerator/IdentifierFormattingTests.cs
@@ -359,6 +359,38 @@ public void TestSuppressedFunctionNamePreservesOriginalBracketingAndCasing()
AssertGenerated(input, options, expected);
}
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIdentifierBracketingIncludeBracketsWindowOverReference()
+ {
+ // An unparenthesized "OVER window_name" reference is an ordinary identifier ("OVER [Win1]"
+ // is valid T-SQL), so it is bracketed like the WINDOW clause name it refers to.
+ const string input = "SELECT Sum(c1) OVER Win1 FROM t1 WINDOW Win1 AS (PARTITION BY c1)";
+ var options = MakeOptions(IdentifierCasing.Preserve, IdentifierBracketing.IncludeBrackets);
+ const string expected =
+@"SELECT Sum([c1]) OVER [Win1] FROM [t1]
+WINDOW [Win1] AS (PARTITION BY [c1]);";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestIdentifierBracketingExcludeBracketsWindowOverReference()
+ {
+ // The "OVER window_name" reference and its WINDOW clause definition must stay in step:
+ // both lose their brackets and both are recased, so the reference still names the window.
+ const string input = "SELECT Sum(c1) OVER [Win1] FROM t1 WINDOW [Win1] AS (PARTITION BY c1)";
+ var options = MakeOptions(IdentifierCasing.Uppercase, IdentifierBracketing.ExcludeBrackets);
+ const string expected =
+@"SELECT Sum(C1) OVER WIN1 FROM T1
+WINDOW WIN1 AS (PARTITION BY C1);";
+
+ AssertGenerated(input, options, expected);
+ }
+
// -----------------------------------------------------------------------------------------
// Interaction: casing is applied after bracketing
// -----------------------------------------------------------------------------------------
@@ -814,22 +846,6 @@ public void TestGapFederationNameNotBracketed()
AssertGenerated(input, options, expected);
}
- // GAP-WINDOW: an unparenthesized "OVER window_name" reference must be a plain identifier
- // ("OVER [Win1]" is invalid), while the WINDOW clause definition name may be bracketed.
- [TestMethod]
- [Priority(0)]
- [SqlStudioTestCategory(Category.UnitTest)]
- public void TestGapWindowOverReferenceNotBracketed()
- {
- const string input = "SELECT Sum(c1) OVER Win1 FROM t1 WINDOW Win1 AS (PARTITION BY c1)";
- var options = MakeOptions(IdentifierCasing.Preserve, IdentifierBracketing.IncludeBrackets);
- const string expected =
-@"SELECT Sum([c1]) OVER Win1 FROM [t1]
-WINDOW [Win1] AS (PARTITION BY [c1]);";
-
- AssertGenerated(input, options, expected);
- }
-
// GAP-TIMESTAMP: the INSERT BULK timestamp/rowversion shorthand column has no data type and its
// name is the TIMESTAMP keyword ("([c2] CHAR, [TIMESTAMP])" is invalid); regular columns are
// still bracketed.
diff --git a/Test/SqlDom/TestScripts/WindowClauseQuotedNameTests160.sql b/Test/SqlDom/TestScripts/WindowClauseQuotedNameTests160.sql
new file mode 100644
index 0000000..8a5ee8a
--- /dev/null
+++ b/Test/SqlDom/TestScripts/WindowClauseQuotedNameTests160.sql
@@ -0,0 +1,46 @@
+CREATE PROCEDURE [dbo].[ExampleProc]
+AS
+BEGIN
+ SELECT ROW_NUMBER() OVER [win] AS [RowNum],
+ SUM([Amount]) OVER [win] AS [RunningTotal]
+ FROM [dbo].[Transactions]
+ WINDOW [win] AS (PARTITION BY [AccountID] ORDER BY [TransactionDate]);
+END
+
+
+GO
+SELECT COUNT(*) OVER [win]
+FROM t1
+WINDOW [win] AS (PARTITION BY c1);
+
+
+GO
+SELECT LAST_VALUE(c1) IGNORE NULLS OVER [win]
+FROM t1
+WINDOW [win] AS (ORDER BY c2);
+
+
+GO
+SELECT LAG(c1) OVER [my win]
+FROM t1
+WINDOW [my win] AS (ORDER BY c2);
+
+
+GO
+SELECT SUM(c1) OVER [select]
+FROM t1
+WINDOW [select] AS (PARTITION BY c2);
+
+
+GO
+SELECT SUM(c1) OVER "win"
+FROM t1
+WINDOW "win" AS (PARTITION BY c2);
+
+
+GO
+CREATE VIEW v1
+AS
+SELECT AVG(c1) OVER [win] AS a
+FROM t1
+WINDOW [win] AS ([base] ORDER BY c2), [base] AS (PARTITION BY c3);
From 5a11b1b14889819f55b9ca52afbf09b8ee90595e Mon Sep 17 00:00:00 2001
From: Allan Cascante <120929896+acascante_microsoft@users.noreply.github.com>
Date: Mon, 12 Oct 2026 16:00:45 -0600
Subject: [PATCH 08/13] Add ROLLBACK transaction priority mode parsing
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
SqlScriptDom/Parser/TSql/TSql180.g | 5 +++++
...atabaseScopedConfigTxnPriorityTests180.sql | 13 ++++++++++++
...atabaseScopedConfigTxnPriorityTests180.sql | 20 +++++++++++++++++++
3 files changed, 38 insertions(+)
create mode 100644 Test/SqlDom/Baselines180/AlterDatabaseScopedConfigTxnPriorityTests180.sql
create mode 100644 Test/SqlDom/TestScripts/AlterDatabaseScopedConfigTxnPriorityTests180.sql
diff --git a/SqlScriptDom/Parser/TSql/TSql180.g b/SqlScriptDom/Parser/TSql/TSql180.g
index 147abda..8ca5cc2 100644
--- a/SqlScriptDom/Parser/TSql/TSql180.g
+++ b/SqlScriptDom/Parser/TSql/TSql180.g
@@ -2873,6 +2873,11 @@ alterDatabaseScopedGenericOption[bool forSecondary] returns [GenericConfiguratio
}
vResult.GenericOptionState = vValue;
}
+ |
+ tRollback:Rollback
+ {
+ vResult.GenericOptionState = CreateIdentifierOrScalarExpressionFromIdentifier(CreateIdentifierFromToken(tRollback));
+ }
|
vValue = stringOrSignedIntegerOrIdentifier
{
diff --git a/Test/SqlDom/Baselines180/AlterDatabaseScopedConfigTxnPriorityTests180.sql b/Test/SqlDom/Baselines180/AlterDatabaseScopedConfigTxnPriorityTests180.sql
new file mode 100644
index 0000000..046a0c4
--- /dev/null
+++ b/Test/SqlDom/Baselines180/AlterDatabaseScopedConfigTxnPriorityTests180.sql
@@ -0,0 +1,13 @@
+ALTER DATABASE SCOPED CONFIGURATION SET TXN_PRIORITY_MODE = OFF;
+
+ALTER DATABASE SCOPED CONFIGURATION SET TXN_PRIORITY_MODE = ROLLBACK;
+
+ALTER DATABASE SCOPED CONFIGURATION SET TXN_PRIORITY_MODE = BLOCK;
+
+ALTER DATABASE SCOPED CONFIGURATION SET TXN_PRIORITY_MIN_WAIT_MILLISECONDS = 5000;
+
+ALTER DATABASE SCOPED CONFIGURATION SET TXN_PRIORITY_MIN_WAIT_MILLISECONDS = 0;
+
+ALTER DATABASE SCOPED CONFIGURATION FOR SECONDARY SET TXN_PRIORITY_MODE = PRIMARY;
+
+ALTER DATABASE SCOPED CONFIGURATION FOR SECONDARY SET TXN_PRIORITY_MIN_WAIT_MILLISECONDS = 1000;
diff --git a/Test/SqlDom/TestScripts/AlterDatabaseScopedConfigTxnPriorityTests180.sql b/Test/SqlDom/TestScripts/AlterDatabaseScopedConfigTxnPriorityTests180.sql
new file mode 100644
index 0000000..bba4e7c
--- /dev/null
+++ b/Test/SqlDom/TestScripts/AlterDatabaseScopedConfigTxnPriorityTests180.sql
@@ -0,0 +1,20 @@
+-- TXN_PRIORITY_MODE = OFF
+ALTER DATABASE SCOPED CONFIGURATION SET TXN_PRIORITY_MODE = OFF;
+
+-- TXN_PRIORITY_MODE = ROLLBACK
+ALTER DATABASE SCOPED CONFIGURATION SET TXN_PRIORITY_MODE = ROLLBACK;
+
+-- TXN_PRIORITY_MODE = identifier (e.g., BLOCK)
+ALTER DATABASE SCOPED CONFIGURATION SET TXN_PRIORITY_MODE = BLOCK;
+
+-- TXN_PRIORITY_MIN_WAIT_MILLISECONDS = integer
+ALTER DATABASE SCOPED CONFIGURATION SET TXN_PRIORITY_MIN_WAIT_MILLISECONDS = 5000;
+
+-- TXN_PRIORITY_MIN_WAIT_MILLISECONDS = 0
+ALTER DATABASE SCOPED CONFIGURATION SET TXN_PRIORITY_MIN_WAIT_MILLISECONDS = 0;
+
+-- FOR SECONDARY with TXN_PRIORITY_MODE
+ALTER DATABASE SCOPED CONFIGURATION FOR SECONDARY SET TXN_PRIORITY_MODE = PRIMARY;
+
+-- FOR SECONDARY with TXN_PRIORITY_MIN_WAIT_MILLISECONDS
+ALTER DATABASE SCOPED CONFIGURATION FOR SECONDARY SET TXN_PRIORITY_MIN_WAIT_MILLISECONDS = 1000;
From 17094a594541d8dfee826d9dabb83b4c81bb06be Mon Sep 17 00:00:00 2001
From: Allan Cascante <120929896+acascante_microsoft@users.noreply.github.com>
Date: Mon, 12 Oct 2026 16:01:03 -0600
Subject: [PATCH 09/13] Apply join newline formatting to APPLY and CROSS JOIN
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
...lScriptGeneratorVisitor.UnqualifiedJoin.cs | 3 +-
Test/SqlDom/Baselines110/OffsetClause.sql | 9 +-
.../Baselines130/FromClauseTests130.sql | 17 ++-
.../Baselines150/FromClauseTests150.sql | 12 +-
.../Baselines170/AiGenerateChunksTests170.sql | 24 ++--
.../Baselines170/ComplexQueryTests170.sql | 83 ++++++-----
.../VectorSearchApproximateTests170.sql | 60 ++++----
.../VectorSearchCrossApplyTests170.sql | 15 +-
Test/SqlDom/Baselines90/FromClauseTests90.sql | 9 +-
.../BaselinesCommon/FromClauseTests.sql | 57 +++++---
.../BaselinesCommon/SelectStatementTests.sql | 3 +-
.../IdentifierFormattingTests.cs | 1 +
.../JoinClauseFormattingTests.cs | 130 +++++++++++++++++-
13 files changed, 300 insertions(+), 123 deletions(-)
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.UnqualifiedJoin.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.UnqualifiedJoin.cs
index 519d594..cc3792a 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.UnqualifiedJoin.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.UnqualifiedJoin.cs
@@ -28,13 +28,14 @@ public override void ExplicitVisit(UnqualifiedJoin node)
{
GenerateFragmentIfNotNull(node.FirstTableReference);
+ GenerateNewLineOrSpace(_options.NewLineBeforeJoinClause);
+
// Emit any pending gap comments before JOIN keywords
EmitPendingGapComments();
List generators = GetValueForEnumKey(_unqualifiedJoinTypeGenerators, node.UnqualifiedJoinType);
if (generators != null)
{
- GenerateSpace();
GenerateTokenList(generators);
}
diff --git a/Test/SqlDom/Baselines110/OffsetClause.sql b/Test/SqlDom/Baselines110/OffsetClause.sql
index 43cc3b8..d7749ca 100644
--- a/Test/SqlDom/Baselines110/OffsetClause.sql
+++ b/Test/SqlDom/Baselines110/OffsetClause.sql
@@ -41,10 +41,11 @@ ORDER BY a
OFFSET (sum(5)) ROWS FETCH NEXT 6 ROWS ONLY;
SELECT *
-FROM t1 CROSS APPLY (SELECT *
- FROM sys.objects
- ORDER BY 1
- OFFSET offset ROWS FETCH NEXT first ROWS ONLY) AS t1;
+FROM t1
+ CROSS APPLY (SELECT *
+ FROM sys.objects
+ ORDER BY 1
+ OFFSET offset ROWS FETCH NEXT first ROWS ONLY) AS t1;
INSERT INTO t
SELECT *
diff --git a/Test/SqlDom/Baselines130/FromClauseTests130.sql b/Test/SqlDom/Baselines130/FromClauseTests130.sql
index 88aed9e..9bf8769 100644
--- a/Test/SqlDom/Baselines130/FromClauseTests130.sql
+++ b/Test/SqlDom/Baselines130/FromClauseTests130.sql
@@ -25,7 +25,8 @@ SELECT *
FROM OPENJSON (@var, N'$') WITH (a INT, b VARCHAR (400), c NVARCHAR (MAX) COLLATE Latin1_General_Bin2 N'$.b.c');
SELECT *
-FROM Orders CROSS APPLY OPENJSON (Orders.Data, N'$.orderLineItems') WITH (ProductId VARCHAR (200) N'$.productId');
+FROM Orders
+ CROSS APPLY OPENJSON (Orders.Data, N'$.orderLineItems') WITH (ProductId VARCHAR (200) N'$.productId');
SELECT *
FROM OPENJSON (col, N'$') WITH (a INT, b VARCHAR (400), c NVARCHAR (MAX) COLLATE Latin1_General_Bin2 N'$.b.c');
@@ -58,19 +59,23 @@ SELECT *
FROM string_split (NULL, N',');
SELECT t2.value
-FROM String_Split ('Lorem-ipsum dolor-sit amet.', '-') AS t1 CROSS APPLY String_Split (t1.value, ' ') AS t2;
+FROM String_Split ('Lorem-ipsum dolor-sit amet.', '-') AS t1
+ CROSS APPLY String_Split (t1.value, ' ') AS t2;
SELECT t2.ordinal
-FROM String_Split ('Lorem-ipsum dolor-sit amet.', '-', 1) AS t1 CROSS APPLY String_Split (t1.ordinal, ' ') AS t2;
+FROM String_Split ('Lorem-ipsum dolor-sit amet.', '-', 1) AS t1
+ CROSS APPLY String_Split (t1.ordinal, ' ') AS t2;
SELECT value
-FROM (VALUES ('Lorem ipsum'), ('dolor sit amet.')) AS x(col) CROSS APPLY STRING_SPLIT (col, ',');
+FROM (VALUES ('Lorem ipsum'), ('dolor sit amet.')) AS x(col)
+ CROSS APPLY STRING_SPLIT (col, ',');
SELECT ordinal
-FROM (VALUES ('Lorem ipsum'), ('dolor sit amet.')) AS x(col) CROSS APPLY STRING_SPLIT (col, ',', 1);
+FROM (VALUES ('Lorem ipsum'), ('dolor sit amet.')) AS x(col)
+ CROSS APPLY STRING_SPLIT (col, ',', 1);
SELECT *
FROM [STRING_SPLIT] (@var, @sep);
SELECT *
-FROM "STRING_SPLIT" (@var, @sep);
+FROM "STRING_SPLIT" (@var, @sep);
\ No newline at end of file
diff --git a/Test/SqlDom/Baselines150/FromClauseTests150.sql b/Test/SqlDom/Baselines150/FromClauseTests150.sql
index b7a5b47..6f2b8b1 100644
--- a/Test/SqlDom/Baselines150/FromClauseTests150.sql
+++ b/Test/SqlDom/Baselines150/FromClauseTests150.sql
@@ -11,16 +11,20 @@ SELECT ordinal
FROM STRING_SPLIT (N'Lorem ipsum dolor sit amet.', N' ', 1);
SELECT t2.value
-FROM String_Split ('Lorem-ipsum dolor-sit amet.', '-', 0) AS t1 CROSS APPLY String_Split (t1.value, ' ') AS t2;
+FROM String_Split ('Lorem-ipsum dolor-sit amet.', '-', 0) AS t1
+ CROSS APPLY String_Split (t1.value, ' ') AS t2;
SELECT t2.ordinal
-FROM String_Split ('Lorem-ipsum dolor-sit amet.', '-', 1) AS t1 CROSS APPLY String_Split (t1.ordinal, ' ') AS t2;
+FROM String_Split ('Lorem-ipsum dolor-sit amet.', '-', 1) AS t1
+ CROSS APPLY String_Split (t1.ordinal, ' ') AS t2;
SELECT value
-FROM (VALUES ('Lorem ipsum'), ('dolor sit amet.')) AS x(col) CROSS APPLY STRING_SPLIT (col, ',', 0);
+FROM (VALUES ('Lorem ipsum'), ('dolor sit amet.')) AS x(col)
+ CROSS APPLY STRING_SPLIT (col, ',', 0);
SELECT ordinal
-FROM (VALUES ('Lorem ipsum'), ('dolor sit amet.')) AS x(col) CROSS APPLY STRING_SPLIT (col, ',', 1);
+FROM (VALUES ('Lorem ipsum'), ('dolor sit amet.')) AS x(col)
+ CROSS APPLY STRING_SPLIT (col, ',', 1);
SELECT *
FROM CHANGETABLE(CHANGES t1, 10, FORCESEEK) AS a;
diff --git a/Test/SqlDom/Baselines170/AiGenerateChunksTests170.sql b/Test/SqlDom/Baselines170/AiGenerateChunksTests170.sql
index ef7cb3c..d1a3530 100644
--- a/Test/SqlDom/Baselines170/AiGenerateChunksTests170.sql
+++ b/Test/SqlDom/Baselines170/AiGenerateChunksTests170.sql
@@ -20,13 +20,16 @@ SELECT *
FROM AI_GENERATE_CHUNKS (SOURCE = NULL, CHUNK_TYPE = fixed, CHUNK_SIZE = 5);
SELECT *
-FROM t1 CROSS APPLY AI_GENERATE_CHUNKS (SOURCE = t1.c1, CHUNK_TYPE = fixed, CHUNK_SIZE = 10);
+FROM t1
+ CROSS APPLY AI_GENERATE_CHUNKS (SOURCE = t1.c1, CHUNK_TYPE = fixed, CHUNK_SIZE = 10);
SELECT *
-FROM t1 CROSS APPLY AI_GENERATE_CHUNKS (SOURCE = 'some text', CHUNK_TYPE = fixed, CHUNK_SIZE = t1.c1);
+FROM t1
+ CROSS APPLY AI_GENERATE_CHUNKS (SOURCE = 'some text', CHUNK_TYPE = fixed, CHUNK_SIZE = t1.c1);
SELECT *
-FROM t1 CROSS APPLY AI_GENERATE_CHUNKS (SOURCE = 'some text', CHUNK_TYPE = fixed, CHUNK_SIZE = 10, OVERLAP = t1.c1);
+FROM t1
+ CROSS APPLY AI_GENERATE_CHUNKS (SOURCE = 'some text', CHUNK_TYPE = fixed, CHUNK_SIZE = 10, OVERLAP = t1.c1);
SELECT *
FROM AI_GENERATE_CHUNKS (SOURCE = N'some text', CHUNK_TYPE = fixed, CHUNK_SIZE = NULL);
@@ -53,7 +56,8 @@ CREATE VIEW v_GeneratedChunksFromTable
AS
SELECT t2.Id,
chunks.*
-FROM t2 CROSS APPLY AI_GENERATE_CHUNKS (SOURCE = t2.Text, CHUNK_TYPE = fixed, CHUNK_SIZE = 5) AS chunks;
+FROM t2
+ CROSS APPLY AI_GENERATE_CHUNKS (SOURCE = t2.Text, CHUNK_TYPE = fixed, CHUNK_SIZE = 5) AS chunks;
GO
CREATE PROCEDURE usp_GenerateChunks
@@ -79,17 +83,21 @@ CREATE TABLE dbo.AI_GENERATE_CHUNKS (
);
SELECT SOURCE
-FROM userTable CROSS APPLY dbo.AI_GENERATE_CHUNKS(target);
+FROM userTable
+ CROSS APPLY dbo.AI_GENERATE_CHUNKS(target);
SELECT SOURCE
-FROM userTable CROSS APPLY dbo.[AI_GENERATE_CHUNKS](SOURCE);
+FROM userTable
+ CROSS APPLY dbo.[AI_GENERATE_CHUNKS](SOURCE);
SELECT SOURCE
-FROM userTable CROSS APPLY dbo.AI_GENERATE_CHUNKS([SOURCE]);
+FROM userTable
+ CROSS APPLY dbo.AI_GENERATE_CHUNKS([SOURCE]);
SELECT SOURCE
-FROM userTable CROSS APPLY dbo.[AI_GENERATE_CHUNKS]([SOURCE]);
+FROM userTable
+ CROSS APPLY dbo.[AI_GENERATE_CHUNKS]([SOURCE]);
SELECT *
FROM dbo.AI_GENERATE_CHUNKS(3);
\ No newline at end of file
diff --git a/Test/SqlDom/Baselines170/ComplexQueryTests170.sql b/Test/SqlDom/Baselines170/ComplexQueryTests170.sql
index 3e2cb7b..581b675 100644
--- a/Test/SqlDom/Baselines170/ComplexQueryTests170.sql
+++ b/Test/SqlDom/Baselines170/ComplexQueryTests170.sql
@@ -1,8 +1,9 @@
SELECT 139 AS [p_1_0]
-FROM ([dbo].[table_0] AS [alias_1_0] CROSS JOIN (([dbo].[table_6] AS [alias_1_4]
- RIGHT OUTER JOIN
- [dbo].[table_3] AS [alias_1_5]
- ON [alias_1_4].[c_19] = CAST ('[-6.464173E+08,1.040823E+07,1.699169E+08]' AS VECTOR(3)))));
+FROM ([dbo].[table_0] AS [alias_1_0]
+ CROSS JOIN (([dbo].[table_6] AS [alias_1_4]
+ RIGHT OUTER JOIN
+ [dbo].[table_3] AS [alias_1_5]
+ ON [alias_1_4].[c_19] = CAST ('[-6.464173E+08,1.040823E+07,1.699169E+08]' AS VECTOR(3)))));
SELECT CAST ('test' AS VECTOR(3, Float32));
@@ -15,34 +16,40 @@ SELECT 1
WHERE col IN (SELECT CAST ('test' AS VECTOR(3, Float32)));
SELECT CAST ($492050157978986.2129 AS MONEY) AS [p_0_0]
-FROM ([dbo].[table_6] AS [alias_0_0] CROSS JOIN [dbo].[table_4] AS [alias_0_1])
+FROM ([dbo].[table_6] AS [alias_0_0]
+ CROSS JOIN [dbo].[table_4] AS [alias_0_1])
WHERE (NOT (SYSDATETIME() IN (SELECT 139 AS [p_1_0]
- FROM ([dbo].[table_0] AS [alias_1_0] CROSS JOIN ([dbo].[table_6] AS [alias_1_1]
- LEFT OUTER JOIN
- ([dbo].[table_6] AS [alias_1_2]
- INNER JOIN
- ([dbo].[table_5] AS [alias_1_3] CROSS APPLY ([dbo].[table_6] AS [alias_1_4]
- RIGHT OUTER JOIN
- [dbo].[table_3] AS [alias_1_5]
- ON [alias_1_4].[c_19] = CAST ('[-6.464173E+08,1.040823E+07,1.699169E+08]' AS VECTOR(3, Float32))))
- ON [alias_1_2].[c_14] = [alias_1_3].[c_13])
- ON [alias_1_1].[c_1] = CONVERT (VECTOR(77), '[-7.230808E+08,4.075427E+08]'))))
+ FROM ([dbo].[table_0] AS [alias_1_0]
+ CROSS JOIN ([dbo].[table_6] AS [alias_1_1]
+ LEFT OUTER JOIN
+ ([dbo].[table_6] AS [alias_1_2]
+ INNER JOIN
+ ([dbo].[table_5] AS [alias_1_3]
+ CROSS APPLY ([dbo].[table_6] AS [alias_1_4]
+ RIGHT OUTER JOIN
+ [dbo].[table_3] AS [alias_1_5]
+ ON [alias_1_4].[c_19] = CAST ('[-6.464173E+08,1.040823E+07,1.699169E+08]' AS VECTOR(3, Float32))))
+ ON [alias_1_2].[c_14] = [alias_1_3].[c_13])
+ ON [alias_1_1].[c_1] = CONVERT (VECTOR(77), '[-7.230808E+08,4.075427E+08]'))))
OR (CONVERT (BIGINT, CONVERT (INT, 8)) >= 157)
OR (140 <= 19)));
SELECT CAST ($492050157978986.2129 AS MONEY) AS [p_0_0]
-FROM ([dbo].[table_6] AS [alias_0_0] CROSS JOIN [dbo].[table_4] AS [alias_0_1])
+FROM ([dbo].[table_6] AS [alias_0_0]
+ CROSS JOIN [dbo].[table_4] AS [alias_0_1])
WHERE (NOT (SYSDATETIME() IN (SELECT 139 AS [p_1_0]
- FROM ([dbo].[table_0] AS [alias_1_0] CROSS JOIN ([dbo].[table_6] AS [alias_1_1]
- LEFT OUTER JOIN
- ([dbo].[table_6] AS [alias_1_2]
- INNER JOIN
- ([dbo].[table_5] AS [alias_1_3] CROSS APPLY ([dbo].[table_6] AS [alias_1_4]
- RIGHT OUTER JOIN
- [dbo].[table_3] AS [alias_1_5]
- ON [alias_1_4].[c_19] = CAST ('[-6.464173E+08,1.040823E+07,1.699169E+08]' AS VECTOR(3))))
- ON [alias_1_2].[c_14] = [alias_1_3].[c_13])
- ON [alias_1_1].[c_1] = CONVERT (VECTOR(77), '[-7.230808E+08,4.075427E+08]'))))
+ FROM ([dbo].[table_0] AS [alias_1_0]
+ CROSS JOIN ([dbo].[table_6] AS [alias_1_1]
+ LEFT OUTER JOIN
+ ([dbo].[table_6] AS [alias_1_2]
+ INNER JOIN
+ ([dbo].[table_5] AS [alias_1_3]
+ CROSS APPLY ([dbo].[table_6] AS [alias_1_4]
+ RIGHT OUTER JOIN
+ [dbo].[table_3] AS [alias_1_5]
+ ON [alias_1_4].[c_19] = CAST ('[-6.464173E+08,1.040823E+07,1.699169E+08]' AS VECTOR(3))))
+ ON [alias_1_2].[c_14] = [alias_1_3].[c_13])
+ ON [alias_1_1].[c_1] = CONVERT (VECTOR(77), '[-7.230808E+08,4.075427E+08]'))))
OR (CONVERT (BIGINT, CONVERT (INT, 8)) >= 157)
OR (140 <= 19)));
@@ -50,17 +57,19 @@ SELECT TOP 100 t1.order_id,
t1.customer_id,
t2.product_name,
t3.category_name
-FROM ([dbo].[orders] AS t1 CROSS JOIN ([dbo].[order_items] AS t2
- LEFT OUTER JOIN
- ([dbo].[products] AS t3
- INNER JOIN
- ([dbo].[categories] AS t4 CROSS APPLY ([dbo].[suppliers] AS t5
- RIGHT OUTER JOIN
- [dbo].[regions] AS t6
- ON t5.region_id = t6.id
- AND t5.active = 1))
- ON t3.category_id = t4.id)
- ON t2.product_id = t3.id))
+FROM ([dbo].[orders] AS t1
+ CROSS JOIN ([dbo].[order_items] AS t2
+ LEFT OUTER JOIN
+ ([dbo].[products] AS t3
+ INNER JOIN
+ ([dbo].[categories] AS t4
+ CROSS APPLY ([dbo].[suppliers] AS t5
+ RIGHT OUTER JOIN
+ [dbo].[regions] AS t6
+ ON t5.region_id = t6.id
+ AND t5.active = 1))
+ ON t3.category_id = t4.id)
+ ON t2.product_id = t3.id))
WHERE (t1.order_date >= '2025-01-01'
AND t2.quantity > 0
AND EXISTS (SELECT 1
diff --git a/Test/SqlDom/Baselines170/VectorSearchApproximateTests170.sql b/Test/SqlDom/Baselines170/VectorSearchApproximateTests170.sql
index 958e8a4..563d5ff 100644
--- a/Test/SqlDom/Baselines170/VectorSearchApproximateTests170.sql
+++ b/Test/SqlDom/Baselines170/VectorSearchApproximateTests170.sql
@@ -1,13 +1,14 @@
SELECT TOP 10 WITH APPROXIMATE qt.qid,
src.id,
ann.distance
-FROM QueryTable AS qt CROSS APPLY VECTOR_SEARCH(
- TABLE = graphnode AS src,
- COLUMN = embedding,
- SIMILAR_TO = qt.qembedding,
- METRIC = 'euclidean',
- TOP_N = 10
- ) AS ann
+FROM QueryTable AS qt
+ CROSS APPLY VECTOR_SEARCH(
+ TABLE = graphnode AS src,
+ COLUMN = embedding,
+ SIMILAR_TO = qt.qembedding,
+ METRIC = 'euclidean',
+ TOP_N = 10
+ ) AS ann
ORDER BY ann.distance;
@@ -15,13 +16,14 @@ GO
SELECT TOP 10 WITH APPROXIMATE qt.qid,
src.id,
ann.distance
-FROM QueryTable AS qt CROSS APPLY VECTOR_SEARCH(
- TABLE = graphnode AS src,
- COLUMN = embedding,
- SIMILAR_TO = qt.qembedding,
- METRIC = 'euclidean',
- TOP_N = 10
- ) AS ann
+FROM QueryTable AS qt
+ CROSS APPLY VECTOR_SEARCH(
+ TABLE = graphnode AS src,
+ COLUMN = embedding,
+ SIMILAR_TO = qt.qembedding,
+ METRIC = 'euclidean',
+ TOP_N = 10
+ ) AS ann
ORDER BY ann.distance;
@@ -29,13 +31,14 @@ GO
SELECT qt.qid,
src.id,
ann.distance
-FROM QueryTable AS qt CROSS APPLY VECTOR_SEARCH(
- TABLE = graphnode AS src,
- COLUMN = embedding,
- SIMILAR_TO = qt.qembedding,
- METRIC = 'euclidean',
- TOP_N = 10
- ) AS ann
+FROM QueryTable AS qt
+ CROSS APPLY VECTOR_SEARCH(
+ TABLE = graphnode AS src,
+ COLUMN = embedding,
+ SIMILAR_TO = qt.qembedding,
+ METRIC = 'euclidean',
+ TOP_N = 10
+ ) AS ann
ORDER BY ann.distance
FETCH APPROXIMATE NEXT 10 ROWS ONLY;
@@ -44,12 +47,13 @@ GO
SELECT qt.qid,
src.id,
ann.distance
-FROM QueryTable AS qt CROSS APPLY VECTOR_SEARCH(
- TABLE = graphnode AS src,
- COLUMN = embedding,
- SIMILAR_TO = qt.qembedding,
- METRIC = 'euclidean',
- TOP_N = 10
- ) AS ann
+FROM QueryTable AS qt
+ CROSS APPLY VECTOR_SEARCH(
+ TABLE = graphnode AS src,
+ COLUMN = embedding,
+ SIMILAR_TO = qt.qembedding,
+ METRIC = 'euclidean',
+ TOP_N = 10
+ ) AS ann
ORDER BY ann.distance
FETCH APPROXIMATE NEXT 10 ROWS ONLY;
\ No newline at end of file
diff --git a/Test/SqlDom/Baselines170/VectorSearchCrossApplyTests170.sql b/Test/SqlDom/Baselines170/VectorSearchCrossApplyTests170.sql
index 3e715b6..e22e588 100644
--- a/Test/SqlDom/Baselines170/VectorSearchCrossApplyTests170.sql
+++ b/Test/SqlDom/Baselines170/VectorSearchCrossApplyTests170.sql
@@ -3,10 +3,11 @@ DECLARE @qv AS VECTOR(1536);
SELECT qt.qid,
src.id,
ann.distance
-FROM QueryTable AS qt CROSS APPLY VECTOR_SEARCH(
- TABLE = graphnode AS src,
- COLUMN = embedding,
- SIMILAR_TO = qt.qembedding,
- METRIC = 'euclidean',
- TOP_N = dbo.qt.top_n
- ) AS ann;
+FROM QueryTable AS qt
+ CROSS APPLY VECTOR_SEARCH(
+ TABLE = graphnode AS src,
+ COLUMN = embedding,
+ SIMILAR_TO = qt.qembedding,
+ METRIC = 'euclidean',
+ TOP_N = dbo.qt.top_n
+ ) AS ann;
\ No newline at end of file
diff --git a/Test/SqlDom/Baselines90/FromClauseTests90.sql b/Test/SqlDom/Baselines90/FromClauseTests90.sql
index dc17df2..1a22eec 100644
--- a/Test/SqlDom/Baselines90/FromClauseTests90.sql
+++ b/Test/SqlDom/Baselines90/FromClauseTests90.sql
@@ -10,7 +10,8 @@ FROM t1 AS table1 TABLESAMPLE (12 + 3 ROWS);
GO
SELECT *
-FROM master..sysprocesses AS p CROSS APPLY ::fn_get_sql (p.sql_handle);
+FROM master..sysprocesses AS p
+ CROSS APPLY ::fn_get_sql (p.sql_handle);
SELECT *
FROM ::f1 (a.b * (12 + 20));
@@ -61,7 +62,8 @@ FROM st PIVOT (AVG (.st.StandardCost) FOR st.DaysToManufacture IN ([0], [1])) AS
GO
SELECT *
-FROM t1 PIVOT (dbo.f (c1) FOR [if] IN (a)) AS PTable CROSS JOIN t2 UNPIVOT (c2 FOR T2 IN ([a], b)) AS UPTable;
+FROM t1 PIVOT (dbo.f (c1) FOR [if] IN (a)) AS PTable
+ CROSS JOIN t2 UNPIVOT (c2 FOR T2 IN ([a], b)) AS UPTable;
SELECT *
FROM t1
@@ -73,7 +75,8 @@ FROM t1
ON t1.c1 = t10.c1 PIVOT (dbo.f (c1) FOR [if] IN (a)) AS PTable;
SELECT *
-FROM t1 CROSS APPLY t2 UNPIVOT (q FOR n IN (t1.c0, c1)) AS a;
+FROM t1
+ CROSS APPLY t2 UNPIVOT (q FOR n IN (t1.c0, c1)) AS a;
SELECT *
FROM (SELECT *
diff --git a/Test/SqlDom/BaselinesCommon/FromClauseTests.sql b/Test/SqlDom/BaselinesCommon/FromClauseTests.sql
index 907f9d2..f836157 100644
--- a/Test/SqlDom/BaselinesCommon/FromClauseTests.sql
+++ b/Test/SqlDom/BaselinesCommon/FromClauseTests.sql
@@ -47,7 +47,8 @@ SELECT c1
FROM t1 WITH (HOLDLOCK, READPAST, INDEX (0));
SELECT *
-FROM t1 WITH (NOLOCK) CROSS APPLY dbo.tvf WITH (NOLOCK);
+FROM t1 WITH (NOLOCK)
+ CROSS APPLY dbo.tvf WITH (NOLOCK);
SELECT c1
FROM t1 AS table1 WITH (INDEX (0, 1, ind2), HOLDLOCK, NOLOCK, PAGLOCK, READCOMMITTED, READPAST, READUNCOMMITTED, REPEATABLEREAD, ROWLOCK, SERIALIZABLE, TABLOCK, TABLOCKX, UPDLOCK, XLOCK, NOWAIT), t2 WITH (NOWAIT);
@@ -68,25 +69,38 @@ FROM @var1, @var2 AS [table 1], @var3 AS table2;
GO
SELECT *
-FROM t1 CROSS JOIN t10;
+FROM t1
+ CROSS JOIN t10;
SELECT *
-FROM t1 CROSS JOIN (t10 CROSS JOIN t11);
+FROM t1
+ CROSS JOIN (t10
+ CROSS JOIN t11);
SELECT *
-FROM (((t1 CROSS JOIN ((t10 CROSS JOIN t11)))));
+FROM (((t1
+ CROSS JOIN ((t10
+ CROSS JOIN t11)))));
SELECT *
-FROM t1 CROSS APPLY (t10 CROSS APPLY t11);
+FROM t1
+ CROSS APPLY (t10
+ CROSS APPLY t11);
SELECT *
-FROM (((t1 CROSS APPLY ((t10 CROSS APPLY t11)))));
+FROM (((t1
+ CROSS APPLY ((t10
+ CROSS APPLY t11)))));
SELECT *
-FROM t1 OUTER APPLY (t10 OUTER APPLY t11);
+FROM t1
+ OUTER APPLY (t10
+ OUTER APPLY t11);
SELECT *
-FROM (((t1 OUTER APPLY ((t10 OUTER APPLY t11)))));
+FROM (((t1
+ OUTER APPLY ((t10
+ OUTER APPLY t11)))));
SELECT *
FROM t1
@@ -240,7 +254,8 @@ FROM (SELECT c1 AS a,
SELECT *
FROM (SELECT *
- FROM t1) AS t10(c1, c2) CROSS JOIN t2;
+ FROM t1) AS t10(c1, c2)
+ CROSS JOIN t2;
SELECT *
FROM ((SELECT *
@@ -265,13 +280,15 @@ FROM { OJ t1
ON c1 < 10 };
SELECT *
-FROM { OJ t1 CROSS JOIN t2
+FROM { OJ t1
+ CROSS JOIN t2
INNER JOIN
t2
ON c1 < 10 };
SELECT *
-FROM { OJ (t1 CROSS JOIN t2)
+FROM { OJ (t1
+ CROSS JOIN t2)
INNER JOIN
t2
ON c1 < 10 };
@@ -279,18 +296,22 @@ FROM { OJ (t1 CROSS JOIN t2)
SELECT *
FROM t1
INNER JOIN
- { OJ t1 CROSS JOIN t2
+ { OJ t1
+ CROSS JOIN t2
INNER JOIN
t2
ON c1 < 10 }
- ON c1 < 10 CROSS JOIN t3;
+ ON c1 < 10
+ CROSS JOIN t3;
SELECT jc.[JobCandidateID]
-FROM [HumanResources].[JobCandidate] AS jc CROSS APPLY jc.[Resume].nodes(N'declare default element namespace "http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/Resume";
+FROM [HumanResources].[JobCandidate] AS jc
+ CROSS APPLY jc.[Resume].nodes(N'declare default element namespace "http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/Resume";
/Resume/Employment') AS Employment(ref);
SELECT ecp.*
-FROM sys.dm_exec_cached_plans AS ecp OUTER APPLY sys.dm_exec_plan_attributes(ecp.plan_handle) AS epa;
+FROM sys.dm_exec_cached_plans AS ecp
+ OUTER APPLY sys.dm_exec_plan_attributes(ecp.plan_handle) AS epa;
SELECT au_id
FROM [dbo].[Authors] AS auth WITH (NOLOCK)
@@ -303,7 +324,8 @@ SELECT *
FROM t AS s WITH (NOLOCK);
SELECT *
-FROM t1 WITH (NOLOCK) CROSS APPLY dbo.tvf WITH (NOLOCK);
+FROM t1 WITH (NOLOCK)
+ CROSS APPLY dbo.tvf WITH (NOLOCK);
SELECT *
FROM t AS s WITH (NOLOCK);
@@ -312,5 +334,4 @@ SELECT *
FROM t WITH (NOLOCK);
SELECT *
-FROM t AS s WITH (NOLOCK);
-
+FROM t AS s WITH (NOLOCK);
\ No newline at end of file
diff --git a/Test/SqlDom/BaselinesCommon/SelectStatementTests.sql b/Test/SqlDom/BaselinesCommon/SelectStatementTests.sql
index ee84ff0..ee13358 100644
--- a/Test/SqlDom/BaselinesCommon/SelectStatementTests.sql
+++ b/Test/SqlDom/BaselinesCommon/SelectStatementTests.sql
@@ -133,7 +133,8 @@ ORDER BY c1, c2 ASC, c3 DESC;
SELECT c1,
c2
-FROM t1 CROSS JOIN t2
+FROM t1
+ CROSS JOIN t2
UNION
SELECT *
FROM t2
diff --git a/Test/SqlDom/ScriptGenerator/IdentifierFormattingTests.cs b/Test/SqlDom/ScriptGenerator/IdentifierFormattingTests.cs
index 95edba3..d7cbf1d 100644
--- a/Test/SqlDom/ScriptGenerator/IdentifierFormattingTests.cs
+++ b/Test/SqlDom/ScriptGenerator/IdentifierFormattingTests.cs
@@ -1016,6 +1016,7 @@ public void TestExcludeBracketsSpecialOperatorNameIsKnownLimitation()
{
const string input = "SELECT SOURCE FROM userTable CROSS APPLY dbo.[AI_GENERATE_CHUNKS](SOURCE);";
var options = MakeOptions(IdentifierCasing.Preserve, IdentifierBracketing.ExcludeBrackets);
+ options.NewLineBeforeJoinClause = false;
const string expected = @"SELECT SOURCE FROM userTable CROSS APPLY dbo.AI_GENERATE_CHUNKS(SOURCE);";
AssertGeneratedWith(new TSql170Parser(true), new Sql170ScriptGenerator(options), input, expected);
diff --git a/Test/SqlDom/ScriptGenerator/JoinClauseFormattingTests.cs b/Test/SqlDom/ScriptGenerator/JoinClauseFormattingTests.cs
index 39ff408..75c2b4e 100644
--- a/Test/SqlDom/ScriptGenerator/JoinClauseFormattingTests.cs
+++ b/Test/SqlDom/ScriptGenerator/JoinClauseFormattingTests.cs
@@ -195,10 +195,11 @@ INNER JOIN c
[TestMethod]
[Priority(0)]
[SqlStudioTestCategory(Category.UnitTest)]
- public void TestCrossJoinUnaffectedByNewOptions()
+ public void TestCrossJoinUnaffectedByOnClauseOptions()
{
- // CROSS JOIN has no ON clause (it is an UnqualifiedJoin), so the new options do not
- // change its formatting.
+ // CROSS JOIN has no ON clause (it is an UnqualifiedJoin), so the ON-related options
+ // (NewLineAfterJoinKeyword, NewLineBeforeOnClause) do not change its formatting. The
+ // leading newline comes from NewLineBeforeJoinClause, which defaults to true.
const string input = "SELECT * FROM a CROSS JOIN b;";
var options = new SqlScriptGeneratorOptions
{
@@ -208,7 +209,8 @@ public void TestCrossJoinUnaffectedByNewOptions()
const string expected =
@"
SELECT *
-FROM a CROSS JOIN b;";
+FROM a
+ CROSS JOIN b;";
AssertGenerated(input, options, expected);
}
@@ -216,9 +218,10 @@ public void TestCrossJoinUnaffectedByNewOptions()
[TestMethod]
[Priority(0)]
[SqlStudioTestCategory(Category.UnitTest)]
- public void TestCrossApplyUnaffectedByNewOptions()
+ public void TestCrossApplyUnaffectedByOnClauseOptions()
{
- // CROSS APPLY has no ON clause either, so the new options leave it unchanged.
+ // CROSS APPLY has no ON clause either, so the ON-related options leave it unchanged. The
+ // leading newline comes from NewLineBeforeJoinClause, which defaults to true.
const string input = "SELECT * FROM a CROSS APPLY dbo.f(a.id) AS b;";
var options = new SqlScriptGeneratorOptions
{
@@ -228,11 +231,126 @@ public void TestCrossApplyUnaffectedByNewOptions()
const string expected =
@"
SELECT *
+FROM a
+ CROSS APPLY dbo.f(a.id) AS b;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCrossApplyNewLineBeforeJoinClauseDefaultTrue()
+ {
+ // NewLineBeforeJoinClause (default true) applies to CROSS APPLY just like a qualified
+ // JOIN: the operator goes on its own line under the first table source.
+ const string input = "SELECT * FROM a CROSS APPLY dbo.f(a.id) AS b;";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+SELECT *
+FROM a
+ CROSS APPLY dbo.f(a.id) AS b;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCrossApplyInlineWhenNewLineBeforeJoinClauseFalse()
+ {
+ // When NewLineBeforeJoinClause is false, CROSS APPLY stays on the same line as the first
+ // table source.
+ const string input = "SELECT * FROM a CROSS APPLY dbo.f(a.id) AS b;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ NewLineBeforeJoinClause = false
+ };
+ const string expected =
+@"
+SELECT *
FROM a CROSS APPLY dbo.f(a.id) AS b;";
AssertGenerated(input, options, expected);
}
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestOuterApplyInlineWhenNewLineBeforeJoinClauseFalse()
+ {
+ // When NewLineBeforeJoinClause is false, OUTER APPLY stays on the same line as the first
+ // table source.
+ const string input = "SELECT * FROM a OUTER APPLY dbo.f(a.id) AS b;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ NewLineBeforeJoinClause = false
+ };
+ const string expected =
+@"
+SELECT *
+FROM a OUTER APPLY dbo.f(a.id) AS b;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestOuterApplyNewLineBeforeJoinClauseDefaultTrue()
+ {
+ // NewLineBeforeJoinClause (default true) applies to OUTER APPLY just like a qualified
+ // JOIN: the operator goes on its own line under the first table source.
+ const string input = "SELECT * FROM a OUTER APPLY dbo.f(a.id) AS b;";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+SELECT *
+FROM a
+ OUTER APPLY dbo.f(a.id) AS b;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCrossJoinInlineWhenNewLineBeforeJoinClauseFalse()
+ {
+ // When NewLineBeforeJoinClause is false, CROSS JOIN stays on the same line as the first
+ // table source.
+ const string input = "SELECT * FROM a CROSS JOIN b;";
+ var options = new SqlScriptGeneratorOptions
+ {
+ NewLineBeforeJoinClause = false
+ };
+ const string expected =
+@"
+SELECT *
+FROM a CROSS JOIN b;";
+
+ AssertGenerated(input, options, expected);
+ }
+
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void TestCrossJoinNewLineBeforeJoinClauseDefaultTrue()
+ {
+ // NewLineBeforeJoinClause (default true) applies to CROSS JOIN just like a qualified
+ // JOIN: the operator goes on its own line under the first table source.
+ const string input = "SELECT * FROM a CROSS JOIN b;";
+ var options = new SqlScriptGeneratorOptions();
+ const string expected =
+@"
+SELECT *
+FROM a
+ CROSS JOIN b;";
+
+ AssertGenerated(input, options, expected);
+ }
+
[TestMethod]
[Priority(0)]
[SqlStudioTestCategory(Category.UnitTest)]
From c3db747e3c0fa94ec94e745564d58e39d728412b Mon Sep 17 00:00:00 2001
From: Allan Cascante <120929896+acascante_microsoft@users.noreply.github.com>
Date: Mon, 12 Oct 2026 16:01:51 -0600
Subject: [PATCH 10/13] Reconcile released query hints and source parity
Preserve the public query-hint support without duplicate generator mappings, update cross-version expectations, and carry over released source cleanup and option documentation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
SqlScriptDom/Parser/TSql/TSql170.g | 6 +--
SqlScriptDom/Parser/TSql/TSql180.g | 6 +--
SqlScriptDom/Parser/TSql/TSqlFabricDW.g | 38 ++++++++++++-------
SqlScriptDom/Parser/TSql/VectorIndexType.cs | 1 +
.../Parser/TSql/VectorMetricTypeHelper.cs | 2 -
...SqlScriptGeneratorVisitor.OptimizerHint.cs | 11 +-----
.../Settings/SqlScriptGeneratorOptions.xml | 2 +-
Test/SqlDom/OnlyFabricDWSyntaxTests.cs | 2 +-
Test/SqlDom/ParserErrorsTests.cs | 2 +-
9 files changed, 36 insertions(+), 34 deletions(-)
diff --git a/SqlScriptDom/Parser/TSql/TSql170.g b/SqlScriptDom/Parser/TSql/TSql170.g
index eb00d94..6ea5674 100644
--- a/SqlScriptDom/Parser/TSql/TSql170.g
+++ b/SqlScriptDom/Parser/TSql/TSql170.g
@@ -22209,14 +22209,14 @@ simpleOptimizerHint returns [OptimizerHint vResult = FragmentFactory.CreateFragm
{
Match(tForce, CodeGenerationSupporter.Force);
vResult.HintKind = OptimizerHintKind.ForceOrder;
- UpdateTokenInfo(vResult, tForce);
+ UpdateTokenInfo(vResult, tForce);
}
|
Distributed tPlan1:Plan
{
Match(tForce, CodeGenerationSupporter.Force);
vResult.HintKind = OptimizerHintKind.ForceDistributedPlan;
- UpdateTokenInfo(vResult, tForce);
+ UpdateTokenInfo(vResult, tForce);
}
|
tSingle:Identifier tNode:Identifier tPlan2:Plan
@@ -22225,7 +22225,7 @@ simpleOptimizerHint returns [OptimizerHint vResult = FragmentFactory.CreateFragm
Match(tSingle, CodeGenerationSupporter.Single);
Match(tNode, CodeGenerationSupporter.GraphNode); // NODE
vResult.HintKind = OptimizerHintKind.ForceSingleNodePlan;
- UpdateTokenInfo(vResult, tForce);
+ UpdateTokenInfo(vResult, tForce);
}
)
| tHash:Identifier Group
diff --git a/SqlScriptDom/Parser/TSql/TSql180.g b/SqlScriptDom/Parser/TSql/TSql180.g
index 8ca5cc2..cbb915f 100644
--- a/SqlScriptDom/Parser/TSql/TSql180.g
+++ b/SqlScriptDom/Parser/TSql/TSql180.g
@@ -22443,14 +22443,14 @@ simpleOptimizerHint returns [OptimizerHint vResult = FragmentFactory.CreateFragm
{
Match(tForce, CodeGenerationSupporter.Force);
vResult.HintKind = OptimizerHintKind.ForceOrder;
- UpdateTokenInfo(vResult, tForce);
+ UpdateTokenInfo(vResult, tForce);
}
|
Distributed tPlan1:Plan
{
Match(tForce, CodeGenerationSupporter.Force);
vResult.HintKind = OptimizerHintKind.ForceDistributedPlan;
- UpdateTokenInfo(vResult, tForce);
+ UpdateTokenInfo(vResult, tForce);
}
|
tSingle:Identifier tNode:Identifier tPlan2:Plan
@@ -22459,7 +22459,7 @@ simpleOptimizerHint returns [OptimizerHint vResult = FragmentFactory.CreateFragm
Match(tSingle, CodeGenerationSupporter.Single);
Match(tNode, CodeGenerationSupporter.GraphNode); // NODE
vResult.HintKind = OptimizerHintKind.ForceSingleNodePlan;
- UpdateTokenInfo(vResult, tForce);
+ UpdateTokenInfo(vResult, tForce);
}
)
| tHash:Identifier Group
diff --git a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
index 98b7989..b3125b2 100644
--- a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
+++ b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
@@ -22018,19 +22018,31 @@ simpleOptimizerHint returns [OptimizerHint vResult = FragmentFactory.CreateFragm
vResult.HintKind = OptimizerHintKind.MergeUnion;
UpdateTokenInfo(vResult, tMergeUnion);
}
- | tForce:Identifier Order
- {
- Match(tForce, CodeGenerationSupporter.Force);
- vResult.HintKind = OptimizerHintKind.ForceOrder;
- UpdateTokenInfo(vResult, tForce);
- }
- | tForceDistributed:Identifier Distributed tPlanDistributed:Plan
- {
- Match(tForceDistributed, CodeGenerationSupporter.Force);
- vResult.HintKind = OptimizerHintKind.ForceDistributedPlan;
- UpdateTokenInfo(vResult, tForceDistributed);
- UpdateTokenInfo(vResult, tPlanDistributed);
- }
+ | {NextTokenMatches(CodeGenerationSupporter.Force)}?
+ tForce:Identifier (
+ Order
+ {
+ Match(tForce, CodeGenerationSupporter.Force);
+ vResult.HintKind = OptimizerHintKind.ForceOrder;
+ UpdateTokenInfo(vResult, tForce);
+ }
+ |
+ Distributed tPlan1:Plan
+ {
+ Match(tForce, CodeGenerationSupporter.Force);
+ vResult.HintKind = OptimizerHintKind.ForceDistributedPlan;
+ UpdateTokenInfo(vResult, tForce);
+ }
+ |
+ tSingle:Identifier tNode:Identifier tPlan2:Plan
+ {
+ Match(tForce, CodeGenerationSupporter.Force);
+ Match(tSingle, CodeGenerationSupporter.Single);
+ Match(tNode, CodeGenerationSupporter.GraphNode); // NODE
+ vResult.HintKind = OptimizerHintKind.ForceSingleNodePlan;
+ UpdateTokenInfo(vResult, tForce);
+ }
+ )
| tHash:Identifier Group
{
Match(tHash, CodeGenerationSupporter.Hash);
diff --git a/SqlScriptDom/Parser/TSql/VectorIndexType.cs b/SqlScriptDom/Parser/TSql/VectorIndexType.cs
index 8f70a02..1bcd9e1 100644
--- a/SqlScriptDom/Parser/TSql/VectorIndexType.cs
+++ b/SqlScriptDom/Parser/TSql/VectorIndexType.cs
@@ -20,4 +20,5 @@ public enum VectorIndexType
#pragma warning restore 1591
}
+
\ No newline at end of file
diff --git a/SqlScriptDom/Parser/TSql/VectorMetricTypeHelper.cs b/SqlScriptDom/Parser/TSql/VectorMetricTypeHelper.cs
index 6b1479a..7043439 100644
--- a/SqlScriptDom/Parser/TSql/VectorMetricTypeHelper.cs
+++ b/SqlScriptDom/Parser/TSql/VectorMetricTypeHelper.cs
@@ -6,8 +6,6 @@
-using static Microsoft.SqlServer.TransactSql.ScriptDom.SensitivityClassification;
-
namespace Microsoft.SqlServer.TransactSql.ScriptDom
{
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OptimizerHint.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OptimizerHint.cs
index 8181667..901b5e5 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OptimizerHint.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OptimizerHint.cs
@@ -119,16 +119,7 @@ partial class SqlScriptGeneratorVisitor
new IdentifierGenerator(CodeGenerationSupporter.NoPerformanceSpool), }},
{ OptimizerHintKind.Label, new List() {
new IdentifierGenerator(CodeGenerationSupporter.Label, true),
- new KeywordGenerator(TSqlTokenType.EqualsSign) }},
- { OptimizerHintKind.ForceSingleNodePlan, new List( ) {
- new IdentifierGenerator(CodeGenerationSupporter.Force, true),
- new IdentifierGenerator(CodeGenerationSupporter.Single, true),
- new IdentifierGenerator(CodeGenerationSupporter.GraphNode, true),
- new KeywordGenerator(TSqlTokenType.Plan) }},
- { OptimizerHintKind.ForceDistributedPlan, new List( ) {
- new IdentifierGenerator(CodeGenerationSupporter.Force, true),
- new KeywordGenerator(TSqlTokenType.Distributed, true),
- new KeywordGenerator(TSqlTokenType.Plan) }}
+ new KeywordGenerator(TSqlTokenType.EqualsSign) }}
};
public override void ExplicitVisit(LiteralOptimizerHint node)
diff --git a/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml b/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml
index a7c8659..2ee4a86 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml
+++ b/SqlScriptDom/ScriptDom/SqlServer/Settings/SqlScriptGeneratorOptions.xml
@@ -49,7 +49,7 @@
IncludeSemicolons_Title
IncludeSemiColons_Description
- Gets or sets a boolean indicating if a semi colon should be included after each statement
+ DEPRECATED: This option is non-functional and always behaves as if set to true. Semicolons are always included after statements regardless of this setting. This property is retained for API compatibility only and will be removed in a future version.
Gets or sets a boolean indicating if a semicolon terminator is written after a BEGIN...END block and after the END CATCH of a BEGIN TRY...END TRY BEGIN CATCH...END CATCH block. When false (the default), these blocks are written without a terminator, preserving the previous script-generation behavior. When true, a semicolon is appended, so a terminator supplied in the source script survives a parse and generate round-trip. END TRY is never terminated because it is internal to the TRY...CATCH block.
diff --git a/Test/SqlDom/OnlyFabricDWSyntaxTests.cs b/Test/SqlDom/OnlyFabricDWSyntaxTests.cs
index 5b4d722..84a94ea 100644
--- a/Test/SqlDom/OnlyFabricDWSyntaxTests.cs
+++ b/Test/SqlDom/OnlyFabricDWSyntaxTests.cs
@@ -32,7 +32,7 @@ public partial class SqlDomTests
new ParserTestFabricDW("CreateDatabaseCollateBeforeEditionTestsFabricDW.sql", nErrors80: 3, nErrors90: 3, nErrors100: 3, nErrors110: 3, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0),
new ParserTestFabricDW("ModernGroupByAllTestsFabricDW.sql", nErrors80: 9, nErrors90: 9, nErrors100: 9, nErrors110: 9, nErrors120: 9, nErrors130: 9, nErrors140: 9, nErrors150: 9, nErrors160: 9, nErrors170: 9, nErrors180: 9),
new ParserTestFabricDW("OrderByAllTestsFabricDW.sql", nErrors80: 13, nErrors90: 13, nErrors100: 13, nErrors110: 13, nErrors120: 13, nErrors130: 13, nErrors140: 13, nErrors150: 13, nErrors160: 13, nErrors170: 13, nErrors180: 13),
- new ParserTestFabricDW("QueryHintsTestsFabricDW.sql", nErrors80: 6, nErrors90: 6, nErrors100: 6, nErrors110: 6, nErrors120: 6, nErrors130: 6, nErrors140: 6, nErrors150: 6, nErrors160: 6, nErrors170: 6, nErrors180: 6),
+ new ParserTestFabricDW("QueryHintsTestsFabricDW.sql", nErrors80: 6, nErrors90: 6, nErrors100: 6, nErrors110: 6, nErrors120: 6, nErrors130: 6, nErrors140: 6, nErrors150: 6, nErrors160: 6, nErrors170: 3, nErrors180: 3),
new ParserTestFabricDW("WindowClauseQuotedNameTests160.sql", nErrors80: 7, nErrors90: 7, nErrors100: 7, nErrors110: 7, nErrors120: 7, nErrors130: 7, nErrors140: 7, nErrors150: 7, nErrors160: 0, nErrors170: 0, nErrors180: 0),
};
diff --git a/Test/SqlDom/ParserErrorsTests.cs b/Test/SqlDom/ParserErrorsTests.cs
index b4d35f6..17988e5 100644
--- a/Test/SqlDom/ParserErrorsTests.cs
+++ b/Test/SqlDom/ParserErrorsTests.cs
@@ -7292,7 +7292,7 @@ public void QueryHintsNegativeTestsFabricDW()
// FORCE DISTRIBUTED PLAN must start with FORCE
//
string keepDistributedPlan = "SELECT c1 FROM t1 OPTION (KEEP DISTRIBUTED PLAN)";
- ParserTestUtils.ErrorTestFabricDW(keepDistributedPlan, new ParserErrorInfo(keepDistributedPlan.IndexOf("KEEP"), "SQL46005", "FORCE", "KEEP"));
+ ParserTestUtils.ErrorTestFabricDW(keepDistributedPlan, new ParserErrorInfo(keepDistributedPlan.IndexOf("KEEP"), "SQL46010", "KEEP"));
// FORCE SINGLE NODE PLAN requires the NODE keyword
//
From 9e0ac89a9e8d8ae0d5a95cbd2cc0ff13aa0c0640 Mon Sep 17 00:00:00 2001
From: Allan Cascante <120929896+acascante_microsoft@users.noreply.github.com>
Date: Mon, 12 Oct 2026 16:01:52 -0600
Subject: [PATCH 11/13] Add release notes for 180.140.0
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
release-notes/180/180.140.0.md | 33 +++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
create mode 100644 release-notes/180/180.140.0.md
diff --git a/release-notes/180/180.140.0.md b/release-notes/180/180.140.0.md
new file mode 100644
index 0000000..a0b8366
--- /dev/null
+++ b/release-notes/180/180.140.0.md
@@ -0,0 +1,33 @@
+# Release Notes
+
+## Microsoft.SqlServer.TransactSql.ScriptDom 180.140.0
+This update brings the following changes over the previous release:
+
+### Target Platform Support
+
+* .NET Framework 4.7.2 (Windows x86, Windows x64)
+* .NET 8 (Windows x86, Windows x64, Linux, macOS)
+* .NET Standard 2.0+ (Windows x86, Windows x64, Linux, macOS)
+
+### Dependencies
+* None
+
+#### .NET Framework
+#### .NET Core
+
+### New Features
+* Adds `ON ERROR` clause support (`ERROR`, `NULL`, and `DEFAULT`) for Fabric DW AI functions: `AI_ANALYZE_SENTIMENT`, `AI_CLASSIFY`, `AI_EXTRACT`, `AI_FIX_GRAMMAR`, `AI_GENERATE_RESPONSE`, `AI_SUMMARIZE`, and `AI_TRANSLATE`.
+* Adds Fabric DW join hints (`REPLICATE` and `REDISTRIBUTE`) and query hints, including `FORCE SINGLE NODE PLAN`, `FORCE DISTRIBUTED PLAN`, and `FOR TIMESTAMP AS OF`.
+
+### Fixed
+* Fixes parsing of bracketed and double-quoted named `WINDOW` references in SQL Server 2022 and later.
+* Fixes lexer handling of U+FFFF in string literals, delimited identifiers, and comments so it is not treated as end-of-input.
+* Fixes source-token ranges for `GROUP BY ALL` in the Fabric DW parser.
+* Fixes `ColumnAliasStyle.Preserve` so it retains whether a SELECT column alias used the `AS` keyword.
+* Fixes preserved comment ordering and placement around comma-separated lists, before `FROM`, and in stored procedure parameters.
+
+### Changes
+* None
+
+### Known Issues
+* None
From bc080748ef2b1dd98015976ef6dcafcd19e630c0 Mon Sep 17 00:00:00 2001
From: Allan Cascante <120929896+acascante_microsoft@users.noreply.github.com>
Date: Mon, 12 Oct 2026 16:06:32 -0600
Subject: [PATCH 12/13] Clean imported source whitespace
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
SqlScriptDom/Parser/TSql/VectorIndexType.cs | 1 -
SqlScriptDom/antlr/antlr/CharScanner.cs | 2 +-
SqlScriptDom/antlr/antlr/NoViableAltForCharException.cs | 4 ++--
3 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/SqlScriptDom/Parser/TSql/VectorIndexType.cs b/SqlScriptDom/Parser/TSql/VectorIndexType.cs
index 1bcd9e1..8f70a02 100644
--- a/SqlScriptDom/Parser/TSql/VectorIndexType.cs
+++ b/SqlScriptDom/Parser/TSql/VectorIndexType.cs
@@ -20,5 +20,4 @@ public enum VectorIndexType
#pragma warning restore 1591
}
-
\ No newline at end of file
diff --git a/SqlScriptDom/antlr/antlr/CharScanner.cs b/SqlScriptDom/antlr/antlr/CharScanner.cs
index 4132950..9957a07 100644
--- a/SqlScriptDom/antlr/antlr/CharScanner.cs
+++ b/SqlScriptDom/antlr/antlr/CharScanner.cs
@@ -409,7 +409,7 @@ public virtual void matchRange(char c1, char c2)
protected virtual void handlePrematureEof()
{
}
-
+
public virtual void newline()
{
inputState.line++;
diff --git a/SqlScriptDom/antlr/antlr/NoViableAltForCharException.cs b/SqlScriptDom/antlr/antlr/NoViableAltForCharException.cs
index d50e943..6b017bf 100644
--- a/SqlScriptDom/antlr/antlr/NoViableAltForCharException.cs
+++ b/SqlScriptDom/antlr/antlr/NoViableAltForCharException.cs
@@ -44,13 +44,13 @@ public NoViableAltForCharException(int c, CharScanner scanner) :
{
foundChar = c;
}
-
+
public NoViableAltForCharException(int c, string fileName, int line, int column) :
base("NoViableAlt", fileName, line, column)
{
foundChar = c;
}
-
+
protected NoViableAltForCharException(SerializationInfo info, StreamingContext context)
: base(info, context)
{ }
From 63a5a96d691bf6e16ca3aafe308151d1021c98f6 Mon Sep 17 00:00:00 2001
From: Allan Cascante <120929896+acascante_microsoft@users.noreply.github.com>
Date: Mon, 12 Oct 2026 16:26:42 -0600
Subject: [PATCH 13/13] Add mhyon as a code owner
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.github/CODEOWNERS | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index ae9e043..99189c1 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1 @@
-* @llali @allancascante @kburtram @aaskhan @benjind
+* @llali @allancascante @kburtram @aaskhan @benjind @mhyon