From 225dcdfd138b319ef23878061d05e971d983be05 Mon Sep 17 00:00:00 2001 From: Naveen Date: Sat, 6 Jun 2026 00:54:49 +0530 Subject: [PATCH 1/2] Add support for Query Hints for MS Fabric in TSql170Parser (#180) --- .../Parser/TSql/CodeGenerationSupporter.cs | 3 +- SqlScriptDom/Parser/TSql/OptimizerHintKind.cs | 3 ++ SqlScriptDom/Parser/TSql/TSql170.g | 45 ++++++++++++++++--- SqlScriptDom/Parser/TSql/TSql180.g | 45 ++++++++++++++++--- SqlScriptDom/Parser/TSql/TSqlFabricDW.g | 45 ++++++++++++++++--- ...SqlScriptGeneratorVisitor.OptimizerHint.cs | 18 +++++++- .../Baselines170/FabricQueryHints170.sql | 21 +++++++++ Test/SqlDom/Only170SyntaxTests.cs | 1 + .../TestScripts/FabricQueryHints170.sql | 17 +++++++ 9 files changed, 178 insertions(+), 20 deletions(-) create mode 100644 Test/SqlDom/Baselines170/FabricQueryHints170.sql create mode 100644 Test/SqlDom/TestScripts/FabricQueryHints170.sql 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..7ebb2c0e 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"), // Comment preservation tests - basic SQL syntax works in all versions new ParserTest170("SingleLineCommentTests170.sql"), new ParserTest170("MultiLineCommentTests170.sql") 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'); From b845162065c2914142f067d8baddc26f151e15da Mon Sep 17 00:00:00 2001 From: Naveen Date: Tue, 21 Jul 2026 01:22:00 +0530 Subject: [PATCH 2/2] Add negative parser error tests and version-gating assertions for MS Fabric query hints --- Test/SqlDom/Only170SyntaxTests.cs | 2 +- Test/SqlDom/ParserErrorsTests.cs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Test/SqlDom/Only170SyntaxTests.cs b/Test/SqlDom/Only170SyntaxTests.cs index 7ebb2c0e..3ccc8f38 100644 --- a/Test/SqlDom/Only170SyntaxTests.cs +++ b/Test/SqlDom/Only170SyntaxTests.cs @@ -41,7 +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"), + 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'")); + } } }