From 3a86904965dfbacd2844229d04e1a646d445c3c3 Mon Sep 17 00:00:00 2001 From: Leila Lali Date: Tue, 16 Sep 2025 11:48:11 -0700 Subject: [PATCH] Add support for JSON_QUERY function --- SqlScriptDom/Parser/TSql/Ast.xml | 1 + .../Parser/TSql/CodeGenerationSupporter.cs | 3 ++ SqlScriptDom/Parser/TSql/TSql170.g | 38 ++++++++++++++ .../SqlScriptGeneratorVisitor.FunctionCall.cs | 16 ++++++ .../Baselines170/JsonFunctionTests170.sql | 10 +++- Test/SqlDom/Only170SyntaxTests.cs | 2 +- Test/SqlDom/ParserErrorsTests.cs | 37 ++++++++++++++ .../TestScripts/JsonFunctionTests170.sql | 51 +++++++++++-------- 8 files changed, 134 insertions(+), 24 deletions(-) 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 8ba896b..d5870a5 100644 --- a/SqlScriptDom/Parser/TSql/TSql170.g +++ b/SqlScriptDom/Parser/TSql/TSql170.g @@ -32833,6 +32833,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))}? @@ -32933,6 +32936,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 baa161f..bd94ebf 100644 --- a/Test/SqlDom/Baselines170/JsonFunctionTests170.sql +++ b/Test/SqlDom/Baselines170/JsonFunctionTests170.sql @@ -132,4 +132,12 @@ 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); \ No newline at end of file +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; \ No newline at end of file diff --git a/Test/SqlDom/Only170SyntaxTests.cs b/Test/SqlDom/Only170SyntaxTests.cs index 736c839..bbd90ae 100644 --- a/Test/SqlDom/Only170SyntaxTests.cs +++ b/Test/SqlDom/Only170SyntaxTests.cs @@ -17,7 +17,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: 10, nErrors90: 8, nErrors100: 35, nErrors110: 35, nErrors120: 35, nErrors130: 35, nErrors140: 35, nErrors150: 35, nErrors160: 35), + new ParserTest170("JsonFunctionTests170.sql", nErrors80: 11, nErrors90: 8, nErrors100: 36, nErrors110: 36, nErrors120: 36, nErrors130: 36, nErrors140: 36, nErrors150: 36, nErrors160: 36), 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 d275126..a8a76d2 100644 --- a/Test/SqlDom/TestScripts/JsonFunctionTests170.sql +++ b/Test/SqlDom/TestScripts/JsonFunctionTests170.sql @@ -90,41 +90,48 @@ 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); \ 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 \ No newline at end of file