Skip to content

Commit 6857a21

Browse files
kyleconroyclaude
andcommitted
Add OPENJSON table reference support with WITH schema clause
- Create OpenJsonTableReference AST type for OPENJSON in FROM clause - Create SchemaDeclarationItemOpenjson for WITH clause column definitions - Handle OPENJSON specially in parseTableReferenceWithName() - Parse WITH clause including column name, data type, COLLATE, path mapping, AS JSON - Add marshaling for OpenJsonTableReference Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ab6e7a7 commit 6857a21

5 files changed

Lines changed: 179 additions & 2 deletions

File tree

ast/openjson_table_reference.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package ast
2+
3+
// OpenJsonTableReference represents an OPENJSON table reference in the FROM clause.
4+
type OpenJsonTableReference struct {
5+
Variable ScalarExpression `json:"Variable,omitempty"`
6+
RowPattern ScalarExpression `json:"RowPattern,omitempty"`
7+
SchemaDeclarationItems []*SchemaDeclarationItemOpenjson `json:"SchemaDeclarationItems,omitempty"`
8+
Alias *Identifier `json:"Alias,omitempty"`
9+
ForPath bool `json:"ForPath,omitempty"`
10+
}
11+
12+
func (*OpenJsonTableReference) node() {}
13+
func (*OpenJsonTableReference) tableReference() {}
14+
15+
// SchemaDeclarationItemOpenjson represents a column definition in OPENJSON WITH clause.
16+
type SchemaDeclarationItemOpenjson struct {
17+
AsJson bool `json:"AsJson,omitempty"`
18+
ColumnDefinition *ColumnDefinitionBase `json:"ColumnDefinition,omitempty"`
19+
Mapping ScalarExpression `json:"Mapping,omitempty"`
20+
}
21+
22+
func (*SchemaDeclarationItemOpenjson) node() {}

parser/marshal.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2749,6 +2749,50 @@ func tableReferenceToJSON(ref ast.TableReference) jsonNode {
27492749
}
27502750
node["ForPath"] = r.ForPath
27512751
return node
2752+
case *ast.OpenJsonTableReference:
2753+
node := jsonNode{
2754+
"$type": "OpenJsonTableReference",
2755+
}
2756+
if r.Variable != nil {
2757+
node["Variable"] = scalarExpressionToJSON(r.Variable)
2758+
}
2759+
if r.RowPattern != nil {
2760+
node["RowPattern"] = scalarExpressionToJSON(r.RowPattern)
2761+
}
2762+
if len(r.SchemaDeclarationItems) > 0 {
2763+
items := make([]jsonNode, len(r.SchemaDeclarationItems))
2764+
for i, item := range r.SchemaDeclarationItems {
2765+
itemNode := jsonNode{
2766+
"$type": "SchemaDeclarationItemOpenjson",
2767+
}
2768+
itemNode["AsJson"] = item.AsJson
2769+
if item.ColumnDefinition != nil {
2770+
colDef := jsonNode{
2771+
"$type": "ColumnDefinitionBase",
2772+
}
2773+
if item.ColumnDefinition.ColumnIdentifier != nil {
2774+
colDef["ColumnIdentifier"] = identifierToJSON(item.ColumnDefinition.ColumnIdentifier)
2775+
}
2776+
if item.ColumnDefinition.DataType != nil {
2777+
colDef["DataType"] = dataTypeReferenceToJSON(item.ColumnDefinition.DataType)
2778+
}
2779+
if item.ColumnDefinition.Collation != nil {
2780+
colDef["Collation"] = identifierToJSON(item.ColumnDefinition.Collation)
2781+
}
2782+
itemNode["ColumnDefinition"] = colDef
2783+
}
2784+
if item.Mapping != nil {
2785+
itemNode["Mapping"] = scalarExpressionToJSON(item.Mapping)
2786+
}
2787+
items[i] = itemNode
2788+
}
2789+
node["SchemaDeclarationItems"] = items
2790+
}
2791+
if r.Alias != nil {
2792+
node["Alias"] = identifierToJSON(r.Alias)
2793+
}
2794+
node["ForPath"] = r.ForPath
2795+
return node
27522796
case *ast.BuiltInFunctionTableReference:
27532797
node := jsonNode{
27542798
"$type": "BuiltInFunctionTableReference",

parser/parse_select.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2512,6 +2512,10 @@ func (p *Parser) parseSingleTableReference() (ast.TableReference, error) {
25122512
ForPath: false,
25132513
}, nil
25142514
}
2515+
// Handle OPENJSON specially
2516+
if upper == "OPENJSON" {
2517+
return p.parseOpenJsonTableReference(params, alias)
2518+
}
25152519
}
25162520

