diff --git a/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs b/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs
index 7bd0e484..fff679b0 100644
--- a/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs
+++ b/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs
@@ -1,4 +1,4 @@
-//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
@@ -972,6 +972,7 @@ internal static class CodeGenerationSupporter
internal const string Signature = "SIGNATURE";
internal const string SimilarTo = "SIMILAR_TO";
internal const string Simple = "SIMPLE";
+ internal const string Single = "SINGLE";
internal const string SingleBlob = "SINGLE_BLOB";
internal const string SingleClob = "SINGLE_CLOB";
internal const string SingleNClob = "SINGLE_NCLOB";
diff --git a/SqlScriptDom/Parser/TSql/OptimizerHintKind.cs b/SqlScriptDom/Parser/TSql/OptimizerHintKind.cs
index 95b5187b..4b6673c4 100644
--- a/SqlScriptDom/Parser/TSql/OptimizerHintKind.cs
+++ b/SqlScriptDom/Parser/TSql/OptimizerHintKind.cs
@@ -59,6 +59,9 @@ public enum OptimizerHintKind
NoPerformanceSpool = 34,
Label = 35,
+ ForceSingleNodePlan = 36,
+ ForceDistributedPlan = 37,
+ ForTimestampAsOf = 38,
}
#pragma warning restore 1591
diff --git a/SqlScriptDom/Parser/TSql/TSql170.g b/SqlScriptDom/Parser/TSql/TSql170.g
index 0babede1..80dc53c0 100644
--- a/SqlScriptDom/Parser/TSql/TSql170.g
+++ b/SqlScriptDom/Parser/TSql/TSql170.g
@@ -22150,6 +22150,7 @@ hint returns [OptimizerHint vResult]
| vResult = optimizeForOptimizerHint
| vResult = tableHintsOptimizerHint
| vResult = useHintClause
+ | vResult = forTimestampAsOfOptimizerHint
;
tableHintsOptimizerHint returns [TableHintsOptimizerHint vResult = FragmentFactory.CreateFragment()]
@@ -22191,12 +22192,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);
- }
+ | {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);
@@ -22373,6 +22393,19 @@ 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;
diff --git a/SqlScriptDom/Parser/TSql/TSql180.g b/SqlScriptDom/Parser/TSql/TSql180.g
index ae426b05..9e21ebb7 100644
--- a/SqlScriptDom/Parser/TSql/TSql180.g
+++ b/SqlScriptDom/Parser/TSql/TSql180.g
@@ -22379,6 +22379,7 @@ hint returns [OptimizerHint vResult]
| vResult = optimizeForOptimizerHint
| vResult = tableHintsOptimizerHint
| vResult = useHintClause
+ | vResult = forTimestampAsOfOptimizerHint
;
tableHintsOptimizerHint returns [TableHintsOptimizerHint vResult = FragmentFactory.CreateFragment()]
@@ -22420,12 +22421,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);
- }
+ | {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);
@@ -22602,6 +22622,19 @@ 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;
diff --git a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
index 4ad40b85..ca1aa081 100644
--- a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
+++ b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g
@@ -21910,6 +21910,7 @@ hint returns [OptimizerHint vResult]
| vResult = optimizeForOptimizerHint
| vResult = tableHintsOptimizerHint
| vResult = useHintClause
+ | vResult = forTimestampAsOfOptimizerHint
;
tableHintsOptimizerHint returns [TableHintsOptimizerHint vResult = FragmentFactory.CreateFragment()]
@@ -21951,12 +21952,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);
- }
+ | {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);
@@ -22133,6 +22153,19 @@ 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;
diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OptimizerHint.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OptimizerHint.cs
index 8f61d188..92068d15 100644
--- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OptimizerHint.cs
+++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.OptimizerHint.cs
@@ -105,7 +105,16 @@ partial class SqlScriptGeneratorVisitor
new IdentifierGenerator(CodeGenerationSupporter.NoPerformanceSpool), }},
{ OptimizerHintKind.Label, new List() {
new IdentifierGenerator(CodeGenerationSupporter.Label, true),
- new KeywordGenerator(TSqlTokenType.EqualsSign) }}
+ 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) }}
};
public override void ExplicitVisit(LiteralOptimizerHint node)
@@ -122,6 +131,13 @@ public override void ExplicitVisit(LiteralOptimizerHint node)
GenerateSpaceAndKeyword(TSqlTokenType.Plan);
}
}
+ else if (node.HintKind == OptimizerHintKind.ForTimestampAsOf)
+ {
+ GenerateKeyword(TSqlTokenType.For);
+ GenerateSpaceAndIdentifier(CodeGenerationSupporter.TimeStamp);
+ GenerateSpaceAndKeyword(TSqlTokenType.As);
+ GenerateSpaceAndKeyword(TSqlTokenType.Of);
+ }
else
{
List generators = GetValueForEnumKey(_optimizerHintKindsGenerators, node.HintKind);
diff --git a/Test/SqlDom/Baselines170/FabricQueryHints170.sql b/Test/SqlDom/Baselines170/FabricQueryHints170.sql
new file mode 100644
index 00000000..fb6678eb
--- /dev/null
+++ b/Test/SqlDom/Baselines170/FabricQueryHints170.sql
@@ -0,0 +1,21 @@
+SELECT OrderDateKey,
+ SUM(SalesAmount) AS TotalSales
+FROM FactInternetSales
+GROUP BY OrderDateKey
+ORDER BY OrderDateKey
+OPTION (FOR TIMESTAMP AS OF '2024-03-13T19:39:35.28');
+
+SELECT OrderDateKey,
+ SalesAmount
+FROM FactInternetSales
+OPTION (FORCE SINGLE NODE PLAN);
+
+SELECT OrderDateKey,
+ SalesAmount
+FROM FactInternetSales
+OPTION (FORCE DISTRIBUTED PLAN);
+
+SELECT OrderDateKey,
+ SalesAmount
+FROM FactInternetSales
+OPTION (FORCE DISTRIBUTED PLAN, FOR TIMESTAMP AS OF '2024-03-13T19:39:35.28');
diff --git a/Test/SqlDom/Only170SyntaxTests.cs b/Test/SqlDom/Only170SyntaxTests.cs
index 51c95f38..3ccc8f38 100644
--- a/Test/SqlDom/Only170SyntaxTests.cs
+++ b/Test/SqlDom/Only170SyntaxTests.cs
@@ -41,6 +41,7 @@ public partial class SqlDomTests
new ParserTest170("VectorSearchCrossApplyTests170.sql", nErrors80: 1, nErrors90: 1, nErrors100: 1, nErrors110: 1, nErrors120: 1, nErrors130: 1, nErrors140: 1, nErrors150: 1, nErrors160: 1),
// Complex query with VECTOR types - parses syntactically in all versions (optimization fix), but VECTOR type only valid in TSql170
new ParserTest170("ComplexQueryTests170.sql"),
+ new ParserTest170("FabricQueryHints170.sql", nErrors80: 4, nErrors90: 4, nErrors100: 4, nErrors110: 4, nErrors120: 4, nErrors130: 4, nErrors140: 4, nErrors150: 4, nErrors160: 4),
// Comment preservation tests - basic SQL syntax works in all versions
new ParserTest170("SingleLineCommentTests170.sql"),
new ParserTest170("MultiLineCommentTests170.sql")
diff --git a/Test/SqlDom/ParserErrorsTests.cs b/Test/SqlDom/ParserErrorsTests.cs
index faeb5a68..281c366e 100644
--- a/Test/SqlDom/ParserErrorsTests.cs
+++ b/Test/SqlDom/ParserErrorsTests.cs
@@ -8202,5 +8202,35 @@ public void TopWithTiesAndApproximateError180()
"SELECT TOP 10 WITH TIES WITH APPROXIMATE * FROM MyTable ORDER BY id;",
new ParserErrorInfo(24, "SQL46010", "WITH"));
}
+
+ ///
+ /// Negative test for Fabric Query Hints - syntax error (TSql170)
+ /// Verifies malformed hints produce expected parser errors
+ ///
+ [TestMethod]
+ [Priority(0)]
+ [SqlStudioTestCategory(Category.UnitTest)]
+ public void FabricQueryHintsErrorTest170()
+ {
+ // Missing NODE keyword in FORCE SINGLE NODE PLAN
+ ParserTestUtils.ErrorTest170(
+ "SELECT OrderDateKey FROM FactInternetSales OPTION (FORCE SINGLE PLAN);",
+ new ParserErrorInfo(64, "SQL46010", "PLAN"));
+
+ // Missing PLAN keyword in FORCE DISTRIBUTED PLAN
+ ParserTestUtils.ErrorTest170(
+ "SELECT OrderDateKey FROM FactInternetSales OPTION (FORCE DISTRIBUTED);",
+ new ParserErrorInfo(68, "SQL46010", ")"));
+
+ // Non-string literal in FOR TIMESTAMP AS OF
+ ParserTestUtils.ErrorTest170(
+ "SELECT OrderDateKey FROM FactInternetSales OPTION (FOR TIMESTAMP AS OF 12345);",
+ new ParserErrorInfo(71, "SQL46010", "12345"));
+
+ // Missing OF keyword in FOR TIMESTAMP AS OF
+ ParserTestUtils.ErrorTest170(
+ "SELECT OrderDateKey FROM FactInternetSales OPTION (FOR TIMESTAMP AS '2024-03-13T19:39:35.28');",
+ new ParserErrorInfo(68, "SQL46010", "'2024-03-13T19:39:35.28'"));
+ }
}
}
diff --git a/Test/SqlDom/TestScripts/FabricQueryHints170.sql b/Test/SqlDom/TestScripts/FabricQueryHints170.sql
new file mode 100644
index 00000000..222a0116
--- /dev/null
+++ b/Test/SqlDom/TestScripts/FabricQueryHints170.sql
@@ -0,0 +1,17 @@
+SELECT OrderDateKey, SUM(SalesAmount) AS TotalSales
+FROM FactInternetSales
+GROUP BY OrderDateKey
+ORDER BY OrderDateKey
+OPTION (FOR TIMESTAMP AS OF '2024-03-13T19:39:35.28');
+
+SELECT OrderDateKey, SalesAmount
+FROM FactInternetSales
+OPTION (FORCE SINGLE NODE PLAN);
+
+SELECT OrderDateKey, SalesAmount
+FROM FactInternetSales
+OPTION (FORCE DISTRIBUTED PLAN);
+
+SELECT OrderDateKey, SalesAmount
+FROM FactInternetSales
+OPTION (FORCE DISTRIBUTED PLAN, FOR TIMESTAMP AS OF '2024-03-13T19:39:35.28');