Skip to content

Commit 9a43ab3

Browse files
kyleconroyclaude
andcommitted
Add JSON_OBJECT and JSON_ARRAY function parsing support
- Add JsonKeyValue AST type for JSON_OBJECT key:value pairs - Add JsonParameters and AbsentOrNullOnNull fields to FunctionCall - Implement parseJsonObjectCall for JSON_OBJECT('key':value, ...) syntax - Implement parseJsonArrayCall for JSON_ARRAY(value1, value2, ...) syntax - Support NULL ON NULL and ABSENT ON NULL modifiers - Fix GlobalVariableExpression handling in SELECT elements (@@spid) - Enable Baselines160_JsonFunctionTests160 and JsonFunctionTests160 tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d7016af commit 9a43ab3

File tree

5 files changed

+184
-4
lines changed

5 files changed

+184
-4
lines changed

ast/function_call.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ type WithinGroupClause struct {
5959

6060
func (*WithinGroupClause) node() {}
6161

62+
// JsonKeyValue represents a key-value pair in JSON_OBJECT function
63+
type JsonKeyValue struct {
64+
JsonKeyName ScalarExpression `json:"JsonKeyName,omitempty"`
65+
JsonValue ScalarExpression `json:"JsonValue,omitempty"`
66+
}
67+
68+
func (*JsonKeyValue) node() {}
69+
6270
// FunctionCall represents a function call.
6371
type FunctionCall struct {
6472
CallTarget CallTarget `json:"CallTarget,omitempty"`
@@ -71,6 +79,8 @@ type FunctionCall struct {
7179
WithArrayWrapper bool `json:"WithArrayWrapper,omitempty"`
7280
TrimOptions *Identifier `json:"TrimOptions,omitempty"` // For TRIM(LEADING/TRAILING/BOTH chars FROM string)
7381
Collation *Identifier `json:"Collation,omitempty"`
82+
JsonParameters []*JsonKeyValue `json:"JsonParameters,omitempty"` // For JSON_OBJECT function key:value pairs
83+
AbsentOrNullOnNull []*Identifier `json:"AbsentOrNullOnNull,omitempty"` // For JSON_OBJECT/JSON_ARRAY NULL ON NULL or ABSENT ON NULL
7484
}
7585

7686
func (*FunctionCall) node() {}

parser/marshal.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1950,6 +1950,24 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode {
19501950
if e.Collation != nil {
19511951
node["Collation"] = identifierToJSON(e.Collation)
19521952
}
1953+
if len(e.JsonParameters) > 0 {
1954+
params := make([]jsonNode, len(e.JsonParameters))
1955+
for i, kv := range e.JsonParameters {
1956+
params[i] = jsonNode{
1957+
"$type": "JsonKeyValue",
1958+
"JsonKeyName": scalarExpressionToJSON(kv.JsonKeyName),
1959+
"JsonValue": scalarExpressionToJSON(kv.JsonValue),
1960+
}
1961+
}
1962+
node["JsonParameters"] = params
1963+
}
1964+
if len(e.AbsentOrNullOnNull) > 0 {
1965+
idents := make([]jsonNode, len(e.AbsentOrNullOnNull))
1966+
for i, ident := range e.AbsentOrNullOnNull {
1967+
idents[i] = identifierToJSON(ident)
1968+
}
1969+
node["AbsentOrNullOnNull"] = idents
1970+
}
19531971
return node
19541972
case *ast.PartitionFunctionCall:
19551973
node := jsonNode{

parser/parse_select.go

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,10 +471,15 @@ func (p *Parser) parseSelectElement() (ast.SelectElement, error) {
471471
}
472472

473473
// Not an assignment, treat as regular scalar expression starting with variable
474-
varRef := &ast.VariableReference{Name: varName}
474+
var varExpr ast.ScalarExpression
475+
if strings.HasPrefix(varName, "@@") {
476+
varExpr = &ast.GlobalVariableExpression{Name: varName}
477+
} else {
478+
varExpr = &ast.VariableReference{Name: varName}
479+
}
475480

476481
// Handle postfix operations (method calls, property access)
477-
expr, err := p.handlePostfixOperations(varRef)
482+
expr, err := p.handlePostfixOperations(varExpr)
478483
if err != nil {
479484
return nil, err
480485
}
@@ -1899,6 +1904,10 @@ func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier)
18991904
return p.parseParseCall(false)
19001905
case "TRY_PARSE":
19011906
return p.parseParseCall(true)
1907+
case "JSON_OBJECT":
1908+
return p.parseJsonObjectCall()
1909+
case "JSON_ARRAY":
1910+
return p.parseJsonArrayCall()
19021911
}
19031912
}
19041913

@@ -5866,6 +5875,149 @@ func (p *Parser) parseParseCall(isTry bool) (ast.ScalarExpression, error) {
58665875
}, nil
58675876
}
58685877

