diff --git a/.github/instructions/testing.guidelines.instructions.md b/.github/instructions/testing.guidelines.instructions.md index ec62811..5b1b477 100644 --- a/.github/instructions/testing.guidelines.instructions.md +++ b/.github/instructions/testing.guidelines.instructions.md @@ -321,6 +321,25 @@ Expected: 'SELECT JSON_ARRAY('value1', 'value2');' **Solution**: Copy the "Actual" output to your baseline file (note spacing differences). +**CRITICAL CHECK**: After copying baseline, compare it against your test script: +```sql +-- Input script (TestScripts/MyTest.sql) +SELECT * FROM FUNC() WITH (HINT); + +-- Generated baseline (Baselines170/MyTest.sql) +SELECT * +FROM FUNC(); +-- ⚠️ WHERE IS 'WITH (HINT)'? +``` + +**If baseline is missing syntax from input:** +1. **This is likely a BUG** - not just formatting difference +2. Check if AST has member to store the missing syntax +3. Verify grammar stores value: `vResult.PropertyName = vValue;` +4. Check script generator outputs the value: `GenerateFragmentIfNotNull(node.Property)` +5. If syntax should be preserved, add AST storage and script generation +6. Document in spec if intentional omission (e.g., query optimizer hints) + #### 2. Error Count Mismatch ``` TestYourFeature.sql: number of errors after parsing is different from expected. @@ -800,6 +819,38 @@ new ParserTest160("RegressionBugFix12345Tests160.sql", nErrors150: 1), // Bug e new ParserTest170("RegressionBugFix12345Tests170.sql", nErrors160: 1), // Bug existed in SQL 2022 ``` +## Round-Trip Fidelity Validation Checklist + +**CRITICAL: After generating baseline files, ALWAYS verify:** + +✅ **Input Preservation Check**: +1. Open test script side-by-side with baseline file +2. For each SQL statement, verify baseline preserves all syntax from input +3. Check optional clauses: `WITH`, `WHERE`, `HAVING`, `ORDER BY`, etc. +4. Check hints: Table hints, query hints, join hints +5. Check keywords: All keywords from input should appear in baseline (unless documented normalization) + +✅ **Missing Syntax Investigation**: +If baseline omits syntax from input: +- [ ] Is this intentional keyword normalization? (e.g., APPROX → APPROXIMATE) +- [ ] Is this a query optimizer hint that doesn't need preservation? +- [ ] Is this a BUG where AST doesn't store the value? + +✅ **Bug Indicators**: +- ❌ Input: `FUNCTION() WITH (HINT)` → Baseline: `FUNCTION()` = **LIKELY BUG** +- ❌ Input: `SELECT ... ORDER BY col` → Baseline: `SELECT ...` = **BUG** +- ✅ Input: `FETCH APPROX` → Baseline: `FETCH APPROXIMATE` = Acceptable normalization +- ✅ Input: `SELECT /*+ HINT */` → Baseline: `SELECT` = Query hint (document in spec) + +✅ **Resolution Steps**: +1. Check AST definition in `Ast.xml` for member to store value +2. Verify grammar assigns value: `vResult.Property = vValue;` +3. Check script generator outputs value: `if (node.Property != null) { ... }` +4. If missing: Add AST member, update grammar, update script generator, rebuild +5. Document decision in spec if intentional omission + +--- + ## Summary The SqlScriptDOM testing framework provides comprehensive validation of parser functionality through: @@ -808,7 +859,11 @@ The SqlScriptDOM testing framework provides comprehensive validation of parser f - **Cross-version validation** (Test syntax across SQL Server versions) - **Error condition testing** (Invalid syntax produces expected errors) - **Exact syntax verification** (Exact T-SQL from user requests is tested precisely) +- **Round-trip fidelity validation** (Baseline preserves all input syntax unless documented) Following these guidelines ensures robust test coverage for parser functionality and prevents regressions when adding new features or fixing bugs. -**Key Principle**: Always test the exact T-SQL syntax provided in user prompts or requests to verify that the specific syntax works as expected, rather than testing generalized or simplified versions of the syntax. \ No newline at end of file +**Key Principles**: +1. Always test the exact T-SQL syntax provided in user prompts or requests +2. Always verify baseline output preserves input syntax (missing syntax may indicate bugs) +3. Document any intentional omissions (normalization, query hints) in spec \ No newline at end of file diff --git a/.gitignore b/.gitignore index 997c172..adb0b54 100644 --- a/.gitignore +++ b/.gitignore @@ -359,4 +359,15 @@ out/ .packages/ # Temporary build artifacts -tmp/ \ No newline at end of file +tmp/ + +# Speckit files +speckit.files +.github/.specify/ +.github/agents/speckit.*.agent.md +.github/prompts/speckit.* + +# Specs directory - ignore all files except spec.md +specs/**/* +!specs/**/ +!specs/**/spec.md diff --git a/SqlScriptDom/Parser/TSql/Ast.xml b/SqlScriptDom/Parser/TSql/Ast.xml index d17d34e..1976cb1 100644 --- a/SqlScriptDom/Parser/TSql/Ast.xml +++ b/SqlScriptDom/Parser/TSql/Ast.xml @@ -4808,5 +4808,6 @@ + diff --git a/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs b/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs index 4f7e5be..4c8a97d 100644 --- a/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs +++ b/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs @@ -75,6 +75,7 @@ internal static class CodeGenerationSupporter internal const string AnsiWarnings = "ANSI_WARNINGS"; internal const string ForcePlan = "FORCEPLAN"; internal const string ForAppend = "FOR_APPEND"; + internal const string ForceAnnOnly = "FORCE_ANN_ONLY"; internal const string ShowPlanAll = "SHOWPLAN_ALL"; internal const string ShowPlanText = "SHOWPLAN_TEXT"; internal const string IO = "IO"; diff --git a/SqlScriptDom/Parser/TSql/TSql170.g b/SqlScriptDom/Parser/TSql/TSql170.g index 85ec9cf..a8a1b49 100644 --- a/SqlScriptDom/Parser/TSql/TSql170.g +++ b/SqlScriptDom/Parser/TSql/TSql170.g @@ -19321,19 +19321,35 @@ vectorSearchTableReference returns [VectorSearchTableReference vResult = Fragmen MatchString(vMetric, CodeGenerationSupporter.Cosine, CodeGenerationSupporter.Dot, CodeGenerationSupporter.Euclidean); vResult.Metric = vMetric; } - Comma tTopN:Identifier EqualsSign vTopN = signedIntegerOrVariableOrColumnReference - { - Match(tTopN, CodeGenerationSupporter.TopN); - - // Validate that TOP_N is not a negative number - if (vTopN is UnaryExpression unaryExpr && unaryExpr.UnaryExpressionType == UnaryExpressionType.Negative) + // TOP_N is optional per SQL Server 2025 (commit 12d3e8fc) + ( + Comma tTopN:Identifier EqualsSign vTopN = signedIntegerOrVariableOrColumnReference { - ThrowParseErrorException("SQL46010", unaryExpr, TSqlParserResource.SQL46010Message, "-"); + Match(tTopN, CodeGenerationSupporter.TopN); + + // Validate that TOP_N is not a negative number + if (vTopN is UnaryExpression unaryExpr && unaryExpr.UnaryExpressionType == UnaryExpressionType.Negative) + { + ThrowParseErrorException("SQL46010", unaryExpr, TSqlParserResource.SQL46010Message, "-"); + } + + vResult.TopN = vTopN; } - - vResult.TopN = vTopN; + )? + tRParen:RightParenthesis + { + UpdateTokenInfo(vResult, tRParen); } - RightParenthesis simpleTableReferenceAliasOpt[vResult] + // WITH clause per SQL Server 2025 (commit 12d3e8fc) + ( + With LeftParenthesis tForceAnnOnly:Identifier tRParen2:RightParenthesis + { + Match(tForceAnnOnly, CodeGenerationSupporter.ForceAnnOnly); + UpdateTokenInfo(vResult, tRParen2); + vResult.ForceAnnOnly = true; + } + )? + simpleTableReferenceAliasOpt[vResult] ; predictTableReference[SubDmlFlags subDmlFlags] returns [PredictTableReference vResult] @@ -33167,6 +33183,7 @@ jsonObjectBuiltInFunctionCall [FunctionCall vParent] jsonObjectAggBuiltInFunctionCall [FunctionCall vParent] { + OverClause vOverClause; } : ( jsonObjectAggExpressionList[vParent] @@ -33177,6 +33194,12 @@ jsonObjectAggBuiltInFunctionCall [FunctionCall vParent] { UpdateTokenInfo(vParent, tRParen); } + ( + vOverClause=overClauseNoOrderBy + { + vParent.OverClause = vOverClause; + } + )? ; jsonQueryBuiltInFunctionCall [FunctionCall vParent] diff --git a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g index 77019ed..21fd33d 100644 --- a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g +++ b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g @@ -23761,8 +23761,6 @@ dropSecurityPolicyStatement returns [DropSecurityPolicyStatement vResult = Fragm createExternalDataSourceStatement returns [CreateExternalDataSourceStatement vResult = FragmentFactory.CreateFragment()] { Identifier vName; - ExternalDataSourceOption vExternalDataSourceOption; - long encounteredOptions = 0; vResult.DataSourceType = ExternalDataSourceType.EXTERNAL_GENERICS; } : tData:Identifier tSource:Identifier vName = identifier @@ -23774,42 +23772,8 @@ createExternalDataSourceStatement returns [CreateExternalDataSourceStatement vRe } tWith:With LeftParenthesis - ( - {NextTokenMatches(CodeGenerationSupporter.Type)}? - externalDataSourceType[vResult] - | - {NextTokenMatches(CodeGenerationSupporter.Location)}? - externalDataSourceLocation[vResult] - | - {NextTokenMatches(CodeGenerationSupporter.PushdownOption)}? - externalDataSourcePushdownOption[vResult] - | - vExternalDataSourceOption = externalDataSourceLiteralOrIdentifierOption - { - CheckOptionDuplication(ref encounteredOptions, (int)vExternalDataSourceOption.OptionKind, vExternalDataSourceOption); - AddAndUpdateTokenInfo(vResult, vResult.ExternalDataSourceOptions, vExternalDataSourceOption); - } - ) - ( - tComma:Comma - ( - {NextTokenMatches(CodeGenerationSupporter.Type)}? - externalDataSourceType[vResult] - | - {NextTokenMatches(CodeGenerationSupporter.Location)}? - externalDataSourceLocation[vResult] - | - {NextTokenMatches(CodeGenerationSupporter.PushdownOption)}? - externalDataSourcePushdownOption[vResult] - | - vExternalDataSourceOption = externalDataSourceLiteralOrIdentifierOption - { - CheckOptionDuplication(ref encounteredOptions, (int)vExternalDataSourceOption.OptionKind, vExternalDataSourceOption); - AddAndUpdateTokenInfo(vResult, vResult.ExternalDataSourceOptions, vExternalDataSourceOption); - } - ) - )* + externalDataSourceLocation[vResult] tRParen:RightParenthesis { @@ -23817,45 +23781,6 @@ createExternalDataSourceStatement returns [CreateExternalDataSourceStatement vRe } ; -externalDataSourceType[CreateExternalDataSourceStatement vParent] - : - tDataSourceType:Identifier EqualsSign - { - Match(tDataSourceType, CodeGenerationSupporter.Type); - UpdateTokenInfo(vParent, tDataSourceType); - } - ( - tExternalDataSourceType:Identifier - { - if (TryMatch(tExternalDataSourceType, CodeGenerationSupporter.Hadoop)) - { - UpdateTokenInfo(vParent, tExternalDataSourceType); - vParent.DataSourceType = ExternalDataSourceType.HADOOP; - } - else if (TryMatch(tExternalDataSourceType, CodeGenerationSupporter.Rdbms)) - { - UpdateTokenInfo(vParent, tExternalDataSourceType); - vParent.DataSourceType = ExternalDataSourceType.RDBMS; - } - else if (TryMatch(tExternalDataSourceType, CodeGenerationSupporter.ShardMapManager)) - { - UpdateTokenInfo(vParent, tExternalDataSourceType); - vParent.DataSourceType = ExternalDataSourceType.SHARD_MAP_MANAGER; - } - else if (TryMatch(tExternalDataSourceType, CodeGenerationSupporter.BlobStorage)) - { - UpdateTokenInfo(vParent, tExternalDataSourceType); - vParent.DataSourceType = ExternalDataSourceType.BLOB_STORAGE; - } - else - { - UpdateTokenInfo(vParent, tExternalDataSourceType); - vParent.DataSourceType = ExternalDataSourceType.EXTERNAL_GENERICS; - } - } - ) - ; - externalDataSourceLocation[ExternalDataSourceStatement vParent] { Literal vLocation; @@ -23868,68 +23793,9 @@ externalDataSourceLocation[ExternalDataSourceStatement vParent] } ; -externalDataSourcePushdownOption[ExternalDataSourceStatement vParent] - : - tPushdownOption:Identifier - { - Match(tPushdownOption, CodeGenerationSupporter.PushdownOption); - UpdateTokenInfo(vParent, tPushdownOption); - } - EqualsSign - ( - tOn:On - { - vParent.PushdownOption = ExternalDataSourcePushdownOption.ON; - UpdateTokenInfo(vParent, tOn); - } - | tOff:Off - { - vParent.PushdownOption = ExternalDataSourcePushdownOption.OFF; - UpdateTokenInfo(vParent, tOff); - } - ) - ; - -externalDataSourceLiteralOrIdentifierOption returns [ExternalDataSourceLiteralOrIdentifierOption vResult = this.FragmentFactory.CreateFragment()] -{ - Literal vLiteral; - Identifier vIdentifier; -} - : - tOption:Identifier - { - vResult.OptionKind = ExternalDataSourceOptionHelper.Instance.ParseOption(tOption); - } - EqualsSign - ( - vIdentifier = identifier - { - if (vResult.OptionKind != ExternalDataSourceOptionKind.Credential) - { - throw GetUnexpectedTokenErrorException(tOption); - } - vResult.Value = IdentifierOrValueExpression(vIdentifier); - } - | - vLiteral = stringLiteral - { - if (vResult.OptionKind != ExternalDataSourceOptionKind.ResourceManagerLocation && - vResult.OptionKind != ExternalDataSourceOptionKind.DatabaseName && - vResult.OptionKind != ExternalDataSourceOptionKind.ShardMapName && - vResult.OptionKind != ExternalDataSourceOptionKind.ConnectionOptions) - { - throw GetUnexpectedTokenErrorException(tOption); - } - vResult.Value = IdentifierOrValueExpression(vLiteral); - } - ) - ; - alterExternalDataSourceStatement returns [AlterExternalDataSourceStatement vResult = FragmentFactory.CreateFragment()] { Identifier vName; - ExternalDataSourceOption vExternalDataSourceOption; - long encounteredOptions = 0; } : tData:Identifier tSource:Identifier vName = identifier { @@ -23940,36 +23806,8 @@ alterExternalDataSourceStatement returns [AlterExternalDataSourceStatement vResu } tSet:Set - ( - {NextTokenMatches(CodeGenerationSupporter.Location)}? - externalDataSourceLocation[vResult] - | - {NextTokenMatches(CodeGenerationSupporter.PushdownOption)}? - externalDataSourcePushdownOption[vResult] - | - vExternalDataSourceOption = externalDataSourceLiteralOrIdentifierOption - { - CheckOptionDuplication(ref encounteredOptions, (int)vExternalDataSourceOption.OptionKind, vExternalDataSourceOption); - AddAndUpdateTokenInfo(vResult, vResult.ExternalDataSourceOptions, vExternalDataSourceOption); - } - ) - ( - tComma:Comma - ( - {NextTokenMatches(CodeGenerationSupporter.Location)}? - externalDataSourceLocation[vResult] - | - {NextTokenMatches(CodeGenerationSupporter.PushdownOption)}? - externalDataSourcePushdownOption[vResult] - | - vExternalDataSourceOption = externalDataSourceLiteralOrIdentifierOption - { - CheckOptionDuplication(ref encounteredOptions, (int)vExternalDataSourceOption.OptionKind, vExternalDataSourceOption); - AddAndUpdateTokenInfo(vResult, vResult.ExternalDataSourceOptions, vExternalDataSourceOption); - } - ) - )* + externalDataSourceLocation[vResult] ; dropExternalDataSourceStatement returns [DropExternalDataSourceStatement vResult = FragmentFactory.CreateFragment()] diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FunctionCall.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FunctionCall.cs index 70d7111..ffa5099 100644 --- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FunctionCall.cs +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FunctionCall.cs @@ -70,6 +70,8 @@ public override void ExplicitVisit(FunctionCall node) GenerateSpace(); GenerateReturnType(node?.ReturnType); GenerateSymbol(TSqlTokenType.RightParenthesis); + // Generate OVER clause for windowed json_objectagg + GenerateSpaceAndFragmentIfNotNull(node.OverClause); } else if (node.FunctionName.Value.ToUpper(CultureInfo.InvariantCulture) == CodeGenerationSupporter.JsonArray) { diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.VectorSearchTableReference.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.VectorSearchTableReference.cs index 55e4730..09b9aa4 100644 --- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.VectorSearchTableReference.cs +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.VectorSearchTableReference.cs @@ -15,9 +15,9 @@ partial class SqlScriptGeneratorVisitor /// TABLE = object[AS source_table_alias], /// COLUMN = vector_column, /// SIMILAR_TO = query_vector, - /// METRIC = { 'cosine' | 'dot' | 'euclidean' }, - /// TOP_N = k - /// ) [AS result_table_alias] + /// METRIC = { 'cosine' | 'dot' | 'euclidean' } + /// [, TOP_N = k] + /// ) [WITH (FORCE_ANN_ONLY)] [AS result_table_alias] /// public override void ExplicitVisit(VectorSearchTableReference node) { @@ -41,13 +41,28 @@ public override void ExplicitVisit(VectorSearchTableReference node) NewLineAndIndent(); GenerateNameEqualsValue(CodeGenerationSupporter.Metric, node.Metric); - GenerateSymbol(TSqlTokenType.Comma); - - NewLineAndIndent(); - GenerateNameEqualsValue(CodeGenerationSupporter.TopN, node.TopN); + + // TOP_N is optional per SQL Server 2025 (commit 12d3e8fc) + if (node.TopN != null) + { + GenerateSymbol(TSqlTokenType.Comma); + NewLineAndIndent(); + GenerateNameEqualsValue(CodeGenerationSupporter.TopN, node.TopN); + } NewLine(); GenerateSymbol(TSqlTokenType.RightParenthesis); + + // WITH (FORCE_ANN_ONLY) hint per SQL Server 2025 (commit 12d3e8fc) + if (node.ForceAnnOnly) + { + GenerateSpaceAndKeyword(TSqlTokenType.With); + GenerateSpace(); + GenerateSymbol(TSqlTokenType.LeftParenthesis); + GenerateIdentifier(CodeGenerationSupporter.ForceAnnOnly); + GenerateSymbol(TSqlTokenType.RightParenthesis); + } + GenerateSpaceAndAlias(node.Alias); PopAlignmentPoint(); diff --git a/Test/SqlDom/Baselines170/JsonObjectAggOverClause170.sql b/Test/SqlDom/Baselines170/JsonObjectAggOverClause170.sql new file mode 100644 index 0000000..8aa7b2e --- /dev/null +++ b/Test/SqlDom/Baselines170/JsonObjectAggOverClause170.sql @@ -0,0 +1,11 @@ +SELECT JSON_OBJECTAGG(CustomerName:OrderDate) OVER (PARTITION BY OrderDate) +FROM Customers; + +SELECT JSON_OBJECTAGG(CustomerName:OrderDate ABSENT ON NULL) OVER (PARTITION BY OrderDate) +FROM Customers; + +SELECT JSON_OBJECTAGG(CustomerName:OrderDate NULL ON NULL) OVER (PARTITION BY OrderDate) +FROM Customers; + +SELECT JSON_OBJECTAGG(CustomerName:OrderDate NULL ON NULL RETURNING JSON) OVER (PARTITION BY OrderDate) +FROM Customers; \ No newline at end of file diff --git a/Test/SqlDom/Baselines170/VectorSearchOptionalTopNTests170.sql b/Test/SqlDom/Baselines170/VectorSearchOptionalTopNTests170.sql new file mode 100644 index 0000000..6d31f39 --- /dev/null +++ b/Test/SqlDom/Baselines170/VectorSearchOptionalTopNTests170.sql @@ -0,0 +1,33 @@ +SELECT * +FROM VECTOR_SEARCH( + TABLE = graphnode, + COLUMN = embedding, + SIMILAR_TO = @qembedding, + METRIC = 'euclidean' + ); + +SELECT * +FROM VECTOR_SEARCH( + TABLE = graphnode, + COLUMN = embedding, + SIMILAR_TO = @qembedding, + METRIC = 'euclidean' + ) WITH (FORCE_ANN_ONLY); + +SELECT * +FROM VECTOR_SEARCH( + TABLE = graphnode, + COLUMN = embedding, + SIMILAR_TO = @qembedding, + METRIC = 'euclidean', + TOP_N = 20 + ); + +SELECT * +FROM VECTOR_SEARCH( + TABLE = graphnode, + COLUMN = embedding, + SIMILAR_TO = @qembedding, + METRIC = 'euclidean', + TOP_N = 20 + ) WITH (FORCE_ANN_ONLY); diff --git a/Test/SqlDom/BaselinesFabricDW/CreateAlterExternalDataSourceTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/CreateAlterExternalDataSourceTestsFabricDW.sql new file mode 100644 index 0000000..fb3aef4 --- /dev/null +++ b/Test/SqlDom/BaselinesFabricDW/CreateAlterExternalDataSourceTestsFabricDW.sql @@ -0,0 +1,11 @@ +CREATE EXTERNAL DATA SOURCE MyDataSource + WITH ( + LOCATION = 'sqlserver://10.10.10.10:1433' + ); + +CREATE EXTERNAL DATA SOURCE BlobSource + WITH ( + LOCATION = 'wasbs://container@account.blob.core.windows.net' + ); + +ALTER EXTERNAL DATA SOURCE MyDataSource SET LOCATION = 'sqlserver://192.168.1.100:1433'; \ No newline at end of file diff --git a/Test/SqlDom/Only170SyntaxTests.cs b/Test/SqlDom/Only170SyntaxTests.cs index a881c8e..12478fd 100644 --- a/Test/SqlDom/Only170SyntaxTests.cs +++ b/Test/SqlDom/Only170SyntaxTests.cs @@ -15,12 +15,14 @@ public partial class SqlDomTests new ParserTest170("RegexpTVFTests170.sql", nErrors80: 1, nErrors90: 1, nErrors100: 0, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0), new ParserTest170("JsonIndexTests170.sql", nErrors80: 2, nErrors90: 10, nErrors100: 10, nErrors110: 10, nErrors120: 10, nErrors130: 10, nErrors140: 10, nErrors150: 10, nErrors160: 10), new ParserTest170("VectorIndexTests170.sql", nErrors80: 2, nErrors90: 12, nErrors100: 12, nErrors110: 12, nErrors120: 12, nErrors130: 12, nErrors140: 12, nErrors150: 12, nErrors160: 12), + new ParserTest170("VectorSearchOptionalTopNTests170.sql"), new ParserTest170("AlterDatabaseManualCutoverTests170.sql", nErrors80: 4, nErrors90: 4, nErrors100: 4, nErrors110: 4, nErrors120: 4, nErrors130: 4, nErrors140: 4, nErrors150: 4, nErrors160: 4), new ParserTest170("CreateColumnStoreIndexTests170.sql", nErrors80: 3, nErrors90: 3, nErrors100: 3, nErrors110: 3, nErrors120: 3, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0), new ParserTest170("RegexpTests170.sql", nErrors80: 0, nErrors90: 0, nErrors100: 0, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0), new ParserTest170("AiGenerateChunksTests170.sql", nErrors80: 19, nErrors90: 16, nErrors100: 15, nErrors110: 15, nErrors120: 15, nErrors130: 15, nErrors140: 15, nErrors150: 15, nErrors160: 15), new ParserTest170("JsonFunctionTests170.sql", nErrors80: 29, nErrors90: 8, nErrors100: 54, nErrors110: 54, nErrors120: 54, nErrors130: 54, nErrors140: 54, nErrors150: 54, nErrors160: 54), new ParserTest170("JsonArrayAggOrderBy170.sql", nErrors80: 10, nErrors90: 9, nErrors100: 9, nErrors110: 9, nErrors120: 9, nErrors130: 9, nErrors140: 9, nErrors150: 9, nErrors160: 9), + 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("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), diff --git a/Test/SqlDom/OnlyFabricDWSyntaxTests.cs b/Test/SqlDom/OnlyFabricDWSyntaxTests.cs index 5bee5e2..a2eaab8 100644 --- a/Test/SqlDom/OnlyFabricDWSyntaxTests.cs +++ b/Test/SqlDom/OnlyFabricDWSyntaxTests.cs @@ -10,6 +10,7 @@ public partial class SqlDomTests private static readonly ParserTest[] OnlyFabricDWTestInfos = { new ParserTestFabricDW("CloneTableTestsFabricDW.sql", nErrors80: 4, nErrors90: 4, nErrors100: 4, nErrors110: 4, nErrors120: 4, nErrors130: 4, nErrors140: 4, nErrors150: 4, nErrors160: 4, nErrors170: 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), new ParserTestFabricDW("CreateAlterTableClusterByTestsFabricDW.sql", nErrors80: 6, nErrors90: 6, nErrors100: 6, nErrors110: 6, nErrors120: 6, nErrors130: 6, nErrors140: 6, nErrors150: 6, nErrors160: 6, nErrors170: 6), new ParserTestFabricDW("CreateExternalTableStatementTestsFabricDW.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 2, nErrors140: 2, nErrors150: 2, nErrors160: 0, nErrors170: 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), @@ -23,7 +24,7 @@ public partial class SqlDomTests new ParserTestFabricDW("AiFixGrammarTestsFabricDW.sql", nErrors80: 2, nErrors90: 1, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 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), new ParserTestFabricDW("AiSummarizeTestsFabricDW.sql", nErrors80: 2, nErrors90: 1, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 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), + new ParserTestFabricDW("AiTranslateTestsFabricDW.sql", nErrors80: 3, nErrors90: 1, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0) }; [TestMethod] diff --git a/Test/SqlDom/ParserErrorsTests.cs b/Test/SqlDom/ParserErrorsTests.cs index 5a89afe..34b0a92 100644 --- a/Test/SqlDom/ParserErrorsTests.cs +++ b/Test/SqlDom/ParserErrorsTests.cs @@ -5236,6 +5236,30 @@ public void CreateExternalDataSourceNegativeTest() ParserTestUtils.ErrorTest130("CREATE EXTERNAL DATA SOURCE eds1 WITH (TYPE = RDBMS, LOCATION = someServer, DATABASE_NAME = 'someDatabase', CREDENTIAL = someCred)", new ParserErrorInfo(64, "SQL46010", "someServer")); ParserTestUtils.ErrorTest130("CREATE EXTERNAL DATA SOURCE eds1 WITH (TYPE = RDBMS, LOCATION = 'someServer', DATABASE_NAME = someDatabase, CREDENTIAL = someCred)", new ParserErrorInfo(78, "SQL46010", "DATABASE_NAME")); ParserTestUtils.ErrorTest130("CREATE EXTERNAL DATA SOURCE eds1 WITH (TYPE = RDBMS, LOCATION = 'someServer', DATABASE_NAME = 'someDatabase', CREDENTIAL = 'someCred')", new ParserErrorInfo(110, "SQL46010", "CREDENTIAL")); + + // CREATE with TYPE parameter (no longer supported — parser expects LOCATION) + // + ParserTestUtils.ErrorTestFabricDW("CREATE EXTERNAL DATA SOURCE eds1 WITH (TYPE = HADOOP, LOCATION = 'protocol://ip_address:port')", new ParserErrorInfo(46, "SQL46010", "HADOOP")); + + // CREATE with extra CREDENTIAL option after LOCATION (only LOCATION allowed, no additional options) + // + ParserTestUtils.ErrorTestFabricDW("CREATE EXTERNAL DATA SOURCE eds1 WITH (LOCATION = 'protocol://ip_address:port', CREDENTIAL = cred1)", new ParserErrorInfo(78, "SQL46010", ",")); + + // CREATE with extra PUSHDOWN option after LOCATION + // + ParserTestUtils.ErrorTestFabricDW("CREATE EXTERNAL DATA SOURCE eds1 WITH (LOCATION = 'protocol://ip_address:port', PUSHDOWN = ON)", new ParserErrorInfo(78, "SQL46010", ",")); + + // CREATE with extra RESOURCE_MANAGER_LOCATION option after LOCATION + // + ParserTestUtils.ErrorTestFabricDW("CREATE EXTERNAL DATA SOURCE eds1 WITH (LOCATION = 'protocol://ip_address:port', RESOURCE_MANAGER_LOCATION = 'ip_address:port')", new ParserErrorInfo(78, "SQL46010", ",")); + + // CREATE with extra DATABASE_NAME option after LOCATION + // + ParserTestUtils.ErrorTestFabricDW("CREATE EXTERNAL DATA SOURCE eds1 WITH (LOCATION = 'someServer', DATABASE_NAME = 'someDatabase')", new ParserErrorInfo(62, "SQL46010", ",")); + + // CREATE with extra CONNECTION_OPTIONS after LOCATION + // + ParserTestUtils.ErrorTestFabricDW("CREATE EXTERNAL DATA SOURCE eds1 WITH (LOCATION = 'protocol://ip_address:port', CONNECTION_OPTIONS = 'some_options')", new ParserErrorInfo(78, "SQL46010", ",")); } @@ -5295,6 +5319,24 @@ public void AlterExternalDataSourceNegativeTest() ParserTestUtils.ErrorTest130("ALTER EXTERNAL DATA SOURCE eds1 SET LOCATION = 'someServer', DATABASE_NAME = someDatabase, SHARD_MAP_NAME = 'someShardMap', CREDENTIAL = someCred", new ParserErrorInfo(61, "SQL46010", "DATABASE_NAME")); ParserTestUtils.ErrorTest130("ALTER EXTERNAL DATA SOURCE eds1 SET LOCATION = 'someServer', DATABASE_NAME = 'someDatabase', SHARD_MAP_NAME = someShardMap, CREDENTIAL = someCred", new ParserErrorInfo(93, "SQL46010", "SHARD_MAP_NAME")); ParserTestUtils.ErrorTest130("ALTER EXTERNAL DATA SOURCE eds1 SET LOCATION = 'someServer', DATABASE_NAME = 'someDatabase', SHARD_MAP_NAME = 'someShardMap', CREDENTIAL = 'someCred'", new ParserErrorInfo(126, "SQL46010", "CREDENTIAL")); + + // ALTER with unsupported option instead of LOCATION + // + ParserTestUtils.ErrorTestFabricDW("ALTER EXTERNAL DATA SOURCE eds1 SET RESOURCE_MANAGER_LOCATION = 'ip_address:port'", new ParserErrorInfo(36, "SQL46005", "LOCATION", "RESOURCE_MANAGER_LOCATION")); + ParserTestUtils.ErrorTestFabricDW("ALTER EXTERNAL DATA SOURCE eds1 SET DATABASE_NAME = 'someDb'", new ParserErrorInfo(36, "SQL46005", "LOCATION", "DATABASE_NAME")); + ParserTestUtils.ErrorTestFabricDW("ALTER EXTERNAL DATA SOURCE eds1 SET CONNECTION_OPTIONS = 'some_options'", new ParserErrorInfo(36, "SQL46005", "LOCATION", "CONNECTION_OPTIONS")); + ParserTestUtils.ErrorTestFabricDW("ALTER EXTERNAL DATA SOURCE eds1 SET SHARD_MAP_NAME = 'someShardMap'", new ParserErrorInfo(36, "SQL46005", "LOCATION", "SHARD_MAP_NAME")); + + // ALTER with unsupported option where the value is not a string literal + // + ParserTestUtils.ErrorTestFabricDW("ALTER EXTERNAL DATA SOURCE eds1 SET PUSHDOWN = ON", new ParserErrorInfo(47, "SQL46010", "ON")); + ParserTestUtils.ErrorTestFabricDW("ALTER EXTERNAL DATA SOURCE eds1 SET CREDENTIAL = cred1", new ParserErrorInfo(49, "SQL46010", "cred1")); + + // ALTER with LOCATION followed by extra unsupported options (comma not expected after LOCATION value) + // + ParserTestUtils.ErrorTestFabricDW("ALTER EXTERNAL DATA SOURCE eds1 SET LOCATION = 'sqlserver://10.10.10.10:1433', PUSHDOWN = ON", new ParserErrorInfo(77, "SQL46010", ",")); + ParserTestUtils.ErrorTestFabricDW("ALTER EXTERNAL DATA SOURCE eds1 SET LOCATION = 'sqlserver://10.10.10.10:1433', CREDENTIAL = cred1", new ParserErrorInfo(77, "SQL46010", ",")); + ParserTestUtils.ErrorTestFabricDW("ALTER EXTERNAL DATA SOURCE eds1 SET LOCATION = 'sqlserver://10.10.10.10:1433', DATABASE_NAME = 'someDb'", new ParserErrorInfo(77, "SQL46010", ",")); } @@ -7059,6 +7101,7 @@ RETURNS NVARCHAR(101) RETURN @first + ' ' + @last END;"; ParserTestUtils.ErrorTestFabricDW(scalarFunctionSyntax2, new ParserErrorInfo(scalarFunctionSyntax2.IndexOf("INLINE"), "SQL46010", "INLINE")); + string scalarFunctionSyntax3 = @"CREATE OR ALTER FUNCTION dbo.CountProducts ( @ProductTable AS dbo.ProductType READONLY @@ -7097,11 +7140,22 @@ FROM @SalesData [SqlStudioTestCategory(Category.UnitTest)] public void IdentityColumnNegativeTestsFabricDW() { - string identityColumnSyntax = @"CREATE TABLE TestTable1 (ID INT IDENTITY(1,1), Name VARCHAR(50));"; - ParserTestUtils.ErrorTestFabricDW(identityColumnSyntax, new ParserErrorInfo(40, "SQL46010", "(")); - - string identityColumnSyntax2 = @"CREATE TABLE TestTable2 (RecordID BIGINT IDENTITY(100,5), Description NVARCHAR(200));"; - ParserTestUtils.ErrorTestFabricDW(identityColumnSyntax2, new ParserErrorInfo(49, "SQL46010", "(")); + string identityColumnSyntax = @"CREATE TABLE TestTable1 ( + ID INT IDENTITY(1,1), + Name VARCHAR(50) + ); + "; + string token = "IDENTITY"; + int errorOffSet = identityColumnSyntax.IndexOf(token) + token.Length; + ParserTestUtils.ErrorTestFabricDW(identityColumnSyntax, new ParserErrorInfo(errorOffSet, "SQL46010", "(")); + + string identityColumnSyntax2 = @"CREATE TABLE TestTable2 ( + RecordID BIGINT IDENTITY(100,5), + Description NVARCHAR(200) + ); + "; + errorOffSet = identityColumnSyntax2.IndexOf(token) + token.Length; + ParserTestUtils.ErrorTestFabricDW(identityColumnSyntax2, new ParserErrorInfo(errorOffSet, "SQL46010", "(")); } /// @@ -7445,10 +7499,9 @@ public void VectorSearchErrorTest170() "SELECT * FROM VECTOR_SEARCH(TABLE = tbl1, COLUMN = col1, SIMILAR_TO = query_vector)", new ParserErrorInfo(82, "SQL46010", ")")); - // Missing required parameters: TOP_N - ParserTestUtils.ErrorTest170( - "SELECT * FROM VECTOR_SEARCH(TABLE = tbl1, COLUMN = col1, SIMILAR_TO = query_vector, METRIC = 'dot')", - new ParserErrorInfo(98, "SQL46010", ")")); + // TOP_N is now OPTIONAL per SQL Server 2025 (commit 12d3e8fc) + // The following test case has been removed as it tested for TOP_N being mandatory + // which is no longer the case. VECTOR_SEARCH now accepts queries without TOP_N. // Invalid order: COLUMN before TABLE ParserTestUtils.ErrorTest170( diff --git a/Test/SqlDom/TestScripts/CreateAlterExternalDataSourceTestsFabricDW.sql b/Test/SqlDom/TestScripts/CreateAlterExternalDataSourceTestsFabricDW.sql new file mode 100644 index 0000000..fb3aef4 --- /dev/null +++ b/Test/SqlDom/TestScripts/CreateAlterExternalDataSourceTestsFabricDW.sql @@ -0,0 +1,11 @@ +CREATE EXTERNAL DATA SOURCE MyDataSource + WITH ( + LOCATION = 'sqlserver://10.10.10.10:1433' + ); + +CREATE EXTERNAL DATA SOURCE BlobSource + WITH ( + LOCATION = 'wasbs://container@account.blob.core.windows.net' + ); + +ALTER EXTERNAL DATA SOURCE MyDataSource SET LOCATION = 'sqlserver://192.168.1.100:1433'; \ No newline at end of file diff --git a/Test/SqlDom/TestScripts/JsonObjectAggOverClause170.sql b/Test/SqlDom/TestScripts/JsonObjectAggOverClause170.sql new file mode 100644 index 0000000..16edf83 --- /dev/null +++ b/Test/SqlDom/TestScripts/JsonObjectAggOverClause170.sql @@ -0,0 +1,11 @@ +-- JSON_OBJECTAGG with OVER clause (PARTITION BY) +SELECT JSON_OBJECTAGG(CustomerName:OrderDate) OVER (PARTITION BY OrderDate) FROM Customers; + +-- JSON_OBJECTAGG with ABSENT ON NULL and OVER clause +SELECT JSON_OBJECTAGG(CustomerName:OrderDate ABSENT ON NULL) OVER (PARTITION BY OrderDate) FROM Customers; + +-- JSON_OBJECTAGG with NULL ON NULL and OVER clause +SELECT JSON_OBJECTAGG(CustomerName:OrderDate NULL ON NULL) OVER (PARTITION BY OrderDate) FROM Customers; + +-- JSON_OBJECTAGG with NULL ON NULL, RETURNING JSON, and OVER clause +SELECT JSON_OBJECTAGG(CustomerName:OrderDate NULL ON NULL RETURNING JSON) OVER (PARTITION BY OrderDate) FROM Customers; \ No newline at end of file diff --git a/Test/SqlDom/TestScripts/VectorSearchOptionalTopNTests170.sql b/Test/SqlDom/TestScripts/VectorSearchOptionalTopNTests170.sql new file mode 100644 index 0000000..e6ebb71 --- /dev/null +++ b/Test/SqlDom/TestScripts/VectorSearchOptionalTopNTests170.sql @@ -0,0 +1,33 @@ +-- Test 1: VECTOR_SEARCH without TOP_N (validates optional parameter) +SELECT * FROM VECTOR_SEARCH( + TABLE = graphnode, + COLUMN = embedding, + SIMILAR_TO = @qembedding, + METRIC = 'euclidean' +); + +-- Test 2: VECTOR_SEARCH without TOP_N + WITH (FORCE_ANN_ONLY) (validates both changes) +SELECT * FROM VECTOR_SEARCH( + TABLE = graphnode, + COLUMN = embedding, + SIMILAR_TO = @qembedding, + METRIC = 'euclidean' +) WITH (FORCE_ANN_ONLY); + +-- Test 3: VECTOR_SEARCH with TOP_N (validates backward compatibility) +SELECT * FROM VECTOR_SEARCH( + TABLE = graphnode, + COLUMN = embedding, + SIMILAR_TO = @qembedding, + METRIC = 'euclidean', + TOP_N = 20 +); + +-- Test 4: VECTOR_SEARCH with TOP_N + WITH (FORCE_ANN_ONLY) (validates both features together) +SELECT * FROM VECTOR_SEARCH( + TABLE = graphnode, + COLUMN = embedding, + SIMILAR_TO = @qembedding, + METRIC = 'euclidean', + TOP_N = 20 +) WITH (FORCE_ANN_ONLY); diff --git a/global.json b/global.json index b2e90b5..f64b7fb 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.415", + "version": "8.0.418", "rollForward": "latestMajor" }, "msbuild-sdks": { diff --git a/release-notes/170/170.179.0.md b/release-notes/170/170.179.0.md new file mode 100644 index 0000000..42788f0 --- /dev/null +++ b/release-notes/170/170.179.0.md @@ -0,0 +1,28 @@ +# Release Notes + +## Microsoft.SqlServer.TransactSql.ScriptDom 170.179.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 +* Updates .NET SDK to latest patch version 8.0.418 + +#### .NET Framework +#### .NET Core + +### New Features +* Adds JSON_OBJECTAGG support for OVER clause (windowed aggregate usage). +* Simplifies CREATE/ALTER EXTERNAL DATA SOURCE syntax for Fabric DW to only support the LOCATION parameter, removes support for TYPE, PUSHDOWN, and other literal/identifier options in the Fabric DW parser. + +### Fixed +* Fixes VECTOR_SEARCH TOP_N parameter to be optional. +* Fixes VECTOR_SEARCH to support WITH (FORCE_ANN_ONLY) query hint. + +### Changes + +### Known Issues