Skip to content

Commit a869586

Browse files
kyleconroyclaude
andcommitted
Add graph DB pseudo column support and $ref handling
- Add graph pseudo columns ($node_id, $edge_id, $from_id, $to_id) to parsePrimaryExpression for general expression context - Add graph pseudo columns to parseColumnReferenceOrFunctionCall for CREATE STATISTICS and similar statements - Add graph pseudo columns to parseInlineIndexDefinition for inline indexes in CREATE TABLE statements - Add getPseudoColumnType helper function with all pseudo columns - Add $ref support for GraphMatchNodeExpression using pointer-based tracking to match ScriptDOM behavior for shared nodes Known issue: GraphDbSyntaxTests140 has remaining structural differences in nested AND expressions within MATCH clauses. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 628f301 commit a869586

3 files changed

Lines changed: 135 additions & 13 deletions

File tree

parser/marshal.go

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3593,33 +3593,83 @@ func booleanExpressionToJSON(expr ast.BooleanExpression) jsonNode {
35933593
}
35943594
}
35953595

3596+
// graphMatchContext tracks seen node pointers for $ref support
3597+
type graphMatchContext struct {
3598+
seenNodes map[*ast.GraphMatchNodeExpression]bool
3599+
}
3600+
3601+
func newGraphMatchContext() *graphMatchContext {
3602+
return &graphMatchContext{
3603+
seenNodes: make(map[*ast.GraphMatchNodeExpression]bool),
3604+
}
3605+
}
3606+
35963607
func graphMatchExpressionToJSON(expr ast.GraphMatchExpression) jsonNode {
3608+
ctx := newGraphMatchContext()
3609+
return graphMatchExpressionToJSONWithContext(expr, ctx)
3610+
}
3611+
3612+
func graphMatchExpressionToJSONWithContext(expr ast.GraphMatchExpression, ctx *graphMatchContext) jsonNode {
35973613
switch e := expr.(type) {
35983614
case *ast.GraphMatchCompositeExpression:
35993615
node := jsonNode{
36003616
"$type": "GraphMatchCompositeExpression",
36013617
}
36023618
if e.LeftNode != nil {
3603-
node["LeftNode"] = graphMatchNodeExpressionToJSON(e.LeftNode)
3619+
node["LeftNode"] = graphMatchNodeExpressionToJSONWithContext(e.LeftNode, ctx)
36043620
}
36053621
if e.Edge != nil {
36063622
node["Edge"] = identifierToJSON(e.Edge)
36073623
}
36083624
if e.RightNode != nil {
3609-
node["RightNode"] = graphMatchNodeExpressionToJSON(e.RightNode)
3625+
node["RightNode"] = graphMatchNodeExpressionToJSONWithContext(e.RightNode, ctx)
36103626
}
36113627
node["ArrowOnRight"] = e.ArrowOnRight
36123628
return node
36133629
case *ast.GraphMatchNodeExpression:
3614-
return graphMatchNodeExpressionToJSON(e)
3630+
return graphMatchNodeExpressionToJSONWithContext(e, ctx)
36153631
case *ast.BooleanBinaryExpression:
36163632
// Chained patterns produce BooleanBinaryExpression with And
3617-
return booleanExpressionToJSON(e)
3633+
return booleanBinaryExpressionToJSONWithGraphContext(e, ctx)
36183634
default:
36193635
return jsonNode{"$type": "UnknownGraphMatchExpression"}
36203636
}
36213637
}
36223638

