Skip to content

Commit 0a26b6d

Browse files
Copilotllalizijchen
authored
Add support for JSON Index creation syntax in SQL Server 2025 (TSql170) (#147)
* Initial plan * Initial planning and exploration for JSON Index support Co-authored-by: llali <[email protected]> * Add JSON Index support - AST definitions, grammar rules, and script generation Co-authored-by: llali <[email protected]> * Complete JSON Index implementation - fix parser recognition and add correct baseline Co-authored-by: llali <[email protected]> * Final fix - correct error count expectations for older SQL parser versions Co-authored-by: llali <[email protected]> * Remove antlr-2.7.5.jar file from repository Co-authored-by: llali <[email protected]> * Remove tmp folder from repository and add to .gitignore Co-authored-by: llali <[email protected]> * Revert global.json SDK version back to 8.0.410 Co-authored-by: llali <[email protected]> * Remove NUL file - ANTLR build artifact Co-authored-by: llali <[email protected]> * Use CreateXmlIndexStatement pattern - inherit from IndexStatement instead of TSqlStatement Co-authored-by: llali <[email protected]> * Address reviewer feedback: rename JsonPaths to ForJsonPaths and add comprehensive error tests Co-authored-by: zijchen <[email protected]> * Fix error test position calculations for accurate error reporting Co-authored-by: zijchen <[email protected]> * Change JSON Index column from ColumnReferenceExpression to Identifier type Co-authored-by: llali <[email protected]> * Remove ErrorTest90/ErrorTest100 calls and fix missing comma in Only170SyntaxTests Co-authored-by: llali <[email protected]> * Fix parser error test offsets - correct positions for JSON keyword errors Co-authored-by: llali <[email protected]> * Remove ErrorTest80 call from ParserErrorsTests as requested Co-authored-by: llali <[email protected]> * Remove ErrorTest160 calls from ParserErrorsTests for JSON Index - only TSql170 tests needed Co-authored-by: zijchen <[email protected]> * fixing the test * Remove filegroup support from JSON Index syntax Co-authored-by: llali <[email protected]> * Fix JSON Index error count in Only170SyntaxTests - correct to 8 errors after filegroup removal Co-authored-by: llali <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: llali <[email protected]> Co-authored-by: zijchen <[email protected]> Co-authored-by: Leila Lali <[email protected]>
1 parent 003271e commit 0a26b6d

File tree

9 files changed

+230
-2
lines changed

9 files changed

+230
-2
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,4 +356,7 @@ MigrationBackup/
356356
out/
357357

358358
# Project packages folder
359-
.packages/
359+
.packages/
360+
361+
# Temporary build artifacts
362+
tmp/

SqlScriptDom/Parser/TSql/Ast.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4600,6 +4600,13 @@
46004600
<Member Name="OnFileGroupOrPartitionScheme" Type="FileGroupOrPartitionScheme" Summary="The filegroup or partition scheme. Might be null."/>
46014601
<Member Name="OrderedColumns" Type="ColumnReferenceExpression" Collection="true" Summary="The columns which ordered columnstore indexes should be ordered on." />
46024602
</Class>
4603+
<Class Name="CreateJsonIndexStatement" Base="IndexStatement" Summary="Represents the create JSON index statement.">
4604+
<InheritedMember Name="Name" ContainerClass="IndexStatement" />
4605+
<InheritedMember Name="OnName" ContainerClass="IndexStatement" />
4606+
<Member Name="JsonColumn" Type="Identifier" Summary="The JSON column for the index."/>
4607+
<Member Name="ForJsonPaths" Type="StringLiteral" Collection="true" Summary="The JSON paths specified in the FOR clause. Optional may have zero elements."/>
4608+
<InheritedMember Name="IndexOptions" ContainerClass="IndexStatement" />
4609+
</Class>
46034610
<Class Name="WindowFrameClause" Summary="Represents the specification of window bounds for windowing aggregates.">
46044611
<Member Name="Top" Type="WindowDelimiter" Summary="Top boundary of the window."/>
46054612
<Member Name="Bottom" Type="WindowDelimiter" Summary="Bottom boundary of the window. Optional may be null."/>

SqlScriptDom/Parser/TSql/TSql170.g

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,9 @@ create2005Statements returns [TSqlStatement vResult = null]
883883
|
884884
{NextTokenMatches(CodeGenerationSupporter.ColumnStore)}?
885885
vResult=createColumnStoreIndexStatement[null, null]
886+
|
887+
{NextTokenMatches(CodeGenerationSupporter.Json)}?
888+
vResult=createJsonIndexStatement[null, null]
886889
|
887890
{NextTokenMatches(CodeGenerationSupporter.Contract)}?
888891
vResult=createContractStatement
@@ -16844,6 +16847,7 @@ createIndexStatement returns [TSqlStatement vResult = null]
1684416847
(
1684516848
vResult=createRelationalIndexStatement[tUnique, isClustered]
1684616849
| vResult=createColumnStoreIndexStatement[tUnique, isClustered]
16850+
| vResult=createJsonIndexStatement[tUnique, isClustered]
1684716851
)
1684816852
)
1684916853
|
@@ -16980,6 +16984,58 @@ createColumnStoreIndexStatement [IToken tUnique, bool? isClustered] returns [Cre
1698016984
)?
1698116985
;
1698216986