5878+
// parseJsonObjectCall parses JSON_OBJECT('key':value, 'key2':value2, ... [NULL|ABSENT ON NULL])
5879+
func (p *Parser) parseJsonObjectCall() (*ast.FunctionCall, error) {
5880+
fc := &ast.FunctionCall{
5881+
FunctionName: &ast.Identifier{Value: "JSON_OBJECT", QuoteType: "NotQuoted"},
5882+
UniqueRowFilter: "NotSpecified",
5883+
WithArrayWrapper: false,
5884+
}
5885+
5886+
p.nextToken() // consume (
5887+
5888+
// Parse key-value pairs
5889+
for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF {
5890+
// Check for NULL ON NULL or ABSENT ON NULL at start of loop
5891+
upperLit := strings.ToUpper(p.curTok.Literal)
5892+
if upperLit == "NULL" || upperLit == "ABSENT" {
5893+
// Look ahead to see if this is "NULL ON NULL" or "ABSENT ON NULL"
5894+
if p.peekIsOnNull() {
5895+
fc.AbsentOrNullOnNull = append(fc.AbsentOrNullOnNull, &ast.Identifier{Value: upperLit, QuoteType: "NotQuoted"})
5896+
p.nextToken() // consume NULL or ABSENT
5897+
p.nextToken() // consume ON
5898+
p.nextToken() // consume NULL
5899+
continue
5900+
}
5901+
}
5902+
5903+
// Parse key expression
5904+
keyExpr, err := p.parseScalarExpression()
5905+
if err != nil {
5906+
return nil, err
5907+
}
5908+
5909+
// Check for : (JSON key-value separator)
5910+
if p.curTok.Type == TokenColon {
5911+
p.nextToken() // consume :
5912+
5913+
// Parse value expression
5914+
valueExpr, err := p.parseScalarExpression()
5915+
if err != nil {
5916+
return nil, err
5917+
}
5918+
5919+
fc.JsonParameters = append(fc.JsonParameters, &ast.JsonKeyValue{
5920+
JsonKeyName: keyExpr,
5921+
JsonValue: valueExpr,
5922+
})
5923+
} else {
5924+
// Just a regular parameter without colon (shouldn't happen for JSON_OBJECT)
5925+
fc.Parameters = append(fc.Parameters, keyExpr)
5926+
}
5927+
5928+
// After parsing a value, check for NULL ON NULL or ABSENT ON NULL
5929+
postValueLit := strings.ToUpper(p.curTok.Literal)
5930+
if postValueLit == "NULL" || postValueLit == "ABSENT" {
5931+
if p.peekIsOnNull() {
5932+
fc.AbsentOrNullOnNull = append(fc.AbsentOrNullOnNull, &ast.Identifier{Value: postValueLit, QuoteType: "NotQuoted"})
5933+
p.nextToken() // consume NULL or ABSENT
5934+
p.nextToken() // consume ON
5935+
p.nextToken() // consume NULL
5936+
// Continue to check for ) or comma
5937+
}
5938+
}
5939+
5940+
if p.curTok.Type == TokenComma {
5941+
p.nextToken() // consume ,
5942+
} else {
5943+
break
5944+
}
5945+
}
5946+
5947+
if p.curTok.Type != TokenRParen {
5948+
return nil, fmt.Errorf("expected ) in JSON_OBJECT, got %s", p.curTok.Literal)
5949+
}
5950+
p.nextToken() // consume )
5951+
5952+
return fc, nil
5953+
}
5954+
5955+
// parseJsonArrayCall parses JSON_ARRAY(value1, value2, ... [NULL|ABSENT ON NULL])
5956+
func (p *Parser) parseJsonArrayCall() (*ast.FunctionCall, error) {
5957+
fc := &ast.FunctionCall{
5958+
FunctionName: &ast.Identifier{Value: "JSON_ARRAY", QuoteType: "NotQuoted"},
5959+
UniqueRowFilter: "NotSpecified",
5960+
WithArrayWrapper: false,
5961+
}
5962+
5963+
p.nextToken() // consume (
5964+
5965+
// Parse array elements
5966+
for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF {
5967+
// Check for NULL ON NULL or ABSENT ON NULL at start of loop
5968+
upperLit := strings.ToUpper(p.curTok.Literal)
5969+
if upperLit == "NULL" || upperLit == "ABSENT" {
5970+
// Look ahead to see if this is "NULL ON NULL" or "ABSENT ON NULL"
5971+
if p.peekIsOnNull() {
5972+
fc.AbsentOrNullOnNull = append(fc.AbsentOrNullOnNull, &ast.Identifier{Value: upperLit, QuoteType: "NotQuoted"})
5973+
p.nextToken() // consume NULL or ABSENT
5974+
p.nextToken() // consume ON
5975+
p.nextToken() // consume NULL
5976+
continue
5977+
}
5978+
}
5979+
5980+
// Parse value expression
5981+
valueExpr, err := p.parseScalarExpression()
5982+
if err != nil {
5983+
return nil, err
5984+
}
5985+
fc.Parameters = append(fc.Parameters, valueExpr)
5986+
5987+
// After parsing a value, check for NULL ON NULL or ABSENT ON NULL
5988+
postValueLit := strings.ToUpper(p.curTok.Literal)
5989+
if postValueLit == "NULL" || postValueLit == "ABSENT" {
5990+
if p.peekIsOnNull() {
5991+
fc.AbsentOrNullOnNull = append(fc.AbsentOrNullOnNull, &ast.Identifier{Value: postValueLit, QuoteType: "NotQuoted"})
5992+
p.nextToken() // consume NULL or ABSENT
5993+
p.nextToken() // consume ON
5994+
p.nextToken() // consume NULL
5995+
// Continue to check for ) or comma
5996+
}
5997+
}
5998+
5999+
if p.curTok.Type == TokenComma {
6000+
p.nextToken() // consume ,
6001+
} else {
6002+
break
6003+
}
6004+
}
6005+
6006+
if p.curTok.Type != TokenRParen {
6007+
return nil, fmt.Errorf("expected ) in JSON_ARRAY, got %s", p.curTok.Literal)
6008+
}
6009+
p.nextToken() // consume )
6010+
6011+
return fc, nil
6012+
}
6013+
6014+
// peekIsOnNull checks if the next tokens are "ON NULL"
6015+
func (p *Parser) peekIsOnNull() bool {
6016+
// Just check if the next token is ON
6017+
// The caller will verify the NULL after ON when consuming
6018+
return p.peekTok.Type == TokenOn
6019+
}
6020+
58696021
// parseChangeTableReference parses CHANGETABLE(CHANGES ...) or CHANGETABLE(VERSION ...)
58706022
func (p *Parser) parseChangeTableReference() (ast.TableReference, error) {
58716023
p.nextToken() // consume CHANGETABLE
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)