3639+
func booleanBinaryExpressionToJSONWithGraphContext(e *ast.BooleanBinaryExpression, ctx *graphMatchContext) jsonNode {
3640+
node := jsonNode{
3641+
"$type": "BooleanBinaryExpression",
3642+
}
3643+
if e.BinaryExpressionType != "" {
3644+
node["BinaryExpressionType"] = e.BinaryExpressionType
3645+
}
3646+
if e.FirstExpression != nil {
3647+
// Check if first expression is a graph match expression type
3648+
switch firstExpr := e.FirstExpression.(type) {
3649+
case *ast.GraphMatchCompositeExpression:
3650+
node["FirstExpression"] = graphMatchExpressionToJSONWithContext(firstExpr, ctx)
3651+
case *ast.BooleanBinaryExpression:
3652+
// Could be nested chained patterns - check if it contains graph match expressions
3653+
node["FirstExpression"] = booleanBinaryExpressionToJSONWithGraphContext(firstExpr, ctx)
3654+
default:
3655+
node["FirstExpression"] = booleanExpressionToJSON(e.FirstExpression)
3656+
}
3657+
}
3658+
if e.SecondExpression != nil {
3659+
// Check if second expression is a graph match expression type
3660+
switch secondExpr := e.SecondExpression.(type) {
3661+
case *ast.GraphMatchCompositeExpression:
3662+
node["SecondExpression"] = graphMatchExpressionToJSONWithContext(secondExpr, ctx)
3663+
case *ast.BooleanBinaryExpression:
3664+
// Could be nested chained patterns - check if it contains graph match expressions
3665+
node["SecondExpression"] = booleanBinaryExpressionToJSONWithGraphContext(secondExpr, ctx)
3666+
default:
3667+
node["SecondExpression"] = booleanExpressionToJSON(e.SecondExpression)
3668+
}
3669+
}
3670+
return node
3671+
}
3672+
36233673
func graphMatchNodeExpressionToJSON(expr *ast.GraphMatchNodeExpression) jsonNode {
36243674
node := jsonNode{
36253675
"$type": "GraphMatchNodeExpression",
@@ -3631,6 +3681,16 @@ func graphMatchNodeExpressionToJSON(expr *ast.GraphMatchNodeExpression) jsonNode
36313681
return node
36323682
}
36333683

3684+
func graphMatchNodeExpressionToJSONWithContext(expr *ast.GraphMatchNodeExpression, ctx *graphMatchContext) jsonNode {
3685+
// Check if we've seen this exact pointer before
3686+
if ctx.seenNodes[expr] {
3687+
// This node pointer has been seen before, use $ref
3688+
return jsonNode{"$ref": "GraphMatchNodeExpression"}
3689+
}
3690+
ctx.seenNodes[expr] = true
3691+
return graphMatchNodeExpressionToJSON(expr)
3692+
}
3693+
36343694
func groupByClauseToJSON(gbc *ast.GroupByClause) jsonNode {
36353695
node := jsonNode{
36363696
"$type": "GroupByClause",

parser/parse_select.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,22 @@ func (p *Parser) parsePrimaryExpression() (ast.ScalarExpression, error) {
11311131
p.nextToken()
11321132
return &ast.ColumnReferenceExpression{ColumnType: "PseudoColumnCuid"}, nil
11331133
}
1134+
if upper == "$NODE_ID" {
1135+
p.nextToken()
1136+
return &ast.ColumnReferenceExpression{ColumnType: "PseudoColumnGraphNodeId"}, nil
1137+
}
1138+
if upper == "$EDGE_ID" {
1139+
p.nextToken()
1140+
return &ast.ColumnReferenceExpression{ColumnType: "PseudoColumnGraphEdgeId"}, nil
1141+
}
1142+
if upper == "$FROM_ID" {
1143+
p.nextToken()
1144+
return &ast.ColumnReferenceExpression{ColumnType: "PseudoColumnGraphFromId"}, nil
1145+
}
1146+
if upper == "$TO_ID" {
1147+
p.nextToken()
1148+
return &ast.ColumnReferenceExpression{ColumnType: "PseudoColumnGraphToId"}, nil
1149+
}
11341150
// Check for NEXT VALUE FOR sequence expression
11351151
if upper == "NEXT" && strings.ToUpper(p.peekTok.Literal) == "VALUE" {
11361152
return p.parseNextValueForExpression()
@@ -1528,6 +1544,14 @@ func (p *Parser) isIdentifierToken() bool {
15281544
}
15291545

15301546
func (p *Parser) parseColumnReferenceOrFunctionCall() (ast.ScalarExpression, error) {
1547+
// Check for graph pseudo columns at the start
1548+
upper := strings.ToUpper(p.curTok.Literal)
1549+
pseudoType := getPseudoColumnType(upper)
1550+
if pseudoType != "" && p.peekTok.Type != TokenDot {
1551+
p.nextToken()
1552+
return &ast.ColumnReferenceExpression{ColumnType: pseudoType}, nil
1553+
}
1554+
15311555
var identifiers []*ast.Identifier
15321556
colType := "Regular"
15331557

@@ -6247,6 +6271,14 @@ func getPseudoColumnType(value string) string {
62476271
return "PseudoColumnRowGuid"
62486272
case "$CUID":
62496273
return "PseudoColumnCuid"
6274+
case "$NODE_ID":
6275+
return "PseudoColumnGraphNodeId"
6276+
case "$EDGE_ID":
6277+
return "PseudoColumnGraphEdgeId"
6278+
case "$FROM_ID":
6279+
return "PseudoColumnGraphFromId"
6280+
case "$TO_ID":
6281+
return "PseudoColumnGraphToId"
62506282
default:
62516283
return ""
62526284
}

parser/parse_statements.go

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -501,16 +501,46 @@ func (p *Parser) parseInlineIndexDefinition() (*ast.IndexDefinition, error) {
501501
if p.curTok.Type == TokenLParen {
502502
p.nextToken() // consume (
503503
for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF {
504-
colIdent := p.parseIdentifier()
505-
col := &ast.ColumnWithSortOrder{
506-
Column: &ast.ColumnReferenceExpression{
507-
ColumnType: "Regular",
508-
MultiPartIdentifier: &ast.MultiPartIdentifier{
509-
Count: 1,
510-
Identifiers: []*ast.Identifier{colIdent},
504+
// Check for graph pseudo columns
505+
upperLit := strings.ToUpper(p.curTok.Literal)
506+
var col *ast.ColumnWithSortOrder
507+
switch upperLit {
508+
case "$NODE_ID":
509+
col = &ast.ColumnWithSortOrder{
510+
Column: &ast.ColumnReferenceExpression{ColumnType: "PseudoColumnGraphNodeId"},
511+
SortOrder: ast.SortOrderNotSpecified,
512+
}
513+
p.nextToken()
514+
case "$EDGE_ID":
515+
col = &ast.ColumnWithSortOrder{
516+
Column: &ast.ColumnReferenceExpression{ColumnType: "PseudoColumnGraphEdgeId"},
517+
SortOrder: ast.SortOrderNotSpecified,
518+
}
519+
p.nextToken()
520+
case "$FROM_ID":
521+
col = &ast.ColumnWithSortOrder{
522+
Column: &ast.ColumnReferenceExpression{ColumnType: "PseudoColumnGraphFromId"},
523+
SortOrder: ast.SortOrderNotSpecified,
524+
}
525+
p.nextToken()
526+
case "$TO_ID":
527+
col = &ast.ColumnWithSortOrder{
528+
Column: &ast.ColumnReferenceExpression{ColumnType: "PseudoColumnGraphToId"},
529+
SortOrder: ast.SortOrderNotSpecified,
530+
}
531+
p.nextToken()
532+
default:
533+
colIdent := p.parseIdentifier()
534+
col = &ast.ColumnWithSortOrder{
535+
Column: &ast.ColumnReferenceExpression{
536+
ColumnType: "Regular",
537+
MultiPartIdentifier: &ast.MultiPartIdentifier{
538+
Count: 1,
539+
Identifiers: []*ast.Identifier{colIdent},
540+
},
511541
},
512-
},
513-
SortOrder: ast.SortOrderNotSpecified,
542+
SortOrder: ast.SortOrderNotSpecified,
543+
}
514544
}
515545

516546
// Parse optional ASC/DESC

0 commit comments

Comments
 (0)