16987+
createJsonIndexStatement [IToken tUnique, bool? isClustered] returns [CreateJsonIndexStatement vResult = FragmentFactory.CreateFragment<CreateJsonIndexStatement>()]
16988+
{
16989+
Identifier vIdentifier;
16990+
SchemaObjectName vSchemaObjectName;
16991+
Identifier vJsonColumn;
16992+
StringLiteral vPath;
16993+
16994+
if (tUnique != null)
16995+
{
16996+
ThrowIncorrectSyntaxErrorException(tUnique);
16997+
}
16998+
if (isClustered.HasValue)
16999+
{
17000+
ThrowIncorrectSyntaxErrorException(LT(1));
17001+
}
17002+
}
17003+
: tJson:Identifier tIndex:Index vIdentifier=identifier
17004+
{
17005+
Match(tJson, CodeGenerationSupporter.Json);
17006+
vResult.Name = vIdentifier;
17007+
}
17008+
tOn:On vSchemaObjectName=schemaObjectThreePartName
17009+
{
17010+
vResult.OnName = vSchemaObjectName;
17011+
}
17012+
LeftParenthesis vJsonColumn=identifier tRParen:RightParenthesis
17013+
{
17014+
vResult.JsonColumn = vJsonColumn;
17015+
UpdateTokenInfo(vResult, tRParen);
17016+
}
17017+
(
17018+
tFor:For LeftParenthesis
17019+
vPath=stringLiteral
17020+
{
17021+
AddAndUpdateTokenInfo(vResult, vResult.ForJsonPaths, vPath);
17022+
}
17023+
(
17024+
Comma vPath=stringLiteral
17025+
{
17026+
AddAndUpdateTokenInfo(vResult, vResult.ForJsonPaths, vPath);
17027+
}
17028+
)*
17029+
RightParenthesis
17030+
)?
17031+
(
17032+
// Greedy due to conflict with withCommonTableExpressionsAndXmlNamespaces
17033+
options {greedy = true; } :
17034+
With
17035+
indexOptionList[IndexAffectingStatement.CreateIndex, vResult.IndexOptions, vResult]
17036+
)?
17037+
;
17038+
1698317039
indexKeyColumnList[CreateIndexStatement vParent]
1698417040
{
1698517041
ColumnWithSortOrder vColumnWithSortOrder;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//------------------------------------------------------------------------------
2+
// <copyright file="SqlScriptGeneratorVisitor.CreateJsonIndexStatement.cs" company="Microsoft">
3+
// Copyright (c) Microsoft Corporation. All rights reserved.
4+
// </copyright>
5+
//------------------------------------------------------------------------------
6+
using System.Collections.Generic;
7+
using Microsoft.SqlServer.TransactSql.ScriptDom;
8+
9+
namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator
10+
{
11+
partial class SqlScriptGeneratorVisitor
12+
{
13+
public override void ExplicitVisit(CreateJsonIndexStatement node)
14+
{
15+
GenerateKeyword(TSqlTokenType.Create);
16+
17+
GenerateSpaceAndIdentifier(CodeGenerationSupporter.Json);
18+
19+
GenerateSpaceAndKeyword(TSqlTokenType.Index);
20+
21+
// name
22+
GenerateSpaceAndFragmentIfNotNull(node.Name);
23+
24+
NewLineAndIndent();
25+
GenerateKeyword(TSqlTokenType.On);
26+
GenerateSpaceAndFragmentIfNotNull(node.OnName);
27+
28+
// JSON column
29+
if (node.JsonColumn != null)
30+
{
31+
GenerateSpace();
32+
GenerateSymbol(TSqlTokenType.LeftParenthesis);
33+
GenerateFragmentIfNotNull(node.JsonColumn);
34+
GenerateSymbol(TSqlTokenType.RightParenthesis);
35+
}
36+
37+
// FOR clause with JSON paths
38+
if (node.ForJsonPaths != null && node.ForJsonPaths.Count > 0)
39+
{
40+
NewLineAndIndent();
41+
GenerateKeyword(TSqlTokenType.For);
42+
GenerateSpace();
43+
GenerateParenthesisedCommaSeparatedList(node.ForJsonPaths);
44+
}
45+
46+
GenerateIndexOptions(node.IndexOptions);
47+
}
48+
}
49+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
CREATE JSON INDEX IX_JSON_Basic
2+
ON dbo.Users (JsonData);
3+
4+
CREATE JSON INDEX IX_JSON_SinglePath
5+
ON dbo.Users (JsonData)
6+
FOR ('$.name');
7+
8+
CREATE JSON INDEX IX_JSON_MultiplePaths
9+
ON dbo.Users (JsonData)
10+
FOR ('$.name', '$.email', '$.age');
11+
12+
CREATE JSON INDEX IX_JSON_WithOptions
13+
ON dbo.Users (JsonData) WITH (FILLFACTOR = 90, ONLINE = OFF);
14+
15+
CREATE JSON INDEX IX_JSON_Complete
16+
ON dbo.Users (JsonData)
17+
FOR ('$.profile.name', '$.profile.email') WITH (MAXDOP = 4, DATA_COMPRESSION = ROW);
18+
19+
CREATE JSON INDEX IX_JSON_Schema
20+
ON MySchema.MyTable (JsonColumn)
21+
FOR ('$.properties.value');
22+
23+
CREATE JSON INDEX [IX JSON Index]
24+
ON [dbo].[Users] ([Json Data])
25+
FOR ('$.data.attributes');
26+
27+
CREATE JSON INDEX IX_JSON_Complex
28+
ON dbo.Documents (Content)
29+
FOR ('$.metadata.title', '$.content.sections[*].text', '$.tags[*]');

Test/SqlDom/Only170SyntaxTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public partial class SqlDomTests
1010
private static readonly ParserTest[] Only170TestInfos =
1111
{
1212
new ParserTest170("RegexpTVFTests170.sql", nErrors80: 1, nErrors90: 1, nErrors100: 0, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0),
13+
new ParserTest170("JsonIndexTests170.sql", nErrors80: 2, nErrors90: 8, nErrors100: 8, nErrors110: 8, nErrors120: 8, nErrors130: 8, nErrors140: 8, nErrors150: 8, nErrors160: 8),
1314
new ParserTest170("AlterDatabaseManualCutoverTests170.sql", nErrors80: 4, nErrors90: 4, nErrors100: 4, nErrors110: 4, nErrors120: 4, nErrors130: 4, nErrors140: 4, nErrors150: 4, nErrors160: 4),
1415
new ParserTest170("CreateColumnStoreIndexTests170.sql", nErrors80: 3, nErrors90: 3, nErrors100: 3, nErrors110: 3, nErrors120: 3, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0)
1516
};

Test/SqlDom/ParserErrorsTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4384,6 +4384,58 @@ public void CreateIndexStatementErrorTest()
43844384
new ParserErrorInfo(47, "SQL46010", "col1"));
43854385
}
43864386

