diff --git a/.github/instructions/bug_fixing.guidelines.instructions.md b/.github/instructions/bug_fixing.guidelines.instructions.md index 79715fa..b1a6793 100644 --- a/.github/instructions/bug_fixing.guidelines.instructions.md +++ b/.github/instructions/bug_fixing.guidelines.instructions.md @@ -90,6 +90,27 @@ The process of fixing a bug, especially one that involves adding new syntax, fol By following these steps, you can ensure that new syntax is correctly parsed, represented in the AST, generated back into a script, and fully validated by the testing framework without breaking existing functionality. +## Testing Best Practices + +### ✅ DO: Use Existing Test Framework +- Add test methods to existing test classes like `Only170SyntaxTests.cs` +- Use the established `TSqlParser.Parse()` pattern for verification +- Example: + ```csharp + [TestMethod] + public void VerifyNewSyntax() + { + var parser = new TSql170Parser(true); + var result = parser.Parse(new StringReader("YOUR SQL HERE"), out var errors); + Assert.AreEqual(0, errors.Count, "Should parse without errors"); + } + ``` + +### ❌ DON'T: Create New Test Projects +- **Never** create standalone `.csproj` files for testing parser functionality +- **Never** create new console applications or test runners +- This causes build issues and doesn't integrate with the existing test infrastructure + ## Special Case: Extending Grammar Rules from Literals to Expressions A common type of bug involves extending existing grammar rules that only accept literal values (like integers or strings) to accept full expressions (parameters, variables, outer references, etc.). This pattern was used to fix the VECTOR_SEARCH TOP_N parameter issue. diff --git a/.github/instructions/grammer.guidelines.instructions.md b/.github/instructions/grammer.guidelines.instructions.md index 900026f..53311f7 100644 --- a/.github/instructions/grammer.guidelines.instructions.md +++ b/.github/instructions/grammer.guidelines.instructions.md @@ -64,17 +64,32 @@ yourContextParameterRule returns [ScalarExpression vResult] Most script generators using `GenerateNameEqualsValue()` or similar methods work automatically with `ScalarExpression`. No changes typically needed. #### Step 4: Add Test Coverage -```sql --- Test parameter -FUNCTION_NAME(PARAM = @parameter) - --- Test outer reference -FUNCTION_NAME(PARAM = outerref.column) - --- Test computed expression -FUNCTION_NAME(PARAM = value + 1) +Add tests within the existing test framework: +```csharp +[TestMethod] +public void VerifyGrammarExtension() +{ + var parser = new TSql170Parser(true); + + // Test parameter + var sql1 = "SELECT FUNCTION_NAME(PARAM = @parameter)"; + var result1 = parser.Parse(new StringReader(sql1), out var errors1); + Assert.AreEqual(0, errors1.Count, "Parameter syntax should work"); + + // Test outer reference + var sql2 = "SELECT FUNCTION_NAME(PARAM = outerref.column)"; + var result2 = parser.Parse(new StringReader(sql2), out var errors2); + Assert.AreEqual(0, errors2.Count, "Outer reference syntax should work"); + + // Test computed expression + var sql3 = "SELECT FUNCTION_NAME(PARAM = value + 1)"; + var result3 = parser.Parse(new StringReader(sql3), out var errors3); + Assert.AreEqual(0, errors3.Count, "Computed expression syntax should work"); +} ``` +**⚠️ CRITICAL**: Add this test method to an existing test class (e.g., `Only170SyntaxTests.cs`). **Never create standalone test projects.** + ### Real-World Example: VECTOR_SEARCH TOP_N **Problem**: `VECTOR_SEARCH` TOP_N parameter only accepted integer literals. diff --git a/.github/instructions/parser.guidelines.instructions.md b/.github/instructions/parser.guidelines.instructions.md index acd96d6..20da2e6 100644 --- a/.github/instructions/parser.guidelines.instructions.md +++ b/.github/instructions/parser.guidelines.instructions.md @@ -59,11 +59,21 @@ case TSql80ParserInternal.Identifier: ## Step-by-Step Fix Process ### 1. Reproduce the Issue -Create a test case to confirm the bug: -```sql -SELECT 1 WHERE (REGEXP_LIKE('a', 'pattern')); -- Should fail without fix +Create a test case within the existing test framework to confirm the bug: +```csharp +[TestMethod] +public void ReproduceParenthesesIssue() +{ + var parser = new TSql170Parser(true); + var sql = "SELECT 1 WHERE (REGEXP_LIKE('a', 'pattern'));"; + var result = parser.Parse(new StringReader(sql), out var errors); + // Should fail before fix, pass after fix + Assert.AreEqual(0, errors.Count, "Should parse without errors after fix"); +} ``` +**⚠️ IMPORTANT**: Add this test to an existing test class like `Only170SyntaxTests.cs`, **do not** create a new test project. + ### 2. Identify the Predicate Constant Find the predicate identifier in `CodeGenerationSupporter`: ```csharp diff --git a/.github/instructions/testing.guidelines.instructions.md b/.github/instructions/testing.guidelines.instructions.md index 1cd1705..ec62811 100644 --- a/.github/instructions/testing.guidelines.instructions.md +++ b/.github/instructions/testing.guidelines.instructions.md @@ -13,6 +13,43 @@ The SqlScriptDOM testing framework validates parser functionality through: 4. **Version-Specific Testing** - Tests syntax across multiple SQL Server versions (SQL 2000-2025) 5. **Exact T-SQL Verification** - When testing specific T-SQL syntax from prompts or user requests, the **exact T-SQL statement must be included and verified** in the test to ensure the specific syntax works as expected +## Quick Verification Tests + +For rapid verification and debugging, add simple test methods directly to existing test classes: + +```csharp +[TestMethod] +[Priority(0)] +[SqlStudioTestCategory(Category.UnitTest)] +public void VerifyMyNewSyntax() +{ + var parser = new TSql170Parser(true); + + // Test basic syntax + var query1 = "SELECT YOUR_NEW_FUNCTION('param1', 'param2');"; + var result1 = parser.Parse(new StringReader(query1), out var errors1); + Assert.AreEqual(0, errors1.Count, "Basic syntax should parse"); + + // Test complex variations + var query2 = "SELECT YOUR_NEW_FUNCTION(@variable);"; + var result2 = parser.Parse(new StringReader(query2), out var errors2); + Assert.AreEqual(0, errors2.Count, "Variable syntax should parse"); + + Console.WriteLine("✅ All tests passed!"); +} +``` + +**Where to Add Quick Tests:** +- **SQL Server 2025 (170) features**: Add to `Test/SqlDom/Only170SyntaxTests.cs` +- **SQL Server 2022 (160) features**: Add to `Test/SqlDom/Only160SyntaxTests.cs` +- **Earlier versions**: Add to corresponding `OnlySyntaxTests.cs` + +**When to Use:** +- Quick verification during development +- Debugging parser issues +- Initial syntax validation before full test suite +- Rapid prototyping of test cases + ## Test Framework Architecture ### Core Components @@ -29,6 +66,30 @@ The SqlScriptDOM testing framework validates parser functionality through: 3. **Validate Phase**: Generated output is compared against baseline file 4. **Error Validation**: Parse error count is compared against expected error count for each SQL version +## ❌ Anti-Patterns: What NOT to Do + +### Do NOT Create New Test Projects + +- ❌ **Don't create new `.csproj` files for testing** +- ❌ **Don't create console applications** like `TestVectorParser.csproj` or `debug_complex.csproj` +- ❌ **Don't create standalone test runners** +- ❌ **Don't add new projects to the solution for testing** + +### Why This Causes Problems + +1. **Build Issues**: New projects often fail to build due to missing dependencies +2. **Integration Problems**: Standalone projects don't integrate with existing test infrastructure +3. **Maintenance Overhead**: Additional projects require separate maintenance and documentation +4. **CI/CD Conflicts**: Build pipelines aren't configured for ad-hoc test projects +5. **Resource Waste**: Creates duplicate testing infrastructure instead of using established patterns + +### The Correct Approach + +✅ **Always add test methods to existing test classes**: +- Add to `Test/SqlDom/Only170SyntaxTests.cs` for SQL Server 2025 features +- Add to `Test/SqlDom/Only160SyntaxTests.cs` for SQL Server 2022 features +- Use the established test framework patterns documented in this guide + ## Adding New Tests ### 1. Create Test Script @@ -639,6 +700,52 @@ new ParserTest170("ErrorConditionTests170.sql", nErrors160: 3, nErrors170: 3), ``` +## Real-World Example: VECTOR Parsing Verification + +This example shows the correct approach used to verify VECTOR data type parsing functionality: + +```csharp +[TestMethod] +[Priority(0)] +[SqlStudioTestCategory(Category.UnitTest)] +public void VerifyComplexQueryFix() +{ + // Test VECTOR parsing in various contexts - this is the real bug we found and fixed + var parser = new TSql170Parser(true); + + // Test 1: Basic VECTOR with base type + var query1 = "SELECT CAST('[1,2,3]' AS VECTOR(3, Float32));"; + var result1 = parser.Parse(new StringReader(query1), out var errors1); + Assert.AreEqual(0, errors1.Count, "Basic VECTOR with base type should parse"); + + // Test 2: VECTOR in complex CAST (from original failing query) + var query2 = "SELECT CAST('[-6.464173E+08,1.040823E+07,1.699169E+08]' AS VECTOR(3, Float32));"; + var result2 = parser.Parse(new StringReader(query2), out var errors2); + Assert.AreEqual(0, errors2.Count, "VECTOR with scientific notation should parse"); + + // Test 3: VECTOR in CONVERT (from original failing query) + var query3 = "SELECT CONVERT(VECTOR(77), '[-7.230808E+08,4.075427E+08]');"; + var result3 = parser.Parse(new StringReader(query3), out var errors3); + Assert.AreEqual(0, errors3.Count, "VECTOR in CONVERT should parse"); + + // Test 4: VECTOR in JOIN context (simplified version of original complex query) + var query4 = @"SELECT t1.id + FROM table1 t1 + INNER JOIN table2 t2 ON t1.vector_col = CAST('[1,2,3]' AS VECTOR(3, Float32));"; + var result4 = parser.Parse(new StringReader(query4), out var errors4); + Assert.AreEqual(0, errors4.Count, "VECTOR in JOIN condition should parse"); + + Console.WriteLine("✅ All VECTOR parsing tests passed - the original VECTOR bug is fixed!"); +} +``` + +**Key Points from This Example:** +1. Test was added directly to `Only170SyntaxTests.cs` - no new project created +2. Tests multiple contexts where the syntax appears (CAST, CONVERT, JOIN) +3. Uses inline assertions with descriptive messages +4. References the original bug being fixed +5. Provides immediate feedback via Console.WriteLine + ## Advanced Testing Patterns ### Multi-File Tests diff --git a/SqlScriptDom/Parser/TSql/Ast.xml b/SqlScriptDom/Parser/TSql/Ast.xml index 4e0e182..d17d34e 100644 --- a/SqlScriptDom/Parser/TSql/Ast.xml +++ b/SqlScriptDom/Parser/TSql/Ast.xml @@ -4769,6 +4769,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs b/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs index 6d285f5..4f7e5be 100644 --- a/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs +++ b/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs @@ -101,6 +101,13 @@ internal static class CodeGenerationSupporter internal const string Aggregate = "AGGREGATE"; internal const string AiGenerateChunks = "AI_GENERATE_CHUNKS"; internal const string AIGenerateEmbeddings = "AI_GENERATE_EMBEDDINGS"; + internal const string AIAnalyzeSentiment = "AI_ANALYZE_SENTIMENT"; + internal const string AIClassify = "AI_CLASSIFY"; + internal const string AIExtract = "AI_EXTRACT"; + internal const string AIFixGrammar = "AI_FIX_GRAMMAR"; + internal const string AIGenerateResponse = "AI_GENERATE_RESPONSE"; + internal const string AISummarize = "AI_SUMMARIZE"; + internal const string AITranslate = "AI_TRANSLATE"; internal const string Algorithm = "ALGORITHM"; internal const string AlterColumn = "ALTERCOLUMN"; internal const string All = "ALL"; diff --git a/SqlScriptDom/Parser/TSql/TSql170.g b/SqlScriptDom/Parser/TSql/TSql170.g index 27f3f61..b793bf5 100644 --- a/SqlScriptDom/Parser/TSql/TSql170.g +++ b/SqlScriptDom/Parser/TSql/TSql170.g @@ -19142,9 +19142,21 @@ joinElement[SubDmlFlags subDmlFlags, ref TableReference vResult] ; selectTableReferenceElement [SubDmlFlags subDmlFlags] returns [TableReference vResult = null] +{ + IToken tAfterJoinParenthesis = null; +} : + // Apply SaveGuessing optimization ONLY when VECTOR keyword is detected in lookahead + // This fixes VECTOR parsing in deeply nested JOINs without breaking other valid SQL patterns + {ContainsVectorInLookahead()}? + ({ if (!SkipGuessing(tAfterJoinParenthesis)) }: + (joinParenthesis[subDmlFlags])=> ({ SaveGuessing(out tAfterJoinParenthesis); }:))=> + ({ if (!SkipGuessing(tAfterJoinParenthesis)) }: + vResult=joinParenthesis[subDmlFlags]) + | + // Standard syntactic predicate for all other cases (joinParenthesis[subDmlFlags])=> - vResult=joinParenthesis[subDmlFlags] + vResult=joinParenthesis[subDmlFlags] | vResult=selectTableReferenceElementWithoutJoinParenthesis[subDmlFlags] ; diff --git a/SqlScriptDom/Parser/TSql/TSql170ParserBaseInternal.cs b/SqlScriptDom/Parser/TSql/TSql170ParserBaseInternal.cs index 2ca8412..9b5146b 100644 --- a/SqlScriptDom/Parser/TSql/TSql170ParserBaseInternal.cs +++ b/SqlScriptDom/Parser/TSql/TSql170ParserBaseInternal.cs @@ -70,5 +70,44 @@ protected SecurityObjectKind ParseSecurityObjectKindTSql170(Identifier identifie return TSql160ParserBaseInternal.ParseSecurityObjectKind(identifier1, identifier2); } } + + /// + /// Checks if VECTOR keyword appears in the upcoming tokens within a reasonable lookahead window. + /// Used to determine if SaveGuessing optimization is needed for VECTOR data type parsing. + /// + /// true if VECTOR keyword found in lookahead; false otherwise + protected bool ContainsVectorInLookahead() + { + // Scan ahead looking for VECTOR keyword (case-insensitive identifier match) + + const int LookaheadLimit = 100; // Define a named constant for the lookahead limit + // We scan up to LookaheadLimit tokens to handle deeply nested JOIN structures with VECTOR types + for (int i = 1; i <= LookaheadLimit; i++) + { + IToken token; + try + { + token = LT(i); + } + catch (Exception ex) + { + Debug.WriteLine($"Error accessing token at lookahead index {i}: {ex.Message}"); + break; + } + if (token == null || token.Type == Token.EOF_TYPE) + { + break; + } + + // Check if this is an identifier token with text "VECTOR" + if (token.Type == TSql170ParserInternal.Identifier && + string.Equals(token.getText(), CodeGenerationSupporter.Vector, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } } } diff --git a/SqlScriptDom/Parser/TSql/TSql80ParserBaseInternal.cs b/SqlScriptDom/Parser/TSql/TSql80ParserBaseInternal.cs index 87201d0..a17fe59 100644 --- a/SqlScriptDom/Parser/TSql/TSql80ParserBaseInternal.cs +++ b/SqlScriptDom/Parser/TSql/TSql80ParserBaseInternal.cs @@ -796,22 +796,35 @@ protected bool IsNextRuleBooleanParenthesis() int caseDepth = 0; // 0 means there was no select int topmostSelect = 0; - int insideIIf = 0; + // Stack to track paren levels where IIF calls started + // This allows proper handling of multiple boolean operators inside IIF + Stack iifParenLevels = new Stack(); + bool pendingIIf = false; for (bool loop = true; loop == true; consume()) { + // Check if the previous token was IIF and this token is its opening parenthesis + // This must be done before resetting pendingIIf, as we need to consume the flag + bool isIIfOpeningParen = pendingIIf && LA(1) == TSql80ParserInternal.LeftParenthesis; + + // Reset pendingIIf at start of each iteration - it will only be set to true + // if this token is the IIF identifier. This ensures IIF must be immediately + // followed by ( to be recognized as a function call. + pendingIIf = false; + switch (LA(1)) { case TSql80ParserInternal.Identifier: // if identifier is IIF if(NextTokenMatches(CodeGenerationSupporter.IIf)) { - ++insideIIf; + // Mark that we're expecting IIF's opening parenthesis next + pendingIIf = true; } // if identifier is REGEXP_LIKE else if(NextTokenMatches(CodeGenerationSupporter.RegexpLike)) { - if (caseDepth == 0 && topmostSelect == 0 && insideIIf == 0) + if (caseDepth == 0 && topmostSelect == 0 && iifParenLevels.Count == 0) { matches = true; loop = false; @@ -820,6 +833,11 @@ protected bool IsNextRuleBooleanParenthesis() break; case TSql80ParserInternal.LeftParenthesis: ++openParens; + if (isIIfOpeningParen) + { + // Record the paren level where IIF started + iifParenLevels.Push(openParens); + } break; case TSql80ParserInternal.RightParenthesis: if (openParens == topmostSelect) @@ -827,6 +845,12 @@ protected bool IsNextRuleBooleanParenthesis() topmostSelect = 0; } + // Check if we're closing an IIF's parenthesis + if (iifParenLevels.Count > 0 && iifParenLevels.Peek() == openParens) + { + iifParenLevels.Pop(); + } + --openParens; if (openParens == 0) { @@ -856,18 +880,13 @@ protected bool IsNextRuleBooleanParenthesis() case TSql80ParserInternal.Exists: case TSql80ParserInternal.TSEqual: case TSql80ParserInternal.Update: - if (caseDepth == 0 && topmostSelect == 0 && insideIIf == 0) + if (caseDepth == 0 && topmostSelect == 0 && iifParenLevels.Count == 0) { // The number of open paranthesis are not important. - // Unless inside an iff + // Unless inside an IIF (tracked by paren level stack) matches = true; loop = false; } - else if (insideIIf > 0) - { - // Found the operator inside IIF - --insideIIf; - } break; case TSql80ParserInternal.Case: ++caseDepth; diff --git a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g index 8eb4d05..77019ed 100644 --- a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g +++ b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g @@ -31629,6 +31629,27 @@ expressionPrimary [ExpressionFlags expressionFlags] returns [PrimaryExpression v | {NextTokenMatches(CodeGenerationSupporter.JsonArray) && (LA(2) == LeftParenthesis)}? vResult=jsonArrayCall + | + {NextTokenMatches(CodeGenerationSupporter.AIAnalyzeSentiment) && (LA(2) == LeftParenthesis)}? + vResult=aiAnalyzeSentimentFunctionCall + | + {NextTokenMatches(CodeGenerationSupporter.AIClassify) && (LA(2) == LeftParenthesis)}? + vResult=aiClassifyFunctionCall + | + {NextTokenMatches(CodeGenerationSupporter.AIExtract) && (LA(2) == LeftParenthesis)}? + vResult=aiExtractFunctionCall + | + {NextTokenMatches(CodeGenerationSupporter.AIFixGrammar) && (LA(2) == LeftParenthesis)}? + vResult=aiFixGrammarFunctionCall + | + {NextTokenMatches(CodeGenerationSupporter.AIGenerateResponse) && (LA(2) == LeftParenthesis)}? + vResult=aiGenerateResponseFunctionCall + | + {NextTokenMatches(CodeGenerationSupporter.AISummarize) && (LA(2) == LeftParenthesis)}? + vResult=aiSummarizeFunctionCall + | + {NextTokenMatches(CodeGenerationSupporter.AITranslate) && (LA(2) == LeftParenthesis)}? + vResult=aiTranslateFunctionCall | (Identifier LeftParenthesis)=> vResult=builtInFunctionCall @@ -32267,6 +32288,182 @@ withinGroupClause returns [WithinGroupClause vResult = FragmentFactory.CreateFra } ; +// AI_ANALYZE_SENTIMENT() +aiAnalyzeSentimentFunctionCall returns [AIAnalyzeSentimentFunctionCall vResult = this.FragmentFactory.CreateFragment()] +{ + ScalarExpression vInput; +} + : tFunc:Identifier LeftParenthesis + { + Match(tFunc, CodeGenerationSupporter.AIAnalyzeSentiment); + UpdateTokenInfo(vResult, tFunc); + } + vInput = expression + { + vResult.Input = vInput; + } + RightParenthesis + ; + +// AI_CLASSIFY(, [, ... up to 21]) +// 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; +} + : tFunc:Identifier LeftParenthesis + { + Match(tFunc, CodeGenerationSupporter.AIClassify); + UpdateTokenInfo(vResult, tFunc); + } + // input: any expression + vExpr = expression + { + vResult.Input = vExpr; + } + // first label (required) + Comma vExpr = expression + { + AddAndUpdateTokenInfo(vResult, vResult.Labels, vExpr); + labelsCount = 1; + } + // additional labels up to 21 total + ( + Comma vExpr = expression + { + ++labelsCount; + if (labelsCount > 21) + {ThrowParseErrorException("SQL46010", vExpr, TSqlParserResource.SQL46010Message, CodeGenerationSupporter.AIClassify); + } + AddAndUpdateTokenInfo(vResult, vResult.Labels, vExpr); + } + )* + tRParen:RightParenthesis + { + UpdateTokenInfo(vResult, tRParen); + } + ; + +// AI_EXTRACT(, [, ... up to 21]) +// 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; +} + : tFunc:Identifier LeftParenthesis + { + Match(tFunc, CodeGenerationSupporter.AIExtract); + UpdateTokenInfo(vResult, tFunc); + } + // input: any expression + vExpr = expression + { + vResult.Input = vExpr; + } + // first label (required) + Comma vExpr = expression + { + AddAndUpdateTokenInfo(vResult, vResult.Labels, vExpr); + labelsCount = 1; + } + // additional labels up to 21 total + ( + Comma vExpr = expression + { + ++labelsCount; + if (labelsCount > 21) + {ThrowParseErrorException("SQL46010", vExpr, TSqlParserResource.SQL46010Message, CodeGenerationSupporter.AIExtract); + } + AddAndUpdateTokenInfo(vResult, vResult.Labels, vExpr); + } + )* + tRParen:RightParenthesis + { + UpdateTokenInfo(vResult, tRParen); + } + ; + +// AI_FIX_GRAMMAR() +aiFixGrammarFunctionCall returns [AIFixGrammarFunctionCall vResult = this.FragmentFactory.CreateFragment()] +{ + ScalarExpression vInput; +} + : tFunc:Identifier LeftParenthesis + { + Match(tFunc, CodeGenerationSupporter.AIFixGrammar); + UpdateTokenInfo(vResult, tFunc); + } + vInput = expression + { + vResult.Input = vInput; + } + RightParenthesis + ; + +// AI_GENERATE_RESPONSE([, ]) +aiGenerateResponseFunctionCall returns [AIGenerateResponseFunctionCall vResult = this.FragmentFactory.CreateFragment()] +{ + ScalarExpression vPart1; + ScalarExpression vPart2; +} + : tFunc:Identifier LeftParenthesis + { + Match(tFunc, CodeGenerationSupporter.AIGenerateResponse); + UpdateTokenInfo(vResult, tFunc); + } + vPart1 = expression + { + vResult.PromptPart1 = vPart1; + } + ( + Comma vPart2 = expression + { + vResult.PromptPart2 = vPart2; + } + )? + RightParenthesis + ; + +// AI_SUMMARIZE() +aiSummarizeFunctionCall returns [AISummarizeFunctionCall vResult = this.FragmentFactory.CreateFragment()] +{ + ScalarExpression vInput; +} + : tFunc:Identifier LeftParenthesis + { + Match(tFunc, CodeGenerationSupporter.AISummarize); + UpdateTokenInfo(vResult, tFunc); + } + vInput = expression + { + vResult.Input = vInput; + } + RightParenthesis + ; + +// AI_TRANSLATE(, ) +aiTranslateFunctionCall returns [AITranslateFunctionCall vResult = this.FragmentFactory.CreateFragment()] +{ + ScalarExpression vInput; + ScalarExpression vLang; +} + : tFunc:Identifier LeftParenthesis + { + Match(tFunc, CodeGenerationSupporter.AITranslate); + UpdateTokenInfo(vResult, tFunc); + } + vInput = expression + { + vResult.Input = vInput; + } + Comma vLang = expression + { + vResult.Language = vLang; + } + RightParenthesis + ; // TODO, olegr: Add more checks for allowed functions here - there are quite some in SQL Server parser builtInFunctionCall returns [FunctionCall vResult = FragmentFactory.CreateFragment()] diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiAnalyzeSentimentFunction.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiAnalyzeSentimentFunction.cs new file mode 100644 index 0000000..181c775 --- /dev/null +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiAnalyzeSentimentFunction.cs @@ -0,0 +1,25 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ +using Microsoft.SqlServer.TransactSql.ScriptDom; + +namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator +{ + partial class SqlScriptGeneratorVisitor + { + /// + /// Emits an AI_ANALYZE_SENTIMENT function call like + /// AI_ANALYZE_SENTIMENT(input), where input is an expression or identifier. + /// + /// Expression node to generate + public override void ExplicitVisit(AIAnalyzeSentimentFunctionCall node) + { + GenerateIdentifier(CodeGenerationSupporter.AIAnalyzeSentiment); + GenerateSymbol(TSqlTokenType.LeftParenthesis); + GenerateFragmentIfNotNull(node.Input); + GenerateSymbol(TSqlTokenType.RightParenthesis); + } + } +} diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiClassifyFunction.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiClassifyFunction.cs new file mode 100644 index 0000000..9baf443 --- /dev/null +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiClassifyFunction.cs @@ -0,0 +1,41 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ +using Microsoft.SqlServer.TransactSql.ScriptDom; + +namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator +{ + partial class SqlScriptGeneratorVisitor + { + /// + /// Emits an AI_CLASSIFY function call like + /// AI_CLASSIFY(input, label1, label2 [, ...]), + /// where input is an expression or identifier, + /// and labels are expressions or identifiers representing classification labels. + /// + /// Expression node to generate + public override void ExplicitVisit(AIClassifyFunctionCall node) + { + GenerateIdentifier(CodeGenerationSupporter.AIClassify); + GenerateSymbol(TSqlTokenType.LeftParenthesis); + + // input + GenerateFragmentIfNotNull(node.Input); + + // labels (require at least two via grammar) + if (node.Labels != null) + { + for (int i = 0; i < node.Labels.Count; i++) + { + GenerateSymbol(TSqlTokenType.Comma); + GenerateSpace(); + GenerateFragmentIfNotNull(node.Labels[i]); + } + } + + GenerateSymbol(TSqlTokenType.RightParenthesis); + } + } +} diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiExtractFunction.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiExtractFunction.cs new file mode 100644 index 0000000..0eb0fbb --- /dev/null +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiExtractFunction.cs @@ -0,0 +1,42 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ +using Microsoft.SqlServer.TransactSql.ScriptDom; + +namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator +{ + partial class SqlScriptGeneratorVisitor + { + /// + /// Emits an AI_EXTRACT function call like + /// AI_EXTRACT(input, label1 [, ...]), + /// where input is an expression or identifier, + /// and labels are expressions or identifiers + /// representing extractible entities. + /// + /// Expression node to generate + public override void ExplicitVisit(AIExtractFunctionCall node) + { + GenerateIdentifier(CodeGenerationSupporter.AIExtract); + GenerateSymbol(TSqlTokenType.LeftParenthesis); + + // input + GenerateFragmentIfNotNull(node.Input); + + // labels + if (node.Labels != null) + { + for (int i = 0; i < node.Labels.Count; i++) + { + GenerateSymbol(TSqlTokenType.Comma); + GenerateSpace(); + GenerateFragmentIfNotNull(node.Labels[i]); + } + } + + GenerateSymbol(TSqlTokenType.RightParenthesis); + } + } +} diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiFixGrammarFunction.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiFixGrammarFunction.cs new file mode 100644 index 0000000..a5de5c1 --- /dev/null +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiFixGrammarFunction.cs @@ -0,0 +1,25 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ +using Microsoft.SqlServer.TransactSql.ScriptDom; + +namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator +{ + partial class SqlScriptGeneratorVisitor + { + /// + /// Emits an AI_ANALYZE_SENTIMENT function call like + /// AI_ANALYZE_SENTIMENT(input), where input is an expression or identifier. + /// + /// Expression node to generate + public override void ExplicitVisit(AIFixGrammarFunctionCall node) + { + GenerateIdentifier(CodeGenerationSupporter.AIFixGrammar); + GenerateSymbol(TSqlTokenType.LeftParenthesis); + GenerateFragmentIfNotNull(node.Input); + GenerateSymbol(TSqlTokenType.RightParenthesis); + } + } +} diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiGenerateResponseFunction.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiGenerateResponseFunction.cs new file mode 100644 index 0000000..393a9af --- /dev/null +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiGenerateResponseFunction.cs @@ -0,0 +1,36 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ +using Microsoft.SqlServer.TransactSql.ScriptDom; + +namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator +{ + partial class SqlScriptGeneratorVisitor + { + /// + /// Emits an AI_GENERATE_RESPONSE function call like + /// AI_GENERATE_RESPONSE(promptPart1, promptPart2), + /// where promptPart1 and promptPart2 are expressions or identifiers + /// representing the parts of the prompt. + /// + /// Expression node to generate + public override void ExplicitVisit(AIGenerateResponseFunctionCall node) + { + GenerateIdentifier(CodeGenerationSupporter.AIGenerateResponse); + GenerateSymbol(TSqlTokenType.LeftParenthesis); + + GenerateFragmentIfNotNull(node.PromptPart1); + + if (node.PromptPart2 != null) + { + GenerateSymbol(TSqlTokenType.Comma); + GenerateSpace(); + GenerateFragmentIfNotNull(node.PromptPart2); + } + + GenerateSymbol(TSqlTokenType.RightParenthesis); + } + } +} diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiSummarizeFunction.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiSummarizeFunction.cs new file mode 100644 index 0000000..fe1adaf --- /dev/null +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiSummarizeFunction.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ +using Microsoft.SqlServer.TransactSql.ScriptDom; + +namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator +{ + partial class SqlScriptGeneratorVisitor + { + /// + /// Emits an AI_SUMMARIZE function call like + /// AI_SUMMARIZE(input), + /// where input is an expression or identifier. + /// + /// Expression node to generate + public override void ExplicitVisit(AISummarizeFunctionCall node) + { + GenerateIdentifier(CodeGenerationSupporter.AISummarize); + GenerateSymbol(TSqlTokenType.LeftParenthesis); + GenerateFragmentIfNotNull(node.Input); + GenerateSymbol(TSqlTokenType.RightParenthesis); + } + } +} diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiTranslateFunction.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiTranslateFunction.cs new file mode 100644 index 0000000..922751c --- /dev/null +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.AiTranslateFunction.cs @@ -0,0 +1,31 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ +using Microsoft.SqlServer.TransactSql.ScriptDom; + +namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator +{ + partial class SqlScriptGeneratorVisitor + { + /// + /// Emits an AI_TRANSLATE function call like + /// AI_TRANSLATE(input, language), + /// where input is an expression or identifier, + /// and language is an expression or identifier + /// representing the target language. + /// + /// Expression node to generate + public override void ExplicitVisit(AITranslateFunctionCall node) + { + GenerateIdentifier(CodeGenerationSupporter.AITranslate); + GenerateSymbol(TSqlTokenType.LeftParenthesis); + GenerateFragmentIfNotNull(node.Input); + GenerateSymbol(TSqlTokenType.Comma); + GenerateSpace(); + GenerateFragmentIfNotNull(node.Language); + GenerateSymbol(TSqlTokenType.RightParenthesis); + } + } +} diff --git a/Test/SqlDom/Baselines160/IifParenthesesTests160.sql b/Test/SqlDom/Baselines160/IifParenthesesTests160.sql new file mode 100644 index 0000000..39e6f21 --- /dev/null +++ b/Test/SqlDom/Baselines160/IifParenthesesTests160.sql @@ -0,0 +1,97 @@ +SELECT (SELECT 1 + WHERE (IIF (1 > 0 + AND 2 > 1, 1, 0)) = 1); + + +GO +SELECT 1 +WHERE (IIF (1 > 0, 1, 0)) = 1; + + +GO +SELECT 1 +WHERE (IIF (1 > 0 + AND 2 > 1 + OR 3 < 4, 1, 0)) = 1; + + +GO +SELECT 1 +WHERE (IIF (1 >= 0 + AND 2 <= 1 + AND 3 <> 4, 1, 0)) = 1; + + +GO +SELECT 1 +WHERE (IIF (IIF (1 > 0, 1, 0) > 0, 'yes', 'no')) = 'yes'; + + +GO +SELECT (IIF (1 > 0, 1, 0)) + 1 AS result; + + +GO +SELECT 1 +WHERE (IIF (1 > 0, 1, 0)) = 1 + AND (IIF (2 > 1, 1, 0)) = 1; + + +GO +SELECT 1 +WHERE IIF (1 > 0 + AND 2 > 1, 1, 0) = 1; + + +GO +SELECT 1 +WHERE ((((IIF (1 > 0 + AND 2 > 1, 1, 0))))) = 1; + + +GO +SELECT 1 +WHERE (((((IIF (1 > 0 + AND 2 > 1 + OR 3 < 4, 1, 0)))))) = 1; + + +GO +SELECT 1 +WHERE (((IIF (1 > 0, 1, 0)) = 1) + AND ((IIF (2 > 1, 1, 0)) = 1)); + + +GO +SELECT ((((IIF (1 > 0 + AND 2 > 1, 10, 20))))) + 5 AS result; + + +GO +SELECT 1 +WHERE ((IIF ((IIF (1 > 0, 1, 0)) > 0, 'yes', 'no'))) = 'yes'; + + +GO +SELECT 1 +WHERE (IIF (((IIF (1 > 0 + AND 2 > 1, 1, 0))) > 0, 'a', 'b')) = 'a'; + + +GO +SELECT 1 +WHERE ((IIF ((IIF ((IIF (1 > 0, 1, 0)) > 0, 2, 3)) > 1, 'x', 'y'))) = 'x'; + + +GO +SELECT 1 +WHERE (((IIF (((IIF (1 > 0 + AND 2 > 1, 1, 0))) = 1 + AND 3 < 5, 'pass', 'fail')))) = 'pass'; + + +GO +SELECT 1 AS IIF +FROM T1; + + diff --git a/Test/SqlDom/Baselines170/ComplexQueryTests170.sql b/Test/SqlDom/Baselines170/ComplexQueryTests170.sql new file mode 100644 index 0000000..3e2cb7b --- /dev/null +++ b/Test/SqlDom/Baselines170/ComplexQueryTests170.sql @@ -0,0 +1,69 @@ +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))))); + +SELECT CAST ('test' AS VECTOR(3, Float32)); + +SELECT CAST ('test' AS VECTOR(3, Float32)); + +SELECT 1 +WHERE col = CAST ('test' AS VECTOR(3, Float32)); + +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]) +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]')))) + 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]) +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]')))) + OR (CONVERT (BIGINT, CONVERT (INT, 8)) >= 157) + OR (140 <= 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)) +WHERE (t1.order_date >= '2025-01-01' + AND t2.quantity > 0 + AND EXISTS (SELECT 1 + FROM [dbo].[inventory] AS inv + WHERE inv.product_id = t2.product_id + AND inv.stock_level > 10)); \ No newline at end of file diff --git a/Test/SqlDom/BaselinesFabricDW/AiAnalyzeSentimentTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/AiAnalyzeSentimentTestsFabricDW.sql new file mode 100644 index 0000000..7c2656f --- /dev/null +++ b/Test/SqlDom/BaselinesFabricDW/AiAnalyzeSentimentTestsFabricDW.sql @@ -0,0 +1,27 @@ +SELECT AI_ANALYZE_SENTIMENT('text'); + +SELECT AI_ANALYZE_SENTIMENT(t.text_col) +FROM dbo.Texts AS t; + +SELECT AI_ANALYZE_SENTIMENT(text_col) +FROM Texts; + +DECLARE @s0 AS NVARCHAR (MAX) = N'Hello world'; + +SELECT AI_ANALYZE_SENTIMENT(@s0 + 'a'); + +SELECT AI_ANALYZE_SENTIMENT('b' + 'a'); + +SELECT * +FROM AI_ANALYZE_SENTIMENT((SELECT t.text_col + FROM dbo.Texts AS t)); + + +GO +CREATE OR ALTER FUNCTION dbo.fx +(@s NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN (AI_ANALYZE_SENTIMENT(@s)); +END diff --git a/Test/SqlDom/BaselinesFabricDW/AiClassifyTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/AiClassifyTestsFabricDW.sql new file mode 100644 index 0000000..4703721 --- /dev/null +++ b/Test/SqlDom/BaselinesFabricDW/AiClassifyTestsFabricDW.sql @@ -0,0 +1,26 @@ +SELECT AI_CLASSIFY('text', 'spam', 'ham'); + +SELECT AI_CLASSIFY(t.text_col, 'spam', 'ham') +FROM dbo.Texts AS t; + +DECLARE @s AS NVARCHAR (MAX) = N'Hello world'; + +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'); + + +GO +CREATE OR ALTER FUNCTION dbo.fx +(@s NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN (AI_CLASSIFY(@s, 'labelA', 'labelB')); +END diff --git a/Test/SqlDom/BaselinesFabricDW/AiExtractTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/AiExtractTestsFabricDW.sql new file mode 100644 index 0000000..4af178b --- /dev/null +++ b/Test/SqlDom/BaselinesFabricDW/AiExtractTestsFabricDW.sql @@ -0,0 +1,26 @@ +SELECT AI_EXTRACT('text', 'spam', 'ham'); + +SELECT AI_EXTRACT(t.text_col, 'spam', 'ham') +FROM dbo.Texts AS t; + +DECLARE @s AS NVARCHAR (MAX) = N'Hello world'; + +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'); + + +GO +CREATE OR ALTER FUNCTION dbo.fx +(@s NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN (AI_EXTRACT(@s, 'labelA', 'labelB')); +END diff --git a/Test/SqlDom/BaselinesFabricDW/AiFixGrammarTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/AiFixGrammarTestsFabricDW.sql new file mode 100644 index 0000000..7e6225a --- /dev/null +++ b/Test/SqlDom/BaselinesFabricDW/AiFixGrammarTestsFabricDW.sql @@ -0,0 +1,27 @@ +SELECT AI_FIX_GRAMMAR('text'); + +SELECT AI_FIX_GRAMMAR(t.text_col) +FROM dbo.Texts AS t; + +SELECT AI_FIX_GRAMMAR(text_col) +FROM Texts; + +DECLARE @s0 AS NVARCHAR (MAX) = N'Hello world'; + +SELECT AI_FIX_GRAMMAR(@s0 + 'a'); + +SELECT AI_FIX_GRAMMAR('b' + 'a'); + +SELECT * +FROM AI_FIX_GRAMMAR((SELECT t.text_col + FROM dbo.Texts AS t)); + + +GO +CREATE OR ALTER FUNCTION dbo.fx +(@s NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN (AI_FIX_GRAMMAR(@s)); +END diff --git a/Test/SqlDom/BaselinesFabricDW/AiGenerateResponseTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/AiGenerateResponseTestsFabricDW.sql new file mode 100644 index 0000000..5a0a6b5 --- /dev/null +++ b/Test/SqlDom/BaselinesFabricDW/AiGenerateResponseTestsFabricDW.sql @@ -0,0 +1,25 @@ +SELECT AI_GENERATE_RESPONSE('Hello'); + +SELECT AI_GENERATE_RESPONSE('Question:', ' What is the time?'); + +DECLARE @p1 AS NVARCHAR (MAX) = N'Prompt '; + +DECLARE @p2 AS NVARCHAR (MAX) = N'Continuation'; + +SELECT AI_GENERATE_RESPONSE(@p1 + 'part1', @p2); + +SELECT AI_GENERATE_RESPONSE('b' + 'a'); + +SELECT * +FROM AI_GENERATE_RESPONSE((SELECT t.text_col + FROM dbo.Texts AS t)); + + +GO +CREATE OR ALTER FUNCTION dbo.fx +(@p NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN (AI_GENERATE_RESPONSE(@p)); +END diff --git a/Test/SqlDom/BaselinesFabricDW/AiSummarizeTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/AiSummarizeTestsFabricDW.sql new file mode 100644 index 0000000..fbb972d --- /dev/null +++ b/Test/SqlDom/BaselinesFabricDW/AiSummarizeTestsFabricDW.sql @@ -0,0 +1,27 @@ +SELECT AI_SUMMARIZE('text'); + +SELECT AI_SUMMARIZE(t.text_col) +FROM dbo.Texts AS t; + +SELECT AI_SUMMARIZE(text_col) +FROM Texts; + +DECLARE @s0 AS NVARCHAR (MAX) = N'Hello world'; + +SELECT AI_SUMMARIZE(@s0 + 'a'); + +SELECT AI_SUMMARIZE('b' + 'a'); + +SELECT * +FROM AI_SUMMARIZE((SELECT t.text_col + FROM dbo.Texts AS t)); + + +GO +CREATE OR ALTER FUNCTION dbo.fx +(@s NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN (AI_SUMMARIZE(@s)); +END diff --git a/Test/SqlDom/BaselinesFabricDW/AiTranslateTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/AiTranslateTestsFabricDW.sql new file mode 100644 index 0000000..10b497d --- /dev/null +++ b/Test/SqlDom/BaselinesFabricDW/AiTranslateTestsFabricDW.sql @@ -0,0 +1,33 @@ +SELECT AI_TRANSLATE(t.text_col, 'es') +FROM dbo.Texts AS t; + +SELECT AI_TRANSLATE('text', 'es'); + +SELECT AI_TRANSLATE('text' + 'text', 'e' + 's'); + +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 * +FROM AI_TRANSLATE((SELECT t.text_col + FROM dbo.Texts AS t), 'e' + 's'); + + +GO +CREATE OR ALTER FUNCTION dbo.fx +(@s NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN (AI_TRANSLATE(@s, 'fr')); +END + + +GO +SELECT AI_TRANSLATE(@s, t.lang) +FROM dbo.Texts AS t; diff --git a/Test/SqlDom/Only160SyntaxTests.cs b/Test/SqlDom/Only160SyntaxTests.cs index 43cba90..552152b 100644 --- a/Test/SqlDom/Only160SyntaxTests.cs +++ b/Test/SqlDom/Only160SyntaxTests.cs @@ -54,6 +54,7 @@ public partial class SqlDomTests new ParserTest160("NotEnforcedConstraintTests160.sql", nErrors80: 3, nErrors90: 1, nErrors100: 1, nErrors110: 1, nErrors120: 1, nErrors130: 1, nErrors140: 1, nErrors150: 1), new ParserTest160("VectorFunctionTests160.sql", nErrors80: 0, nErrors90: 0, nErrors100: 0, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0), new ParserTest160("CreateEventSessionNotLikePredicate.sql", nErrors80: 2, nErrors90: 1, nErrors100: 1, nErrors110: 1, nErrors120: 1, nErrors130: 0, nErrors140: 0, nErrors150: 0), + new ParserTest160("IifParenthesesTests160.sql", nErrors80: 16, nErrors90: 16, nErrors100: 16, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0), }; private static readonly ParserTest[] SqlAzure160_TestInfos = diff --git a/Test/SqlDom/Only170SyntaxTests.cs b/Test/SqlDom/Only170SyntaxTests.cs index 3132f3c..5039493 100644 --- a/Test/SqlDom/Only170SyntaxTests.cs +++ b/Test/SqlDom/Only170SyntaxTests.cs @@ -31,7 +31,9 @@ public partial class SqlDomTests new ParserTest170("VectorTypeSecondParameterTests170.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 2, nErrors140: 2, nErrors150: 2, nErrors160: 2), new ParserTest170("RegexpLikeTests170.sql", nErrors80: 15, nErrors90: 15, nErrors100: 15, nErrors110: 18, nErrors120: 18, nErrors130: 18, nErrors140: 18, nErrors150: 18, nErrors160: 18), new ParserTest170("OptimizedLockingTests170.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 2, nErrors140: 2, nErrors150: 2, nErrors160: 2), - new ParserTest170("CreateEventSessionNotLikePredicate.sql", nErrors80: 2, nErrors90: 1, nErrors100: 1, nErrors110: 1, nErrors120: 1, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0) + new ParserTest170("CreateEventSessionNotLikePredicate.sql", nErrors80: 2, nErrors90: 1, nErrors100: 1, nErrors110: 1, nErrors120: 1, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0), + // Complex query with VECTOR types - parses syntactically in all versions (optimization fix), but VECTOR type only valid in TSql170 + new ParserTest170("ComplexQueryTests170.sql") }; private static readonly ParserTest[] SqlAzure170_TestInfos = diff --git a/Test/SqlDom/OnlyFabricDWSyntaxTests.cs b/Test/SqlDom/OnlyFabricDWSyntaxTests.cs index 87c490a..5bee5e2 100644 --- a/Test/SqlDom/OnlyFabricDWSyntaxTests.cs +++ b/Test/SqlDom/OnlyFabricDWSyntaxTests.cs @@ -17,6 +17,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), new ParserTestFabricDW("ScalarFunctionTestsFabricDW.sql", nErrors80: 3, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 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), + new ParserTestFabricDW("AiAnalyzeSentimentTestsFabricDW.sql", nErrors80: 2, nErrors90: 1, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 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), + new ParserTestFabricDW("AiExtractTestsFabricDW.sql", nErrors80: 2, nErrors90: 1, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 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), + 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), }; [TestMethod] diff --git a/Test/SqlDom/ParserErrorsTests.cs b/Test/SqlDom/ParserErrorsTests.cs index 1fb3965..2204c30 100644 --- a/Test/SqlDom/ParserErrorsTests.cs +++ b/Test/SqlDom/ParserErrorsTests.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using Microsoft.SqlServer.TransactSql.ScriptDom; using Microsoft.VisualStudio.TestTools.UnitTesting; using SqlStudio.Tests.AssemblyTools.TestCategory; @@ -7103,87 +7104,6 @@ public void IdentityColumnNegativeTestsFabricDW() ParserTestUtils.ErrorTestFabricDW(identityColumnSyntax2, new ParserErrorInfo(49, "SQL46010", "(")); } - /// - /// Negative tests for VECTOR INDEX syntax - /// - [TestMethod] - [Priority(0)] - [SqlStudioTestCategory(Category.UnitTest)] - public void VectorIndexNegativeTests() - { - // Missing INDEX keyword - ParserTestUtils.ErrorTest170("CREATE VECTOR IX_Test ON dbo.Documents (VectorData)", - new ParserErrorInfo(7, "SQL46010", "VECTOR")); - - // Missing table name - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON (VectorData)", - new ParserErrorInfo(31, "SQL46010", "(")); - - // Missing column specification - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents", - new ParserErrorInfo(44, "SQL46029", "")); - - // Empty column list - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents ()", - new ParserErrorInfo(46, "SQL46010", ")")); - - // Multiple columns (not supported for vector indexes) - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData, OtherColumn)", - new ParserErrorInfo(56, "SQL46010", ",")); - - // Invalid metric value - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (METRIC = 'invalid')", - new ParserErrorInfo(73, "SQL46010", "'invalid'")); - - // Invalid type value - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (TYPE = 'invalid')", - new ParserErrorInfo(71, "SQL46010", "'invalid'")); - - // Missing option value - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (METRIC = )", - new ParserErrorInfo(73, "SQL46010", ")")); - - // Empty option value - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (METRIC = '')", - new ParserErrorInfo(73, "SQL46010", "''")); - - // Missing WITH keyword when options are present - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) (METRIC = 'cosine')", - new ParserErrorInfo(59, "SQL46010", "METRIC")); - - // Missing parentheses around options - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH METRIC = 'cosine'", - new ParserErrorInfo(63, "SQL46010", "METRIC")); - - // Invalid option name - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (INVALID_OPTION = 'value')", - new ParserErrorInfo(64, "SQL46010", "INVALID_OPTION")); - - // Metric value without quotes - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (METRIC = cosine)", - new ParserErrorInfo(73, "SQL46010", "cosine")); - - // Type value without quotes - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (TYPE = DiskANN)", - new ParserErrorInfo(71, "SQL46010", "DiskANN")); - - // MAXDOP with invalid value - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (MAXDOP = 'invalid')", - new ParserErrorInfo(73, "SQL46010", "'invalid'")); - - // MAXDOP with negative value - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (MAXDOP = -1)", - new ParserErrorInfo(73, "SQL46010", "-")); - - // Missing equals sign in option - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (METRIC 'cosine')", - new ParserErrorInfo(64, "SQL46010", "METRIC")); - - // Incomplete WITH clause - ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH", - new ParserErrorInfo(58, "SQL46010", "WITH")); - } - /// /// Negative tests for AI_GENERATE_CHUNKS syntax /// @@ -7625,5 +7545,234 @@ public void VectorSearchErrorTest170() "SELECT * FROM VECTOR_SEARCH('tbl1', 'col1', 'query_vector', 'dot', 5)", new ParserErrorInfo(28, "SQL46010", "'tbl1'")); } + + /// + /// Negative tests for VECTOR INDEX syntax + /// + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void VectorIndexNegativeTests() + { + // Missing INDEX keyword + ParserTestUtils.ErrorTest170("CREATE VECTOR IX_Test ON dbo.Documents (VectorData)", + new ParserErrorInfo(7, "SQL46010", "VECTOR")); + + // Missing table name + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON (VectorData)", + new ParserErrorInfo(31, "SQL46010", "(")); + + // Missing column specification + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents", + new ParserErrorInfo(44, "SQL46029", "")); + + // Empty column list + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents ()", + new ParserErrorInfo(46, "SQL46010", ")")); + + // Multiple columns (not supported for vector indexes) + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData, OtherColumn)", + new ParserErrorInfo(56, "SQL46010", ",")); + + // Invalid metric value + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (METRIC = 'invalid')", + new ParserErrorInfo(73, "SQL46010", "'invalid'")); + + // Invalid type value + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (TYPE = 'invalid')", + new ParserErrorInfo(71, "SQL46010", "'invalid'")); + + // Missing option value + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (METRIC = )", + new ParserErrorInfo(73, "SQL46010", ")")); + + // Empty option value + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (METRIC = '')", + new ParserErrorInfo(73, "SQL46010", "''")); + + // Missing WITH keyword when options are present + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) (METRIC = 'cosine')", + new ParserErrorInfo(59, "SQL46010", "METRIC")); + + // Missing parentheses around options + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH METRIC = 'cosine'", + new ParserErrorInfo(63, "SQL46010", "METRIC")); + + // Invalid option name + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (INVALID_OPTION = 'value')", + new ParserErrorInfo(64, "SQL46010", "INVALID_OPTION")); + + // Metric value without quotes + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (METRIC = cosine)", + new ParserErrorInfo(73, "SQL46010", "cosine")); + + // Type value without quotes + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (TYPE = DiskANN)", + new ParserErrorInfo(71, "SQL46010", "DiskANN")); + + // MAXDOP with invalid value + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (MAXDOP = 'invalid')", + new ParserErrorInfo(73, "SQL46010", "'invalid'")); + + // MAXDOP with negative value + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (MAXDOP = -1)", + new ParserErrorInfo(73, "SQL46010", "-")); + + // Missing equals sign in option + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH (METRIC 'cosine')", + new ParserErrorInfo(64, "SQL46010", "METRIC")); + + // Incomplete WITH clause + ParserTestUtils.ErrorTest170("CREATE VECTOR INDEX IX_Test ON dbo.Documents (VectorData) WITH", + new ParserErrorInfo(58, "SQL46010", "WITH")); + } + + /// + /// Negative tests for AI_ANALYZE_SENTIMET syntax + /// + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void AiAnalyzeSentimentNegativeTestsFabricDw() + { + // Missing arguments + ParserTestUtils.ErrorTestFabricDW( + "SELECT AI_ANALYZE_SENTIMENT()", + new ParserErrorInfo(28, "SQL46010", ")")); + + // Too many arguments + ParserTestUtils.ErrorTestFabricDW( + "SELECT AI_ANALYZE_SENTIMENT('1', '2')", + new ParserErrorInfo(31, "SQL46010", ",")); + } + + /// + /// Negative tests for AI_CLASSIFY syntax + /// + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void AiClassifyNegativeTestsFabricDw() + { + // Missing arguments + ParserTestUtils.ErrorTestFabricDW( + "SELECT AI_CLASSIFY()", + new ParserErrorInfo(19, "SQL46010", ")")); + + // Too few arguments + ParserTestUtils.ErrorTestFabricDW( + "SELECT AI_CLASSIFY('1')", + new ParserErrorInfo(22, "SQL46010", ")")); + + // Too many arguments + ParserTestUtils.ErrorTestFabricDW( + $"SELECT AI_CLASSIFY({string.Join(", ", Enumerable.Repeat("'1'", 23))})", + new ParserErrorInfo(129, "SQL46010", "AI_CLASSIFY")); + } + + /// + /// Negative tests for AI_EXTRACT syntax + /// + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void AiExtractNegativeTestsFabricDw() + { + // Missing arguments + ParserTestUtils.ErrorTestFabricDW( + "SELECT AI_EXTRACT()", + new ParserErrorInfo(18, "SQL46010", ")")); + + // Too few arguments + ParserTestUtils.ErrorTestFabricDW( + "SELECT AI_EXTRACT('1')", + new ParserErrorInfo(21, "SQL46010", ")")); + + // Too many arguments + ParserTestUtils.ErrorTestFabricDW( + $"SELECT AI_EXTRACT({string.Join(", ", Enumerable.Repeat("'1'", 23))})", + new ParserErrorInfo(128, "SQL46010", "AI_EXTRACT")); + } + + /// + /// Negative tests for AI_SUMMARIZE syntax + /// + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void AiFixGrammarNegativeTestsFabricDw() + { + // Missing arguments + ParserTestUtils.ErrorTestFabricDW( + "SELECT AI_FIX_GRAMMAR()", + new ParserErrorInfo(22, "SQL46010", ")")); + + // Too many arguments + ParserTestUtils.ErrorTestFabricDW( + "SELECT AI_FIX_GRAMMAR('1', '2')", + new ParserErrorInfo(25, "SQL46010", ",")); + } + + /// + /// Negative tests for AI_GENERATE_RESPONSE syntax + /// + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void AiGenerateResponseNegativeTestsFabricDw() + { + // Missing arguments + ParserTestUtils.ErrorTestFabricDW( + "SELECT AI_GENERATE_RESPONSE()", + new ParserErrorInfo(28, "SQL46010", ")")); + + // Too many arguments + ParserTestUtils.ErrorTestFabricDW( + "SELECT AI_GENERATE_RESPONSE('1', '2', '3')", + new ParserErrorInfo(36, "SQL46010", ",")); + } + + /// + /// Negative tests for AI_SUMMARIZE syntax + /// + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void AiSummarizeNegativeTestsFabricDw() + { + // Missing arguments + ParserTestUtils.ErrorTestFabricDW( + "SELECT AI_SUMMARIZE()", + new ParserErrorInfo(20, "SQL46010", ")")); + + // Too many arguments + ParserTestUtils.ErrorTestFabricDW( + "SELECT AI_SUMMARIZE('1', '2')", + new ParserErrorInfo(23, "SQL46010", ",")); + } + + /// + /// Negative tests for AI_GENERATE_RESPONSE syntax + /// + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void AiTranslateNegativeTestsFabricDw() + { + // Missing arguments + ParserTestUtils.ErrorTestFabricDW( + "SELECT AI_TRANSLATE()", + new ParserErrorInfo(20, "SQL46010", ")")); + + // Too few arguments + ParserTestUtils.ErrorTestFabricDW( + "SELECT AI_TRANSLATE('1')", + new ParserErrorInfo(23, "SQL46010", ")")); + + // Too many arguments + ParserTestUtils.ErrorTestFabricDW( + "SELECT AI_TRANSLATE('1', '2', '3')", + new ParserErrorInfo(28, "SQL46010", ",")); + } } } diff --git a/Test/SqlDom/TestScripts/AiAnalyzeSentimentTestsFabricDW.sql b/Test/SqlDom/TestScripts/AiAnalyzeSentimentTestsFabricDW.sql new file mode 100644 index 0000000..7c2656f --- /dev/null +++ b/Test/SqlDom/TestScripts/AiAnalyzeSentimentTestsFabricDW.sql @@ -0,0 +1,27 @@ +SELECT AI_ANALYZE_SENTIMENT('text'); + +SELECT AI_ANALYZE_SENTIMENT(t.text_col) +FROM dbo.Texts AS t; + +SELECT AI_ANALYZE_SENTIMENT(text_col) +FROM Texts; + +DECLARE @s0 AS NVARCHAR (MAX) = N'Hello world'; + +SELECT AI_ANALYZE_SENTIMENT(@s0 + 'a'); + +SELECT AI_ANALYZE_SENTIMENT('b' + 'a'); + +SELECT * +FROM AI_ANALYZE_SENTIMENT((SELECT t.text_col + FROM dbo.Texts AS t)); + + +GO +CREATE OR ALTER FUNCTION dbo.fx +(@s NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN (AI_ANALYZE_SENTIMENT(@s)); +END diff --git a/Test/SqlDom/TestScripts/AiClassifyTestsFabricDW.sql b/Test/SqlDom/TestScripts/AiClassifyTestsFabricDW.sql new file mode 100644 index 0000000..2a623c0 --- /dev/null +++ b/Test/SqlDom/TestScripts/AiClassifyTestsFabricDW.sql @@ -0,0 +1,23 @@ +-- Scalar input + two string labels +SELECT AI_CLASSIFY('text', 'spam', 'ham'); + +-- Column input + two string labels +SELECT AI_CLASSIFY(t.text_col, 'spam', 'ham') +FROM dbo.Texts AS t; + +-- Variable input + three labels +DECLARE @s NVARCHAR(MAX) = N'Hello world'; +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'); +GO + +-- RETURN context +CREATE OR ALTER FUNCTION dbo.fx(@s NVARCHAR(MAX)) +RETURNS NVARCHAR(MAX) +AS +BEGIN + RETURN (AI_CLASSIFY(@s, 'labelA', 'labelB')); +END; +GO diff --git a/Test/SqlDom/TestScripts/AiExtractTestsFabricDW.sql b/Test/SqlDom/TestScripts/AiExtractTestsFabricDW.sql new file mode 100644 index 0000000..cf2c82e --- /dev/null +++ b/Test/SqlDom/TestScripts/AiExtractTestsFabricDW.sql @@ -0,0 +1,23 @@ +-- Scalar input + two string labels +SELECT AI_EXTRACT('text', 'spam', 'ham'); + +-- Column input + two string labels +SELECT AI_EXTRACT(t.text_col, 'spam', 'ham') +FROM dbo.Texts AS t; + +-- Variable input + three labels +DECLARE @s NVARCHAR(MAX) = N'Hello world'; +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'); +GO + +-- RETURN context +CREATE OR ALTER FUNCTION dbo.fx(@s NVARCHAR(MAX)) +RETURNS NVARCHAR(MAX) +AS +BEGIN + RETURN (AI_EXTRACT(@s, 'labelA', 'labelB')); +END; +GO diff --git a/Test/SqlDom/TestScripts/AiFixGrammarTestsFabricDW.sql b/Test/SqlDom/TestScripts/AiFixGrammarTestsFabricDW.sql new file mode 100644 index 0000000..7e6225a --- /dev/null +++ b/Test/SqlDom/TestScripts/AiFixGrammarTestsFabricDW.sql @@ -0,0 +1,27 @@ +SELECT AI_FIX_GRAMMAR('text'); + +SELECT AI_FIX_GRAMMAR(t.text_col) +FROM dbo.Texts AS t; + +SELECT AI_FIX_GRAMMAR(text_col) +FROM Texts; + +DECLARE @s0 AS NVARCHAR (MAX) = N'Hello world'; + +SELECT AI_FIX_GRAMMAR(@s0 + 'a'); + +SELECT AI_FIX_GRAMMAR('b' + 'a'); + +SELECT * +FROM AI_FIX_GRAMMAR((SELECT t.text_col + FROM dbo.Texts AS t)); + + +GO +CREATE OR ALTER FUNCTION dbo.fx +(@s NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN (AI_FIX_GRAMMAR(@s)); +END diff --git a/Test/SqlDom/TestScripts/AiGenerateResponseTestsFabricDW.sql b/Test/SqlDom/TestScripts/AiGenerateResponseTestsFabricDW.sql new file mode 100644 index 0000000..55cec5a --- /dev/null +++ b/Test/SqlDom/TestScripts/AiGenerateResponseTestsFabricDW.sql @@ -0,0 +1,19 @@ +SELECT AI_GENERATE_RESPONSE('Hello'); + +SELECT AI_GENERATE_RESPONSE('Question:', ' What is the time?'); + +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 * FROM AI_GENERATE_RESPONSE((SELECT t.text_col + FROM dbo.Texts AS t)); +GO + +CREATE OR ALTER FUNCTION dbo.fx(@p NVARCHAR(MAX)) +RETURNS NVARCHAR(MAX) +AS +BEGIN + RETURN (AI_GENERATE_RESPONSE(@p)); +END; +GO diff --git a/Test/SqlDom/TestScripts/AiSummarizeTestsFabricDW.sql b/Test/SqlDom/TestScripts/AiSummarizeTestsFabricDW.sql new file mode 100644 index 0000000..fbb972d --- /dev/null +++ b/Test/SqlDom/TestScripts/AiSummarizeTestsFabricDW.sql @@ -0,0 +1,27 @@ +SELECT AI_SUMMARIZE('text'); + +SELECT AI_SUMMARIZE(t.text_col) +FROM dbo.Texts AS t; + +SELECT AI_SUMMARIZE(text_col) +FROM Texts; + +DECLARE @s0 AS NVARCHAR (MAX) = N'Hello world'; + +SELECT AI_SUMMARIZE(@s0 + 'a'); + +SELECT AI_SUMMARIZE('b' + 'a'); + +SELECT * +FROM AI_SUMMARIZE((SELECT t.text_col + FROM dbo.Texts AS t)); + + +GO +CREATE OR ALTER FUNCTION dbo.fx +(@s NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN (AI_SUMMARIZE(@s)); +END diff --git a/Test/SqlDom/TestScripts/AiTranslateTestsFabricDW.sql b/Test/SqlDom/TestScripts/AiTranslateTestsFabricDW.sql new file mode 100644 index 0000000..bcda9fb --- /dev/null +++ b/Test/SqlDom/TestScripts/AiTranslateTestsFabricDW.sql @@ -0,0 +1,26 @@ +SELECT AI_TRANSLATE (t.text_col, 'es') +FROM dbo.Texts AS t; + +SELECT AI_TRANSLATE ('text', 'es'); +SELECT AI_TRANSLATE ('text' + 'text', 'e' + 's'); + +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 * FROM AI_TRANSLATE((SELECT t.text_col + FROM dbo.Texts AS t), 'e' + 's'); +GO + +CREATE OR ALTER FUNCTION dbo.fx +(@s NVARCHAR (MAX)) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN (AI_TRANSLATE (@s, 'fr')); +END + +GO + +SELECT AI_TRANSLATE (@s, t.lang) +FROM dbo.Texts AS t; diff --git a/Test/SqlDom/TestScripts/ComplexQueryTests170.sql b/Test/SqlDom/TestScripts/ComplexQueryTests170.sql new file mode 100644 index 0000000..0275e03 --- /dev/null +++ b/Test/SqlDom/TestScripts/ComplexQueryTests170.sql @@ -0,0 +1,72 @@ +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)) +))) + + +SELECT CAST('test' AS VECTOR (3, Float32)) + +SELECT CAST('test' AS VECTOR(3, Float32)) + +SELECT 1 WHERE col = CAST('test' AS VECTOR (3, Float32)); + +SELECT 1 WHERE col IN (SELECT CAST('test' AS VECTOR (3, Float32))); + +-- Complex query with nested subqueries and various joins with 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]) +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]')))) +OR (CONVERT (BIGINT, CONVERT (INT, 8)) >= 157) +OR (140 <= 19))) + +-- Complex query with nested subqueries and various joins with VECTOR(3) +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]) +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]')))) +OR (CONVERT (BIGINT, CONVERT (INT, 8)) >= 157) +OR (140 <= 19))) + +-- Complex query involving multiple joins, CROSS APPLY, and filtering conditions +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)) +WHERE (t1.order_date >= '2025-01-01' + AND t2.quantity > 0 + AND EXISTS (SELECT 1 FROM [dbo].[inventory] AS inv + WHERE inv.product_id = t2.product_id + AND inv.stock_level > 10)) \ No newline at end of file diff --git a/Test/SqlDom/TestScripts/IifParenthesesTests160.sql b/Test/SqlDom/TestScripts/IifParenthesesTests160.sql new file mode 100644 index 0000000..2c6bda6 --- /dev/null +++ b/Test/SqlDom/TestScripts/IifParenthesesTests160.sql @@ -0,0 +1,75 @@ +-- Test IIF with parentheses wrapping - regression test for customer-reported bug +-- The parser was incorrectly rejecting valid T-SQL like WHERE (IIF(...)) = 1 + +-- Original repro case: IIF with multiple boolean operators wrapped in parentheses +SELECT +( + SELECT 1 + WHERE (IIF(1 > 0 AND 2 > 1, 1, 0)) = 1 +); +GO + +-- Simple IIF wrapped in parentheses with comparison +SELECT 1 WHERE (IIF(1 > 0, 1, 0)) = 1; +GO + +-- IIF with multiple boolean operators (AND, OR) wrapped in parentheses +SELECT 1 WHERE (IIF(1 > 0 AND 2 > 1 OR 3 < 4, 1, 0)) = 1; +GO + +-- IIF with comparison operators inside, wrapped in parentheses +SELECT 1 WHERE (IIF(1 >= 0 AND 2 <= 1 AND 3 <> 4, 1, 0)) = 1; +GO + +-- Nested IIF wrapped in parentheses +SELECT 1 WHERE (IIF(IIF(1 > 0, 1, 0) > 0, 'yes', 'no')) = 'yes'; +GO + +-- IIF in SELECT clause wrapped in parentheses +SELECT (IIF(1 > 0, 1, 0)) + 1 AS result; +GO + +-- Multiple IIF expressions with parentheses +SELECT 1 WHERE (IIF(1 > 0, 1, 0)) = 1 AND (IIF(2 > 1, 1, 0)) = 1; +GO + +-- IIF without parentheses (should still work - baseline) +SELECT 1 WHERE IIF(1 > 0 AND 2 > 1, 1, 0) = 1; +GO + +-- Deeply nested parentheses (4 levels) around IIF +SELECT 1 WHERE ((((IIF(1 > 0 AND 2 > 1, 1, 0))))) = 1; +GO + +-- Deeply nested parentheses (5 levels) around IIF with complex boolean +SELECT 1 WHERE (((((IIF(1 > 0 AND 2 > 1 OR 3 < 4, 1, 0)))))) = 1; +GO + +-- Mixed nesting: parentheses around boolean operators and IIF +SELECT 1 WHERE (((IIF(1 > 0, 1, 0)) = 1) AND ((IIF(2 > 1, 1, 0)) = 1)); +GO + +-- Deeply nested IIF inside arithmetic expression +SELECT ((((IIF(1 > 0 AND 2 > 1, 10, 20))))) + 5 AS result; +GO + +-- Nested IIF with extra parentheses around outer IIF +SELECT 1 WHERE ((IIF((IIF(1 > 0, 1, 0)) > 0, 'yes', 'no'))) = 'yes'; +GO + +-- Nested IIF with extra parentheses around inner IIF +SELECT 1 WHERE (IIF(((IIF(1 > 0 AND 2 > 1, 1, 0))) > 0, 'a', 'b')) = 'a'; +GO + +-- Triple nested IIF with parentheses +SELECT 1 WHERE ((IIF((IIF((IIF(1 > 0, 1, 0)) > 0, 2, 3)) > 1, 'x', 'y'))) = 'x'; +GO + +-- Nested IIF with boolean operators and extra parentheses +SELECT 1 WHERE (((IIF(((IIF(1 > 0 AND 2 > 1, 1, 0))) = 1 AND 3 < 5, 'pass', 'fail')))) = 'pass'; +GO + +-- Edge case: IIF used as column alias (not a function call) +-- This tests that pendingIIf flag is reset when IIF is not followed by ( +SELECT 1 AS IIF FROM T1; +GO diff --git a/release-notes/170/170.157.0.md b/release-notes/170/170.157.0.md new file mode 100644 index 0000000..4e9cb54 --- /dev/null +++ b/release-notes/170/170.157.0.md @@ -0,0 +1,26 @@ +# Release Notes + +## Microsoft.SqlServer.TransactSql.ScriptDom 170.157.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.415 + +#### .NET Framework +#### .NET Core + +### New Features +* Adds support for Fabric DW specific AI functions + +### Fixed +* Fixes https://github.com/microsoft/SqlScriptDOM/issues/161 +* Fixes https://github.com/microsoft/SqlScriptDOM/issues/28 +### Changes + +### Known Issues