Skip to content

Commit

Permalink
Implement EXCEPT and REPLACE in select item (#196)
Browse files Browse the repository at this point in the history
* Implement EXCEPT and REPLACE

* Update testdata

* Fix lookaheadSetOperatorExcept

* Add comment

* Rename to StarModifierExcept, StarModifierReplace

* Fix DotStar.end

* Make IF and GRAPH_TABLE as a keyword, and add IfExpr

* Update testdata
  • Loading branch information
apstndb authored Dec 18, 2024
1 parent e0d66c0 commit 7eae73a
Show file tree
Hide file tree
Showing 37 changed files with 1,989 additions and 12 deletions.
55 changes: 50 additions & 5 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -651,26 +651,71 @@ type SubQuery struct {
Limit *Limit // optional
}

// StarModifierExcept is EXCEPT node in Star and DotStar of SelectItem.
//
// EXCEPT ({{Columns | sqlJoin ", "}})
type StarModifierExcept struct {
// pos = Except
// end = Rparen + 1

Except token.Pos
Rparen token.Pos

Columns []*Ident
}

// StarModifierReplaceItem is a single item of StarModifierReplace.
//
// {{.Expr | sql}} AS {{.Name | sql}}
type StarModifierReplaceItem struct {
// pos = Expr.pos
// end = Name.end

Expr Expr
Name *Ident
}

// StarModifierReplace is REPLACE node in Star and DotStar of SelectItem.
//
// REPLACE ({{Columns | sqlJoin ", "}})
type StarModifierReplace struct {
// pos = Replace
// end = Rparen + 1

Replace token.Pos
Rparen token.Pos

Columns []*StarModifierReplaceItem
}

// Star is a single * in SELECT result columns list.
//
// *
// {{"*"}} {{.Except | sqlOpt}} {{.Replace | sqlOpt}}
//
// Note: The text/template notation escapes * to avoid normalize * to - by some formatters
// because the prefix * is ambiguous with bulletin list.
type Star struct {
// pos = Star
// end = Star + 1
// end = (Replace ?? Except).end || Star + 1

Star token.Pos // position of "*"

Except *StarModifierExcept // optional
Replace *StarModifierReplace // optional
}

// DotStar is expression with * in SELECT result columns list.
//
// {{.Expr | sql}}.*
// {{.Expr | sql}}.* {{.Except | sqlOpt}} {{.Replace | sqlOpt}}
type DotStar struct {
// pos = Expr.pos
// end = Star + 1
// end = (Replace ?? Except).end || Star + 1

Star token.Pos // position of "*"

Expr Expr
Expr Expr
Except *StarModifierExcept // optional
Replace *StarModifierReplace // optional
}

// Alias is aliased expression by AS clause.
Expand Down
28 changes: 26 additions & 2 deletions ast/pos.go

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

10 changes: 8 additions & 2 deletions ast/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,18 @@ func (s *SubQuery) SQL() string {
return sql
}

func (s *StarModifierExcept) SQL() string { return "EXCEPT (" + sqlJoin(s.Columns, " ") + ")" }

func (s *StarModifierReplaceItem) SQL() string { return s.Expr.SQL() + " AS " + s.Name.SQL() }

func (s *StarModifierReplace) SQL() string { return "REPLACE (" + sqlJoin(s.Columns, ", ") + ")" }

func (s *Star) SQL() string {
return "*"
return "*" + sqlOpt(" ", s.Except, "") + sqlOpt(" ", s.Replace, "")
}

func (s *DotStar) SQL() string {
return s.Expr.SQL() + ".*"
return s.Expr.SQL() + ".*" + sqlOpt(" ", s.Except, "") + sqlOpt(" ", s.Replace, "")
}

func (a *Alias) SQL() string {
Expand Down
73 changes: 70 additions & 3 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,11 +450,74 @@ func (p *Parser) parseSelectResults() []ast.SelectItem {
return results
}

// lookaheadStarModifierExcept is needed to distinct "* EXCEPT (columns)" and "* EXCEPT {ALL|DISTINCT}".
func (p *Parser) lookaheadStarModifierExcept() bool {
lexer := p.Lexer.Clone()
defer func() {
p.Lexer = lexer
}()

if p.Token.Kind != "EXCEPT" {
return false
}
p.nextToken()
return p.Token.Kind == "("
}

func (p *Parser) tryParseStarModifierExcept() *ast.StarModifierExcept {
if !p.lookaheadStarModifierExcept() {
return nil
}

pos := p.expect("EXCEPT").Pos
p.expect("(")
columns := parseCommaSeparatedList(p, p.parseIdent)
rparen := p.expect(")").Pos

return &ast.StarModifierExcept{
Except: pos,
Rparen: rparen,
Columns: columns,
}
}

func (p *Parser) parseStarModifierReplaceItem() *ast.StarModifierReplaceItem {
expr := p.parseExpr()
p.expect("AS")
name := p.parseIdent()

return &ast.StarModifierReplaceItem{
Expr: expr,
Name: name,
}
}

func (p *Parser) tryParseStarModifierReplace() *ast.StarModifierReplace {
if !p.Token.IsKeywordLike("REPLACE") {
return nil
}

pos := p.expectKeywordLike("REPLACE").Pos
p.expect("(")
columns := parseCommaSeparatedList(p, p.parseStarModifierReplaceItem)
rparen := p.expect(")").Pos

return &ast.StarModifierReplace{
Replace: pos,
Rparen: rparen,
Columns: columns,
}
}

func (p *Parser) parseSelectItem() ast.SelectItem {
if p.Token.Kind == "*" {
pos := p.expect("*").Pos
except := p.tryParseStarModifierExcept()
replace := p.tryParseStarModifierReplace()
return &ast.Star{
Star: pos,
Star: pos,
Except: except,
Replace: replace,
}
}

Expand All @@ -469,9 +532,13 @@ func (p *Parser) parseSelectItem() ast.SelectItem {
if p.Token.Kind == "." {
p.nextToken()
pos := p.expect("*").Pos
except := p.tryParseStarModifierExcept()
replace := p.tryParseStarModifierReplace()
return &ast.DotStar{
Star: pos,
Expr: expr,
Star: pos,
Expr: expr,
Except: except,
Replace: replace,
}
}

Expand Down
1 change: 1 addition & 0 deletions testdata/input/query/edge_case_dot_star_except.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT (SELECT T.* EXCEPT (s)) AS n FROM (SELECT 1 AS n, "foo" AS s) AS T
1 change: 1 addition & 0 deletions testdata/input/query/edge_case_dot_star_except_all.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT (SELECT T.* EXCEPT ALL SELECT T.*) AS n FROM (SELECT 1 AS n) AS T
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT (SELECT T.* EXCEPT DISTINCT SELECT T.*) AS n FROM (SELECT 1 AS n) AS T
1 change: 1 addition & 0 deletions testdata/input/query/select_dot_star.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT T.* FROM (SELECT "apple" AS fruit, "carrot" AS vegetable) T
1 change: 1 addition & 0 deletions testdata/input/query/select_dot_star_except.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT T.* EXCEPT (vegetable) FROM (SELECT "apple" AS fruit, "carrot" AS vegetable) AS T
1 change: 1 addition & 0 deletions testdata/input/query/select_dot_star_except_replace.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT T.* EXCEPT (vegetable) REPLACE ("orange" AS fruit) FROM (SELECT "apple" AS fruit, "carrot" AS vegetable) AS T
1 change: 1 addition & 0 deletions testdata/input/query/select_dot_star_replace.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT T.* REPLACE ("orange" AS fruit) FROM (SELECT "apple" AS fruit, "carrot" AS vegetable) AS T
1 change: 1 addition & 0 deletions testdata/input/query/select_star.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT * FROM (SELECT "apple" AS fruit, "carrot" AS vegetable)
1 change: 1 addition & 0 deletions testdata/input/query/select_star_except.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT * EXCEPT (vegetable) FROM (SELECT "apple" AS fruit, "carrot" AS vegetable)
1 change: 1 addition & 0 deletions testdata/input/query/select_star_except_replace.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT * EXCEPT (vegetable) REPLACE ("orange" AS fruit) FROM (SELECT "apple" AS fruit, "carrot" AS vegetable)
1 change: 1 addition & 0 deletions testdata/input/query/select_star_replace.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT * REPLACE ("orange" AS fruit) FROM (SELECT "apple" AS fruit, "carrot" AS vegetable)
101 changes: 101 additions & 0 deletions testdata/result/query/edge_case_dot_star_except.sql.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
--- edge_case_dot_star_except.sql
SELECT (SELECT T.* EXCEPT (s)) AS n FROM (SELECT 1 AS n, "foo" AS s) AS T
--- AST
&ast.QueryStatement{
Query: &ast.Select{
Results: []ast.SelectItem{
&ast.Alias{
Expr: &ast.ScalarSubQuery{
Lparen: 7,
Rparen: 29,
Query: &ast.Select{
Select: 8,
Results: []ast.SelectItem{
&ast.DotStar{
Star: 17,
Expr: &ast.Ident{
NamePos: 15,
NameEnd: 16,
Name: "T",
},
Except: &ast.StarModifierExcept{
Except: 19,
Rparen: 28,
Columns: []*ast.Ident{
&ast.Ident{
NamePos: 27,
NameEnd: 28,
Name: "s",
},
},
},
},
},
},
},
As: &ast.AsAlias{
As: 31,
Alias: &ast.Ident{
NamePos: 34,
NameEnd: 35,
Name: "n",
},
},
},
},
From: &ast.From{
From: 36,
Source: &ast.SubQueryTableExpr{
Lparen: 41,
Rparen: 67,
Query: &ast.Select{
Select: 42,
Results: []ast.SelectItem{
&ast.Alias{
Expr: &ast.IntLiteral{
ValuePos: 49,
ValueEnd: 50,
Base: 10,
Value: "1",
},
As: &ast.AsAlias{
As: 51,
Alias: &ast.Ident{
NamePos: 54,
NameEnd: 55,
Name: "n",
},
},
},
&ast.Alias{
Expr: &ast.StringLiteral{
ValuePos: 57,
ValueEnd: 62,
Value: "foo",
},
As: &ast.AsAlias{
As: 63,
Alias: &ast.Ident{
NamePos: 66,
NameEnd: 67,
Name: "s",
},
},
},
},
},
As: &ast.AsAlias{
As: 69,
Alias: &ast.Ident{
NamePos: 72,
NameEnd: 73,
Name: "T",
},
},
},
},
},
}

--- SQL
SELECT (SELECT T.* EXCEPT (s)) AS n FROM (SELECT 1 AS n, "foo" AS s) AS T
Loading

0 comments on commit 7eae73a

Please sign in to comment.