4387+
/// <summary>
4388+
/// JSON Index error tests - ensure JSON Index syntax is rejected in older versions and malformed syntax produces appropriate errors
4389+
/// </summary>
4390+
[TestMethod]
4391+
[Priority(0)]
4392+
[SqlStudioTestCategory(Category.UnitTest)]
4393+
public void CreateJsonIndexStatementErrorTest()
4394+
{
4395+
// JSON Index syntax should not be supported in SQL Server versions prior to 2025 (TSql170)
4396+
// Test basic JSON Index syntax in older versions
4397+
ParserTestUtils.ErrorTest160("CREATE JSON INDEX idx1 ON table1 (jsonColumn)",
4398+
new ParserErrorInfo(7, "SQL46010", "JSON"));
4399+
ParserTestUtils.ErrorTest150("CREATE JSON INDEX idx1 ON table1 (jsonColumn)",
4400+
new ParserErrorInfo(7, "SQL46010", "JSON"));
4401+
ParserTestUtils.ErrorTest140("CREATE JSON INDEX idx1 ON table1 (jsonColumn)",
4402+
new ParserErrorInfo(7, "SQL46010", "JSON"));
4403+
ParserTestUtils.ErrorTest130("CREATE JSON INDEX idx1 ON table1 (jsonColumn)",
4404+
new ParserErrorInfo(7, "SQL46010", "JSON"));
4405+
ParserTestUtils.ErrorTest120("CREATE JSON INDEX idx1 ON table1 (jsonColumn)",
4406+
new ParserErrorInfo(7, "SQL46010", "JSON"));
4407+
ParserTestUtils.ErrorTest110("CREATE JSON INDEX idx1 ON table1 (jsonColumn)",
4408+
new ParserErrorInfo(7, "SQL46010", "JSON"));
4409+
4410+
4411+
4412+
// Test that UNIQUE and CLUSTERED/NONCLUSTERED are not allowed with JSON indexes in TSql170
4413+
TSql170Parser parser170 = new TSql170Parser(true);
4414+
ParserTestUtils.ErrorTest(parser170, "CREATE UNIQUE JSON INDEX idx1 ON table1 (jsonColumn)",
4415+
new ParserErrorInfo(14, "SQL46010", "JSON"));
4416+
ParserTestUtils.ErrorTest(parser170, "CREATE CLUSTERED JSON INDEX idx1 ON table1 (jsonColumn)",
4417+
new ParserErrorInfo(17, "SQL46005", "COLUMNSTORE", "JSON"));
4418+
ParserTestUtils.ErrorTest(parser170, "CREATE NONCLUSTERED JSON INDEX idx1 ON table1 (jsonColumn)",
4419+
new ParserErrorInfo(20, "SQL46005", "COLUMNSTORE", "JSON"));
4420+
4421+
// Test malformed JSON Index syntax in TSql170
4422+
// Missing column specification
4423+
ParserTestUtils.ErrorTest(parser170, "CREATE JSON INDEX idx1 ON table1",
4424+
new ParserErrorInfo(32, "SQL46029"));
4425+
4426+
// Empty FOR clause
4427+
ParserTestUtils.ErrorTest(parser170, "CREATE JSON INDEX idx1 ON table1 (jsonColumn) FOR ()",
4428+
new ParserErrorInfo(51, "SQL46010", ")"));
4429+
4430+
// Invalid JSON path (missing quotes)
4431+
ParserTestUtils.ErrorTest(parser170, "CREATE JSON INDEX idx1 ON table1 (jsonColumn) FOR ($.name)",
4432+
new ParserErrorInfo(51, "SQL46010", "$"));
4433+
4434+
// Missing table name
4435+
ParserTestUtils.ErrorTest(parser170, "CREATE JSON INDEX idx1 ON (jsonColumn)",
4436+
new ParserErrorInfo(26, "SQL46010", "("));
4437+
}
4438+
43874439
/// <summary>
43884440
/// Check that the value of MAXDOP index option is within range
43894441
/// </summary>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
-- Basic JSON index creation
2+
CREATE JSON INDEX IX_JSON_Basic ON dbo.Users (JsonData);
3+
4+
-- JSON index with FOR clause (single path)
5+
CREATE JSON INDEX IX_JSON_SinglePath ON dbo.Users (JsonData)
6+
FOR ('$.name');
7+
8+
-- JSON index with FOR clause (multiple paths)
9+
CREATE JSON INDEX IX_JSON_MultiplePaths ON dbo.Users (JsonData)
10+
FOR ('$.name', '$.email', '$.age');
11+
12+
-- JSON index with WITH options
13+
CREATE JSON INDEX IX_JSON_WithOptions ON dbo.Users (JsonData)
14+
WITH (FILLFACTOR = 90, ONLINE = OFF);
15+
16+
-- JSON index with FOR clause and WITH options
17+
CREATE JSON INDEX IX_JSON_Complete ON dbo.Users (JsonData)
18+
FOR ('$.profile.name', '$.profile.email')
19+
WITH (MAXDOP = 4, DATA_COMPRESSION = ROW);
20+
21+
-- JSON index on schema-qualified table
22+
CREATE JSON INDEX IX_JSON_Schema ON MySchema.MyTable (JsonColumn)
23+
FOR ('$.properties.value');
24+
25+
-- JSON index with quoted identifiers
26+
CREATE JSON INDEX [IX JSON Index] ON [dbo].[Users] ([Json Data])
27+
FOR ('$.data.attributes');
28+
29+
-- JSON index with complex path expressions
30+
CREATE JSON INDEX IX_JSON_Complex ON dbo.Documents (Content)
31+
FOR ('$.metadata.title', '$.content.sections[*].text', '$.tags[*]');

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "8.0.411",
3+
"version": "8.0.117",
44
"rollForward": "latestMajor"
55
},
66
"msbuild-sdks": {

0 commit comments

Comments
 (0)