diff --git a/SqlScriptDom/Parser/TSql/Ast.xml b/SqlScriptDom/Parser/TSql/Ast.xml index 9a1d8b5..482f047 100644 --- a/SqlScriptDom/Parser/TSql/Ast.xml +++ b/SqlScriptDom/Parser/TSql/Ast.xml @@ -649,6 +649,7 @@ + diff --git a/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs b/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs index acb3acb..228acf2 100644 --- a/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs +++ b/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs @@ -524,6 +524,9 @@ internal static class CodeGenerationSupporter internal const string JsonObject = "JSON_OBJECT"; internal const string JsonObjectAgg = "JSON_OBJECTAGG"; internal const string JsonArrayAgg = "JSON_ARRAYAGG"; + internal const string JsonQuery = "JSON_QUERY"; + internal const string Array = "ARRAY"; + internal const string Wrapper = "WRAPPER"; internal const string Keep = "KEEP"; internal const string KeepDefaults = "KEEPDEFAULTS"; internal const string KeepFixed = "KEEPFIXED"; diff --git a/SqlScriptDom/Parser/TSql/TSql170.g b/SqlScriptDom/Parser/TSql/TSql170.g index cb1a22d..b04a570 100644 --- a/SqlScriptDom/Parser/TSql/TSql170.g +++ b/SqlScriptDom/Parser/TSql/TSql170.g @@ -32836,6 +32836,9 @@ builtInFunctionCall returns [FunctionCall vResult = FragmentFactory.CreateFragme | {(vResult.FunctionName != null && vResult.FunctionName.Value.ToUpper(CultureInfo.InvariantCulture) == CodeGenerationSupporter.JsonArrayAgg)}? jsonArrayAggBuiltInFunctionCall[vResult] + | + {(vResult.FunctionName != null && vResult.FunctionName.Value.ToUpper(CultureInfo.InvariantCulture) == CodeGenerationSupporter.JsonQuery)}? + jsonQueryBuiltInFunctionCall[vResult] | {(vResult.FunctionName != null && vResult.FunctionName.Value.ToUpper(CultureInfo.InvariantCulture) == CodeGenerationSupporter.Trim) && (NextTokenMatches(CodeGenerationSupporter.Leading) | NextTokenMatches(CodeGenerationSupporter.Trailing) | NextTokenMatches(CodeGenerationSupporter.Both))}? @@ -32936,6 +32939,41 @@ jsonObjectAggBuiltInFunctionCall [FunctionCall vParent] } ; +jsonQueryBuiltInFunctionCall [FunctionCall vParent] +{ + ScalarExpression vExpression; + ScalarExpression vPath; +} + : vExpression=expression + { + AddAndUpdateTokenInfo(vParent, vParent.Parameters, vExpression); + } + ( + Comma vPath=expression + { + AddAndUpdateTokenInfo(vParent, vParent.Parameters, vPath); + } + )? + tRParen:RightParenthesis + { + UpdateTokenInfo(vParent, tRParen); + } + ( + With tArray:Identifier tWrapper:Identifier + { + if (!tArray.getText().Equals(CodeGenerationSupporter.Array, StringComparison.OrdinalIgnoreCase)) + { + throw GetUnexpectedTokenErrorException(tArray); + } + if (!tWrapper.getText().Equals(CodeGenerationSupporter.Wrapper, StringComparison.OrdinalIgnoreCase)) + { + throw GetUnexpectedTokenErrorException(tWrapper); + } + vParent.WithArrayWrapper = true; + } + )? + ; + regularBuiltInFunctionCall [FunctionCall vParent] { ColumnReferenceExpression vColumn; diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FunctionCall.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FunctionCall.cs index 0daaff0..25c8bd0 100644 --- a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FunctionCall.cs +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FunctionCall.cs @@ -93,6 +93,22 @@ public override void ExplicitVisit(FunctionCall node) GenerateReturnType(node?.ReturnType); GenerateSymbol(TSqlTokenType.RightParenthesis); } + else if (node.FunctionName.Value.ToUpper(CultureInfo.InvariantCulture) == CodeGenerationSupporter.JsonQuery) + { + GenerateCommaSeparatedList(node.Parameters); + GenerateSymbol(TSqlTokenType.RightParenthesis); + + // Handle WITH ARRAY WRAPPER clause + if (node.WithArrayWrapper) + { + GenerateSpace(); + GenerateKeyword(TSqlTokenType.With); + GenerateSpace(); + GenerateIdentifier(CodeGenerationSupporter.Array); + GenerateSpace(); + GenerateIdentifier(CodeGenerationSupporter.Wrapper); + } + } else { GenerateUniqueRowFilter(node.UniqueRowFilter, false); diff --git a/Test/SqlDom/Baselines170/JsonFunctionTests170.sql b/Test/SqlDom/Baselines170/JsonFunctionTests170.sql index 0b3f776..c8042bc 100644 --- a/Test/SqlDom/Baselines170/JsonFunctionTests170.sql +++ b/Test/SqlDom/Baselines170/JsonFunctionTests170.sql @@ -134,6 +134,22 @@ AS SELECT JSON_OBJECTAGG(c1:c2) AS jsoncontents FROM (VALUES ('key1', 'c'), ('key2', 'b'), ('key3', 'a')) AS t(c1, c2); + +GO +SELECT JSON_QUERY('{ "a": 1 }'); + +SELECT JSON_QUERY('{ "a": 1 }', '$.a'); + +SELECT JSON_QUERY('{ "a": [1,2,3] }', '$.a') WITH ARRAY WRAPPER; + + +GO +CREATE VIEW dbo.jsonfunctest +AS +SELECT JSON_OBJECTAGG(c1:c2) AS jsoncontents +FROM (VALUES ('key1', 'c'), ('key2', 'b'), ('key3', 'a')) AS t(c1, c2); + + GO SELECT TOP (5) c.object_id, JSON_OBJECTAGG(c.name:c.column_id) AS columns diff --git a/Test/SqlDom/Only170SyntaxTests.cs b/Test/SqlDom/Only170SyntaxTests.cs index 488f624..523cca0 100644 --- a/Test/SqlDom/Only170SyntaxTests.cs +++ b/Test/SqlDom/Only170SyntaxTests.cs @@ -19,7 +19,7 @@ public partial class SqlDomTests 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: 11, nErrors90: 8, nErrors100: 36, nErrors110: 36, nErrors120: 36, nErrors130: 36, nErrors140: 36, nErrors150: 36, nErrors160: 36), + new ParserTest170("JsonFunctionTests170.sql", nErrors80: 13, nErrors90: 8, nErrors100: 38, nErrors110: 38, nErrors120: 38, nErrors130: 38, nErrors140: 38, nErrors150: 38, nErrors160: 38), new ParserTest170("AiGenerateEmbeddingsTests170.sql", nErrors80: 12, nErrors90: 9, nErrors100: 9, nErrors110: 9, nErrors120: 9, nErrors130: 9, nErrors140: 9, nErrors150: 9, nErrors160: 9), new ParserTest170("CreateExternalModelStatementTests170.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 4, nErrors140: 4, nErrors150: 4, nErrors160: 4), new ParserTest170("AlterExternalModelStatementTests170.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 5, nErrors140: 5, nErrors150: 5, nErrors160: 5), diff --git a/Test/SqlDom/ParserErrorsTests.cs b/Test/SqlDom/ParserErrorsTests.cs index 26b5e09..b793a14 100644 --- a/Test/SqlDom/ParserErrorsTests.cs +++ b/Test/SqlDom/ParserErrorsTests.cs @@ -577,6 +577,43 @@ public void JsonArrayAggSyntaxNegativeTest() new ParserErrorInfo(34, "SQL46010", "NULL")); } + /// + /// Negative tests for JSON_QUERY syntax in functions + /// + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void JsonQuerySyntaxNegativeTest() + { + // Cannot use WITH without ARRAY WRAPPER (incomplete syntax) + ParserTestUtils.ErrorTest170("SELECT JSON_QUERY('{ \"a\": 1 }') WITH", + new ParserErrorInfo(32, "SQL46010", "WITH")); + + // Cannot use WITH ARRAY without WRAPPER (unexpected end of file) + ParserTestUtils.ErrorTest170("SELECT JSON_QUERY('{ \"a\": 1 }') WITH ARRAY", + new ParserErrorInfo(42, "SQL46029", "")); + + // Cannot use WITH WRAPPER without ARRAY (unexpected end of file) + ParserTestUtils.ErrorTest170("SELECT JSON_QUERY('{ \"a\": 1 }') WITH WRAPPER", + new ParserErrorInfo(44, "SQL46029", "")); + + // Cannot use incorrect keyword instead of ARRAY + ParserTestUtils.ErrorTest170("SELECT JSON_QUERY('{ \"a\": 1 }') WITH OBJECT WRAPPER", + new ParserErrorInfo(37, "SQL46010", "OBJECT")); + + // Cannot use incorrect keyword instead of WRAPPER + ParserTestUtils.ErrorTest170("SELECT JSON_QUERY('{ \"a\": 1 }') WITH ARRAY OBJECT", + new ParserErrorInfo(43, "SQL46010", "OBJECT")); + + // Cannot use JSON_QUERY with colon syntax (key:value pairs like JSON_OBJECT) + ParserTestUtils.ErrorTest170("SELECT JSON_QUERY('name':'value')", + new ParserErrorInfo(24, "SQL46010", ":")); + + // WITH ARRAY WRAPPER must come after closing parenthesis, not before + ParserTestUtils.ErrorTest170("SELECT JSON_QUERY('{ \"a\": 1 }' WITH ARRAY WRAPPER)", + new ParserErrorInfo(31, "SQL46010", "WITH")); + } + /// /// Negative tests for Data Masking Alter Column syntax. /// diff --git a/Test/SqlDom/TestScripts/JsonFunctionTests170.sql b/Test/SqlDom/TestScripts/JsonFunctionTests170.sql index f5847a0..89564cf 100644 --- a/Test/SqlDom/TestScripts/JsonFunctionTests170.sql +++ b/Test/SqlDom/TestScripts/JsonFunctionTests170.sql @@ -90,46 +90,59 @@ SELECT JSON_OBJECTAGG(); SELECT JSON_OBJECTAGG('name':1); -SELECT JSON_OBJECTAGG('name':JSON_ARRAY(1, 2)); +SELECT JSON_OBJECTAGG('name':JSON_ARRAY(1, 2)); SELECT JSON_OBJECTAGG('name':'b' NULL ON NULL RETURNING JSON); SELECT JSON_OBJECTAGG('name':'b' ABSENT ON NULL RETURNING JSON); SELECT JSON_OBJECTAGG('name':'b' RETURNING JSON); - + SELECT JSON_ARRAYAGG('name'); -SELECT JSON_ARRAYAGG('a'); -SELECT JSON_OBJECTAGG( c1:c2 ) -SELECT JSON_OBJECTAGG( c1:'c2' ) - -SELECT JSON_ARRAYAGG('a' NULL ON NULL); - -SELECT JSON_ARRAYAGG('a' NULL ON NULL RETURNING JSON); - +SELECT JSON_ARRAYAGG('a'); +SELECT JSON_OBJECTAGG( c1:c2 ) +SELECT JSON_OBJECTAGG( c1:'c2' ) + +SELECT JSON_ARRAYAGG('a' NULL ON NULL); + +SELECT JSON_ARRAYAGG('a' NULL ON NULL RETURNING JSON); + SELECT s.session_id, JSON_ARRAYAGG(s.host_name) FROM sys.dm_exec_sessions AS s -WHERE s.is_user_process = 1; - +WHERE s.is_user_process = 1; + SELECT s.session_id, JSON_ARRAYAGG(s.host_name NULL ON NULL) FROM sys.dm_exec_sessions AS s -WHERE s.is_user_process = 1; - +WHERE s.is_user_process = 1; + SELECT s.session_id, JSON_ARRAYAGG(s.host_name NULL ON NULL RETURNING JSON) FROM sys.dm_exec_sessions AS s -WHERE s.is_user_process = 1; - -GO -CREATE VIEW dbo.jsonfunctest AS - SELECT JSON_OBJECTAGG( c1:c2 ) as jsoncontents - FROM ( - VALUES('key1', 'c'), ('key2', 'b'), ('key3','a') - ) AS t(c1, c2); - -GO -SELECT TOP(5) c.object_id, JSON_OBJECTAGG(c.name:c.column_id) AS columns - FROM sys.columns AS c - GROUP BY c.object_id; \ No newline at end of file +WHERE s.is_user_process = 1; + +GO +CREATE VIEW dbo.jsonfunctest AS + SELECT JSON_OBJECTAGG( c1:c2 ) as jsoncontents + FROM ( + VALUES('key1', 'c'), ('key2', 'b'), ('key3','a') + ) AS t(c1, c2); + +GO + +SELECT JSON_QUERY('{ "a": 1 }'); +SELECT JSON_QUERY('{ "a": 1 }', '$.a'); +SELECT JSON_QUERY('{ "a": [1,2,3] }', '$.a') WITH ARRAY WRAPPER; + +GO +CREATE VIEW dbo.jsonfunctest AS + SELECT JSON_OBJECTAGG( c1:c2 ) as jsoncontents + FROM ( + VALUES('key1', 'c'), ('key2', 'b'), ('key3','a') + ) AS t(c1, c2); + +GO +SELECT TOP(5) c.object_id, JSON_OBJECTAGG(c.name:c.column_id) AS columns + FROM sys.columns AS c + GROUP BY c.object_id;