Skip to content

Commit 35b7ba5

Browse files
authored
Add support for JSON_QUERY function (#165)
1 parent 0cc00b5 commit 35b7ba5

File tree

8 files changed

+152
-28
lines changed

8 files changed

+152
-28
lines changed

SqlScriptDom/Parser/TSql/Ast.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,7 @@
649649
<Member Name="JsonParameters" Type="JsonKeyValue" Collection="true" Summary="The Json parameters to the function."/>
650650
<Member Name="AbsentOrNullOnNull" Type="Identifier" Collection="true" Summary="The Absent or Null on Null will convert or remove sql null to json null"/>
651651
<Member Name="ReturnType" Type="Identifier" Collection="true" Summary="Return type of function. Used by json_arrayagg, json_objectagg, json_array, json_object and json_value"/>
652+
<Member Name="WithArrayWrapper" Type="bool" Summary="Indicates whether WITH ARRAY WRAPPER clause is specified for JSON_QUERY function."/>
652653
</Class>
653654
<Class Name="CallTarget" Abstract="true" Summary="Represents a target of a function call.">
654655
</Class>

SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,9 @@ internal static class CodeGenerationSupporter
524524
internal const string JsonObject = "JSON_OBJECT";
525525
internal const string JsonObjectAgg = "JSON_OBJECTAGG";
526526
internal const string JsonArrayAgg = "JSON_ARRAYAGG";
527+
internal const string JsonQuery = "JSON_QUERY";
528+
internal const string Array = "ARRAY";
529+
internal const string Wrapper = "WRAPPER";
527530
internal const string Keep = "KEEP";
528531
internal const string KeepDefaults = "KEEPDEFAULTS";
529532
internal const string KeepFixed = "KEEPFIXED";

SqlScriptDom/Parser/TSql/TSql170.g

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32834,6 +32834,9 @@ builtInFunctionCall returns [FunctionCall vResult = FragmentFactory.CreateFragme
3283432834
|
3283532835
{(vResult.FunctionName != null && vResult.FunctionName.Value.ToUpper(CultureInfo.InvariantCulture) == CodeGenerationSupporter.JsonArrayAgg)}?
3283632836
jsonArrayAggBuiltInFunctionCall[vResult]
32837+
|
32838+
{(vResult.FunctionName != null && vResult.FunctionName.Value.ToUpper(CultureInfo.InvariantCulture) == CodeGenerationSupporter.JsonQuery)}?
32839+
jsonQueryBuiltInFunctionCall[vResult]
3283732840
|
3283832841
{(vResult.FunctionName != null && vResult.FunctionName.Value.ToUpper(CultureInfo.InvariantCulture) == CodeGenerationSupporter.Trim) &&
3283932842
(NextTokenMatches(CodeGenerationSupporter.Leading) | NextTokenMatches(CodeGenerationSupporter.Trailing) | NextTokenMatches(CodeGenerationSupporter.Both))}?
@@ -32934,6 +32937,41 @@ jsonObjectAggBuiltInFunctionCall [FunctionCall vParent]
3293432937
}
3293532938
;
3293632939

32940+
jsonQueryBuiltInFunctionCall [FunctionCall vParent]
32941+
{
32942+
ScalarExpression vExpression;
32943+
ScalarExpression vPath;
32944+
}
32945+
: vExpression=expression
32946+
{
32947+
AddAndUpdateTokenInfo(vParent, vParent.Parameters, vExpression);
32948+
}
32949+
(
32950+
Comma vPath=expression
32951+
{
32952+
AddAndUpdateTokenInfo(vParent, vParent.Parameters, vPath);
32953+
}
32954+
)?
32955+
tRParen:RightParenthesis
32956+
{
32957+
UpdateTokenInfo(vParent, tRParen);
32958+
}
32959+
(
32960+
With tArray:Identifier tWrapper:Identifier
32961+
{
32962+
if (!tArray.getText().Equals(CodeGenerationSupporter.Array, StringComparison.OrdinalIgnoreCase))
32963+
{
32964+
throw GetUnexpectedTokenErrorException(tArray);
32965+
}
32966+
if (!tWrapper.getText().Equals(CodeGenerationSupporter.Wrapper, StringComparison.OrdinalIgnoreCase))
32967+
{
32968+
throw GetUnexpectedTokenErrorException(tWrapper);
32969+
}
32970+
vParent.WithArrayWrapper = true;
32971+
}
32972+
)?
32973+
;
32974+
3293732975
regularBuiltInFunctionCall [FunctionCall vParent]
3293832976
{
3293932977
ColumnReferenceExpression vColumn;

SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.FunctionCall.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,22 @@ public override void ExplicitVisit(FunctionCall node)
9393
GenerateReturnType(node?.ReturnType);
9494
GenerateSymbol(TSqlTokenType.RightParenthesis);
9595
}
96+
else if (node.FunctionName.Value.ToUpper(CultureInfo.InvariantCulture) == CodeGenerationSupporter.JsonQuery)
97+
{
98+
GenerateCommaSeparatedList(node.Parameters);
99+
GenerateSymbol(TSqlTokenType.RightParenthesis);
100+
101+
// Handle WITH ARRAY WRAPPER clause
102+
if (node.WithArrayWrapper)
103+
{
104+
GenerateSpace();
105+
GenerateKeyword(TSqlTokenType.With);
106+
GenerateSpace();
107+
GenerateIdentifier(CodeGenerationSupporter.Array);
108+
GenerateSpace();
109+
GenerateIdentifier(CodeGenerationSupporter.Wrapper);
110+
}
111+
}
96112
else
97113
{
98114
GenerateUniqueRowFilter(node.UniqueRowFilter, false);

Test/SqlDom/Baselines170/JsonFunctionTests170.sql

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,22 @@ AS
134134
SELECT JSON_OBJECTAGG(c1:c2) AS jsoncontents
135135
FROM (VALUES ('key1', 'c'), ('key2', 'b'), ('key3', 'a')) AS t(c1, c2);
136136

137+
138+
GO
139+
SELECT JSON_QUERY('{ "a": 1 }');
140+
141+
SELECT JSON_QUERY('{ "a": 1 }', '$.a');
142+
143+
SELECT JSON_QUERY('{ "a": [1,2,3] }', '$.a') WITH ARRAY WRAPPER;
144+
145+
146+
GO
147+
CREATE VIEW dbo.jsonfunctest
148+
AS
149+
SELECT JSON_OBJECTAGG(c1:c2) AS jsoncontents
150+
FROM (VALUES ('key1', 'c'), ('key2', 'b'), ('key3', 'a')) AS t(c1, c2);
151+
152+
137153
GO
138154
SELECT TOP (5) c.object_id,
139155
JSON_OBJECTAGG(c.name:c.column_id) AS columns

Test/SqlDom/Only170SyntaxTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public partial class SqlDomTests
1919
new ParserTest170("CreateColumnStoreIndexTests170.sql", nErrors80: 3, nErrors90: 3, nErrors100: 3, nErrors110: 3, nErrors120: 3, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0),
2020
new ParserTest170("RegexpTests170.sql", nErrors80: 0, nErrors90: 0, nErrors100: 0, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0),
2121
new ParserTest170("AiGenerateChunksTests170.sql", nErrors80: 19, nErrors90: 16, nErrors100: 15, nErrors110: 15, nErrors120: 15, nErrors130: 15, nErrors140: 15, nErrors150: 15, nErrors160: 15),
22-
new ParserTest170("JsonFunctionTests170.sql", nErrors80: 11, nErrors90: 8, nErrors100: 36, nErrors110: 36, nErrors120: 36, nErrors130: 36, nErrors140: 36, nErrors150: 36, nErrors160: 36),
22+
new ParserTest170("JsonFunctionTests170.sql", nErrors80: 13, nErrors90: 8, nErrors100: 38, nErrors110: 38, nErrors120: 38, nErrors130: 38, nErrors140: 38, nErrors150: 38, nErrors160: 38),
2323
new ParserTest170("AiGenerateEmbeddingsTests170.sql", nErrors80: 12, nErrors90: 9, nErrors100: 9, nErrors110: 9, nErrors120: 9, nErrors130: 9, nErrors140: 9, nErrors150: 9, nErrors160: 9),
2424
new ParserTest170("CreateExternalModelStatementTests170.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 4, nErrors140: 4, nErrors150: 4, nErrors160: 4),
2525
new ParserTest170("AlterExternalModelStatementTests170.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 5, nErrors140: 5, nErrors150: 5, nErrors160: 5),

Test/SqlDom/ParserErrorsTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,43 @@ public void JsonArrayAggSyntaxNegativeTest()
577577
new ParserErrorInfo(34, "SQL46010", "NULL"));
578578
}
579579

580+
/// <summary>
581+
/// Negative tests for JSON_QUERY syntax in functions
582+
/// </summary>
583+
[TestMethod]
584+
[Priority(0)]
585+
[SqlStudioTestCategory(Category.UnitTest)]
586+
public void JsonQuerySyntaxNegativeTest()
587+
{
588+
// Cannot use WITH without ARRAY WRAPPER (incomplete syntax)
589+
ParserTestUtils.ErrorTest170("SELECT JSON_QUERY('{ \"a\": 1 }') WITH",
590+
new ParserErrorInfo(32, "SQL46010", "WITH"));
591+
592+
// Cannot use WITH ARRAY without WRAPPER (unexpected end of file)
593+
ParserTestUtils.ErrorTest170("SELECT JSON_QUERY('{ \"a\": 1 }') WITH ARRAY",
594+
new ParserErrorInfo(42, "SQL46029", ""));
595+
596+
// Cannot use WITH WRAPPER without ARRAY (unexpected end of file)
597+
ParserTestUtils.ErrorTest170("SELECT JSON_QUERY('{ \"a\": 1 }') WITH WRAPPER",
598+
new ParserErrorInfo(44, "SQL46029", ""));
599+
600+
// Cannot use incorrect keyword instead of ARRAY
601+
ParserTestUtils.ErrorTest170("SELECT JSON_QUERY('{ \"a\": 1 }') WITH OBJECT WRAPPER",
602+
new ParserErrorInfo(37, "SQL46010", "OBJECT"));
603+
604+
// Cannot use incorrect keyword instead of WRAPPER
605+
ParserTestUtils.ErrorTest170("SELECT JSON_QUERY('{ \"a\": 1 }') WITH ARRAY OBJECT",
606+
new ParserErrorInfo(43, "SQL46010", "OBJECT"));
607+
608+
// Cannot use JSON_QUERY with colon syntax (key:value pairs like JSON_OBJECT)
609+
ParserTestUtils.ErrorTest170("SELECT JSON_QUERY('name':'value')",
610+
new ParserErrorInfo(24, "SQL46010", ":"));
611+
612+
// WITH ARRAY WRAPPER must come after closing parenthesis, not before
613+
ParserTestUtils.ErrorTest170("SELECT JSON_QUERY('{ \"a\": 1 }' WITH ARRAY WRAPPER)",
614+
new ParserErrorInfo(31, "SQL46010", "WITH"));
615+
}
616+
580617
/// <summary>
581618
/// Negative tests for Data Masking Alter Column syntax.
582619
/// </summary>

Test/SqlDom/TestScripts/JsonFunctionTests170.sql

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -90,46 +90,59 @@ SELECT JSON_OBJECTAGG();
9090

9191
SELECT JSON_OBJECTAGG('name':1);
9292

93-
SELECT JSON_OBJECTAGG('name':JSON_ARRAY(1, 2));
93+
SELECT JSON_OBJECTAGG('name':JSON_ARRAY(1, 2));
9494
SELECT JSON_OBJECTAGG('name':'b' NULL ON NULL RETURNING JSON);
9595

9696
SELECT JSON_OBJECTAGG('name':'b' ABSENT ON NULL RETURNING JSON);
9797

9898
SELECT JSON_OBJECTAGG('name':'b' RETURNING JSON);
99-
99+
100100
SELECT JSON_ARRAYAGG('name');
101101

102-
SELECT JSON_ARRAYAGG('a');
103-
SELECT JSON_OBJECTAGG( c1:c2 )
104-
SELECT JSON_OBJECTAGG( c1:'c2' )
105-
106-
SELECT JSON_ARRAYAGG('a' NULL ON NULL);
107-
108-
SELECT JSON_ARRAYAGG('a' NULL ON NULL RETURNING JSON);
109-
102+
SELECT JSON_ARRAYAGG('a');
103+
SELECT JSON_OBJECTAGG( c1:c2 )
104+
SELECT JSON_OBJECTAGG( c1:'c2' )
105+
106+
SELECT JSON_ARRAYAGG('a' NULL ON NULL);
107+
108+
SELECT JSON_ARRAYAGG('a' NULL ON NULL RETURNING JSON);
109+
110110
SELECT s.session_id,
111111
JSON_ARRAYAGG(s.host_name)
112112
FROM sys.dm_exec_sessions AS s
113-
WHERE s.is_user_process = 1;
114-
113+
WHERE s.is_user_process = 1;
114+
115115
SELECT s.session_id,
116116
JSON_ARRAYAGG(s.host_name NULL ON NULL)
117117
FROM sys.dm_exec_sessions AS s
118-
WHERE s.is_user_process = 1;
119-
118+
WHERE s.is_user_process = 1;
119+
120120
SELECT s.session_id,
121121
JSON_ARRAYAGG(s.host_name NULL ON NULL RETURNING JSON)
122122
FROM sys.dm_exec_sessions AS s
123-
WHERE s.is_user_process = 1;
124-
125-
GO
126-
CREATE VIEW dbo.jsonfunctest AS
127-
SELECT JSON_OBJECTAGG( c1:c2 ) as jsoncontents
128-
FROM (
129-
VALUES('key1', 'c'), ('key2', 'b'), ('key3','a')
130-
) AS t(c1, c2);
131-
132-
GO
133-
SELECT TOP(5) c.object_id, JSON_OBJECTAGG(c.name:c.column_id) AS columns
134-
FROM sys.columns AS c
135-
GROUP BY c.object_id;
123+
WHERE s.is_user_process = 1;
124+
125+
GO
126+
CREATE VIEW dbo.jsonfunctest AS
127+
SELECT JSON_OBJECTAGG( c1:c2 ) as jsoncontents
128+
FROM (
129+
VALUES('key1', 'c'), ('key2', 'b'), ('key3','a')
130+
) AS t(c1, c2);
131+
132+
GO
133+
134+
SELECT JSON_QUERY('{ "a": 1 }');
135+
SELECT JSON_QUERY('{ "a": 1 }', '$.a');
136+
SELECT JSON_QUERY('{ "a": [1,2,3] }', '$.a') WITH ARRAY WRAPPER;
137+
138+
GO
139+
CREATE VIEW dbo.jsonfunctest AS
140+
SELECT JSON_OBJECTAGG( c1:c2 ) as jsoncontents
141+
FROM (
142+
VALUES('key1', 'c'), ('key2', 'b'), ('key3','a')
143+
) AS t(c1, c2);
144+
145+
GO
146+
SELECT TOP(5) c.object_id, JSON_OBJECTAGG(c.name:c.column_id) AS columns
147+
FROM sys.columns AS c
148+
GROUP BY c.object_id;

0 commit comments

Comments
 (0)