Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 75 additions & 3 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ func (ParenExpr) isExpr() {}
func (ScalarSubQuery) isExpr() {}
func (ArraySubQuery) isExpr() {}
func (ExistsSubQuery) isExpr() {}
func (ExistsGQLSubQuery) isExpr() {}
func (ArrayGQLSubQuery) isExpr() {}
func (ValueGQLSubQuery) isExpr() {}
func (Param) isExpr() {}
func (Ident) isExpr() {}
func (Path) isExpr() {}
Expand Down Expand Up @@ -305,9 +308,10 @@ type InCondition interface {
isInCondition()
}

func (UnnestInCondition) isInCondition() {}
func (SubQueryInCondition) isInCondition() {}
func (ValuesInCondition) isInCondition() {}
func (UnnestInCondition) isInCondition() {}
func (SubQueryInCondition) isInCondition() {}
func (GQLSubQueryInCondition) isInCondition() {}
func (ValuesInCondition) isInCondition() {}

// TypelessStructLiteralArg represents an argument of typeless STRUCT literals.
type TypelessStructLiteralArg interface {
Expand Down Expand Up @@ -1902,6 +1906,74 @@ type ExistsSubQuery struct {
Query QueryExpr
}

// GQLExistsContent is the body of EXISTS { ... }.
type GQLExistsContent interface {
Node
isGQLExistsContent()
}

func (GQLMultiLinearQueryStatement) isGQLExistsContent() {}
func (GQLMatch) isGQLExistsContent() {}
func (GQLGraphPattern) isGQLExistsContent() {}

// ExistsGQLSubQuery is EXISTS { ... } GQL subquery expression.
//
// EXISTS {{.Hint | sqlOpt}} { {{.GraphClause | sqlOpt}} {{.Query | sql}} }
type ExistsGQLSubQuery struct {
// pos = Exists
// end = Rbrace + 1

Exists token.Pos // position of "EXISTS"
Rbrace token.Pos // position of "}"

Hint *Hint // optional
GraphClause *GQLGraphClause // optional
Query GQLExistsContent
}

// ArrayGQLSubQuery is ARRAY { gql_query_expr } expression.
//
// ARRAY { {{.GraphClause | sqlOpt}} {{.Query | sql}} }
type ArrayGQLSubQuery struct {
// pos = Array
// end = Rbrace + 1

Array token.Pos // position of "ARRAY"
Rbrace token.Pos // position of "}"

GraphClause *GQLGraphClause // optional
Query *GQLMultiLinearQueryStatement
}

// ValueGQLSubQuery is VALUE { gql_query_expr } expression.
//
// VALUE {{.Hint | sqlOpt}} { {{.GraphClause | sqlOpt}} {{.Query | sql}} }
type ValueGQLSubQuery struct {
// pos = Value
// end = Rbrace + 1

Value token.Pos // position of "VALUE"
Rbrace token.Pos // position of "}"

Hint *Hint // optional
GraphClause *GQLGraphClause // optional
Query *GQLMultiLinearQueryStatement
}

// GQLSubQueryInCondition is IN { gql_query_expr } condition.
//
// { {{.GraphClause | sqlOpt}} {{.Query | sql}} }
type GQLSubQueryInCondition struct {
// pos = Lbrace
// end = Rbrace + 1

Lbrace token.Pos // position of "{"
Rbrace token.Pos // position of "}"

GraphClause *GQLGraphClause // optional
Query *GQLMultiLinearQueryStatement
}

// ================================================================================
//
// Literal
Expand Down
3 changes: 3 additions & 0 deletions ast/expr_impls_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions ast/pos.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 17 additions & 1 deletion ast/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const (
func exprPrec(e Expr) prec {
switch e := e.(type) {
case *BadExpr, *CallExpr, *CountStarExpr, *CastExpr, *ExtractExpr, *ReplaceFieldsExpr, *CaseExpr, *IfExpr, *ParenExpr, *ScalarSubQuery,
*ArraySubQuery, *ExistsSubQuery, *Param, *Ident, *Path, *ArrayLiteral, *TupleStructLiteral, *TypedStructLiteral,
*ArraySubQuery, *ExistsSubQuery, *ExistsGQLSubQuery, *ArrayGQLSubQuery, *ValueGQLSubQuery, *Param, *Ident, *Path, *ArrayLiteral, *TupleStructLiteral, *TypedStructLiteral,
*TypelessStructLiteral, *NullLiteral, *BoolLiteral, *IntLiteral, *FloatLiteral, *StringLiteral, *BytesLiteral,
*DateLiteral, *TimestampLiteral, *NumericLiteral, *JSONLiteral, *IntervalLiteralSingle, *IntervalLiteralRange,
*NewConstructor, *BracedNewConstructor, *BracedConstructor, *WithExpr:
Expand Down Expand Up @@ -608,6 +608,22 @@ func (e *ExistsSubQuery) SQL() string {
"(" + e.Query.SQL() + ")"
}

func (e *ExistsGQLSubQuery) SQL() string {
return "EXISTS" + sqlOpt(" ", e.Hint, "") + " { " + sqlOpt("", e.GraphClause, " ") + e.Query.SQL() + " }"
}

func (a *ArrayGQLSubQuery) SQL() string {
return "ARRAY { " + sqlOpt("", a.GraphClause, " ") + a.Query.SQL() + " }"
}

func (v *ValueGQLSubQuery) SQL() string {
return "VALUE" + sqlOpt(" ", v.Hint, "") + " { " + sqlOpt("", v.GraphClause, " ") + v.Query.SQL() + " }"
}

func (g *GQLSubQueryInCondition) SQL() string {
return "{ " + sqlOpt("", g.GraphClause, " ") + g.Query.SQL() + " }"
}

func (p *Param) SQL() string {
return "@" + p.Name
}
Expand Down
18 changes: 18 additions & 0 deletions ast/walk_internal.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

135 changes: 134 additions & 1 deletion parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -1735,6 +1735,19 @@ func (p *Parser) parseInCondition() ast.InCondition {
}
}

if p.Token.Kind == "{" {
lbrace := p.expect("{").Pos
graphClause := p.tryParseGQLGraphClause()
query := p.parseGQLMultiLinearQueryStatement()
rbrace := p.expect("}").Pos
return &ast.GQLSubQueryInCondition{
Lbrace: lbrace,
Rbrace: rbrace,
GraphClause: graphClause,
Query: query,
}
}

if p.Token.Kind == "(" {
lparen := p.Token.Pos
p.nextToken()
Expand Down Expand Up @@ -2039,6 +2052,11 @@ func (p *Parser) parseLit() ast.Expr {
return p.parseCastExpr()
case id.IsKeywordLike("REPLACE_FIELDS"):
return p.parseReplaceFieldsExpr()
case id.IsKeywordLike("VALUE"):
// VALUE is non-reserved, so look ahead for the `VALUE hint? {` form before choosing the GQL subquery parser.
if p.lookaheadValueGQLSubQuery() {
return p.parseValueGQLSubQuery()
}
}

if p.lookaheadCallExpr() {
Expand Down Expand Up @@ -2456,18 +2474,113 @@ func (p *Parser) parseCastExpr() *ast.CastExpr {
}
}

func (p *Parser) parseExistsSubQuery() *ast.ExistsSubQuery {
func (p *Parser) parseExistsSubQuery() ast.Expr {
exists := p.expect("EXISTS").Pos
hint := p.tryParseHint()
if p.Token.Kind == "{" {
p.nextToken()
graphClause := p.tryParseGQLGraphClause()
query := p.parseGQLExistsContent()
rbrace := p.expect("}").Pos
return &ast.ExistsGQLSubQuery{
Exists: exists,
Rbrace: rbrace,
Hint: hint,
GraphClause: graphClause,
Query: query,
}
}
p.expect("(")
query := p.parseQueryExpr()
rparen := p.expect(")").Pos
return &ast.ExistsSubQuery{
Exists: exists,
Rparen: rparen,
Hint: hint,
Query: query,
}
}

func (p *Parser) parseGQLExistsContent() ast.GQLExistsContent {
switch {
case p.lookaheadGQLPathVariable():
return p.parseGQLGraphPattern()
case p.Token.IsKeywordLike("MATCH") || p.Token.IsKeywordLike("OPTIONAL"):
// OPTIONAL MATCH or MATCH — if followed by more primitive statements / NEXT / RETURN chain,
// treat as multi-linear; otherwise single match statement form.
return p.parseGQLExistsMatchOrQuery()
case p.Token.Kind == "@":
// A leading traversal hint is invalid, but routing it through the graph-pattern
// parser produces the specific leading-hint diagnostic instead of a generic
// missing-query-statement error.
return p.parseGQLGraphPattern()
case p.Token.Kind == "(" || p.Token.Kind == "-" || p.Token.Kind == "<" || p.Token.Kind == "->",
p.Token.Kind == "ALL" || p.Token.Kind == "ANY",
p.Token.IsKeywordLike("SHORTEST") || p.Token.IsKeywordLike("CHEAPEST"),
p.Token.IsKeywordLike("WALK") || p.Token.IsKeywordLike("TRAIL") ||
p.Token.IsKeywordLike("SIMPLE") || p.Token.IsKeywordLike("ACYCLIC"):
return p.parseGQLGraphPattern()
default:
return p.parseGQLMultiLinearQueryStatement()
}
}

func (p *Parser) parseGQLExistsMatchOrQuery() ast.GQLExistsContent {
// A MATCH can be either the standalone EXISTS form or the first statement of
// a query. Reparse the latter through the normal query path to keep this
// ambiguity handling separate from query parsing and recovery.
lexer := p.cloneLexer()
errorCount := len(p.errors)
match := p.parseGQLMatch()
if p.Token.Kind == "}" {
return match
}
p.Lexer = lexer
p.errors = p.errors[:errorCount]
return p.parseGQLMultiLinearQueryStatement()
}

func (p *Parser) lookaheadGQLPathVariable() bool {
if p.Token.Kind != token.TokenIdent {
return false
}

lexer := p.cloneLexer()
lexer.nextToken(false)
return lexer.Token.Kind == "="
}

func (p *Parser) lookaheadValueGQLSubQuery() bool {
lexer := p.cloneLexer()
errorCount := len(p.errors)
defer func() {
p.Lexer = lexer
p.errors = p.errors[:errorCount]
}()
if !p.Token.IsKeywordLike("VALUE") {
return false
}
p.nextToken()
p.tryParseHint()
return p.Token.Kind == "{"
}

func (p *Parser) parseValueGQLSubQuery() *ast.ValueGQLSubQuery {
value := p.expectKeywordLike("VALUE").Pos
hint := p.tryParseHint()
p.expect("{")
graphClause := p.tryParseGQLGraphClause()
query := p.parseGQLMultiLinearQueryStatement()
rbrace := p.expect("}").Pos
return &ast.ValueGQLSubQuery{
Value: value,
Rbrace: rbrace,
Hint: hint,
GraphClause: graphClause,
Query: query,
}
}

func (p *Parser) parseExtractExpr() *ast.ExtractExpr {
extract := p.expect("EXTRACT").Pos
p.expect("(")
Expand Down Expand Up @@ -2591,6 +2704,19 @@ func (p *Parser) parseParenExpr() ast.Expr {
func (p *Parser) parseArrayLiteralOrSubQuery() ast.Expr {
pos := p.expect("ARRAY").Pos

if p.Token.Kind == "{" {
p.nextToken()
graphClause := p.tryParseGQLGraphClause()
query := p.parseGQLMultiLinearQueryStatement()
rbrace := p.expect("}").Pos
return &ast.ArrayGQLSubQuery{
Array: pos,
Rbrace: rbrace,
GraphClause: graphClause,
Query: query,
}
}

if p.Token.Kind == "(" {
p.nextToken()
query := p.parseQueryExpr()
Expand Down Expand Up @@ -6942,6 +7068,13 @@ func (p *Parser) parseGQLGraphClause() *ast.GQLGraphClause {
}
}

func (p *Parser) tryParseGQLGraphClause() *ast.GQLGraphClause {
if !p.Token.IsKeywordLike("GRAPH") {
return nil
}
return p.parseGQLGraphClause()
}

func (p *Parser) parseGQLMultiLinearQueryStatement() *ast.GQLMultiLinearQueryStatement {
var stmts []ast.GQLLinearQueryStatement
stmts = append(stmts, p.parseGQLLinearQueryStatement())
Expand Down
7 changes: 7 additions & 0 deletions testdata/input/dml/update_in_braced_gql_graph_clause.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
UPDATE Account
SET is_active = TRUE
WHERE id IN {
GRAPH FinGraph
MATCH (a)
RETURN a.id
}
2 changes: 2 additions & 0 deletions testdata/input/expr/gql_braced_ambiguity_array_paren.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Regression: ARRAY(...) SQL subquery must still parse after ARRAY { gql } support.
ARRAY(SELECT 1)
Loading