25172521
ref := &ast.SchemaObjectFunctionTableReference{
@@ -2528,6 +2532,113 @@ func (p *Parser) parseSingleTableReference() (ast.TableReference, error) {
25282532
return p.parseNamedTableReferenceWithName(son)
25292533
}
25302534

2535+
// parseOpenJsonTableReference parses OPENJSON function with optional WITH clause
2536+
func (p *Parser) parseOpenJsonTableReference(params []ast.ScalarExpression, alias *ast.Identifier) (ast.TableReference, error) {
2537+
ref := &ast.OpenJsonTableReference{
2538+
ForPath: false,
2539+
Alias: alias,
2540+
}
2541+
2542+
// First parameter is the Variable (JSON expression)
2543+
if len(params) > 0 {
2544+
ref.Variable = params[0]
2545+
}
2546+
2547+
// Second parameter is the RowPattern (optional path expression)
2548+
if len(params) > 1 {
2549+
ref.RowPattern = params[1]
2550+
}
2551+
2552+
// Check for WITH clause (schema declaration)
2553+
if p.curTok.Type == TokenWith {
2554+
p.nextToken() // consume WITH
2555+
if p.curTok.Type != TokenLParen {
2556+
return nil, fmt.Errorf("expected ( after OPENJSON WITH, got %s", p.curTok.Literal)
2557+
}
2558+
p.nextToken() // consume (
2559+
2560+
// Parse schema declaration items
2561+
for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF {
2562+
item, err := p.parseSchemaDeclarationItemOpenjson()
2563+
if err != nil {
2564+
return nil, err
2565+
}
2566+
ref.SchemaDeclarationItems = append(ref.SchemaDeclarationItems, item)
2567+
2568+
if p.curTok.Type == TokenComma {
2569+
p.nextToken()
2570+
} else {
2571+
break
2572+
}
2573+
}
2574+
2575+
if p.curTok.Type == TokenRParen {
2576+
p.nextToken() // consume )
2577+
}
2578+
}
2579+
2580+
// Parse optional alias after WITH clause
2581+
if ref.Alias == nil {
2582+
if p.curTok.Type == TokenAs {
2583+
p.nextToken()
2584+
ref.Alias = p.parseIdentifier()
2585+
} else if p.curTok.Type == TokenIdent {
2586+
upper := strings.ToUpper(p.curTok.Literal)
2587+
if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "WINDOW" && upper != "ORDER" &&
2588+
upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" &&
2589+
upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" &&
2590+
upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" {
2591+
ref.Alias = p.parseIdentifier()
2592+
}
2593+
}
2594+
}
2595+
2596+
return ref, nil
2597+
}
2598+
2599+
// parseSchemaDeclarationItemOpenjson parses a column definition in OPENJSON WITH clause
2600+
func (p *Parser) parseSchemaDeclarationItemOpenjson() (*ast.SchemaDeclarationItemOpenjson, error) {
2601+
item := &ast.SchemaDeclarationItemOpenjson{
2602+
ColumnDefinition: &ast.ColumnDefinitionBase{},
2603+
}
2604+
2605+
// Parse column name
2606+
item.ColumnDefinition.ColumnIdentifier = p.parseIdentifier()
2607+
2608+
// Parse data type
2609+
dataType, err := p.parseDataTypeReference()
2610+
if err != nil {
2611+
return nil, err
2612+
}
2613+
item.ColumnDefinition.DataType = dataType
2614+
2615+
// Parse optional COLLATE
2616+
if strings.ToUpper(p.curTok.Literal) == "COLLATE" {
2617+
p.nextToken() // consume COLLATE
2618+
item.ColumnDefinition.Collation = p.parseIdentifier()
2619+
}
2620+
2621+
// Parse optional path mapping (string literal) or AS JSON
2622+
if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString {
2623+
mapping, err := p.parseScalarExpression()
2624+
if err != nil {
2625+
return nil, err
2626+
}
2627+
item.Mapping = mapping
2628+
}
2629+
2630+
// Parse optional AS JSON
2631+
if p.curTok.Type == TokenAs {
2632+
p.nextToken() // consume AS
2633+
if strings.ToUpper(p.curTok.Literal) == "JSON" {
2634+
item.AsJson = true
2635+
p.nextToken() // consume JSON
2636+
}
2637+
}
2638+
2639+
return item, nil
2640+
}
2641+
25312642
// parseDerivedTableReference parses a derived table (parenthesized query) like (SELECT ...) AS alias
25322643
// or an inline derived table (VALUES clause) like (VALUES (...), (...)) AS alias(cols)
25332644
// or a data modification table reference (DML with OUTPUT) like (INSERT ... OUTPUT ...) AS alias
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}

0 commit comments

Comments
 (0)