Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support nested update #263

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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
30 changes: 25 additions & 5 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,15 @@ type InsertInput interface {
func (ValuesInput) isInsertInput() {}
func (SubQueryInput) isInsertInput() {}

// UpdateItem represents SET clause items in UPDATE.
type UpdateItem interface {
Node
isUpdateItem()
}

func (UpdateItemSetValue) isUpdateItem() {}
func (UpdateItemDML) isUpdateItem() {}

// ChangeStreamFor represents FOR clause in CREATE/ALTER CHANGE STREAM statement.
type ChangeStreamFor interface {
Node
Expand Down Expand Up @@ -3953,7 +3962,7 @@ type Insert struct {

Hint *Hint // optional
TableName *Path
Columns []*Ident
Columns []*Ident // optional when nested
Input InsertInput
ThenReturn *ThenReturn // optional
}
Expand Down Expand Up @@ -4037,16 +4046,27 @@ type Update struct {

Hint *Hint // optional
TableName *Path
As *AsAlias // optional
Updates []*UpdateItem // len(Updates) > 0
As *AsAlias // optional
Updates []UpdateItem // len(Updates) > 0
Where *Where
ThenReturn *ThenReturn // optional
}

// UpdateItem is SET clause items in UPDATE.
// UpdateItemDML is nested update UpdateItem node in UPDATE statement.
//
// ({{.DML | sql}})
type UpdateItemDML struct {
// pos = Lparen
// end = Rparen + 1

Lparen, Rparen token.Pos // position of "(", ")"
DML DML
}

// UpdateItemSetValue is assignment style UpdateItem node in UPDATE statement .
//
// {{.Path | sqlJoin "."}} = {{.DefaultExpr | sql}}
type UpdateItem struct {
type UpdateItemSetValue struct {
// pos = Path[0].pos
// end = DefaultExpr.end

Expand Down
12 changes: 10 additions & 2 deletions ast/pos.go

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

6 changes: 5 additions & 1 deletion ast/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -1354,10 +1354,14 @@ func (u *Update) SQL() string {
sqlOpt(" ", u.ThenReturn, "")
}

func (u *UpdateItem) SQL() string {
func (u *UpdateItemSetValue) SQL() string {
return sqlJoin(u.Path, ".") + " = " + u.DefaultExpr.SQL()
}

func (u *UpdateItemDML) SQL() string {
return "(" + u.DML.SQL() + ")"
}

// ================================================================================
//
// Procedural language
Expand Down
5 changes: 4 additions & 1 deletion ast/walk_internal.go

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

50 changes: 35 additions & 15 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func (p *Parser) parseStatementInternal(hint *ast.Hint) (stmt ast.Statement) {
case p.Token.Kind == "SELECT" || p.Token.Kind == "WITH" || p.Token.Kind == "(" || p.Token.Kind == "FROM":
return p.parseQueryStatementInternal(hint)
case p.Token.IsKeywordLike("INSERT") || p.Token.IsKeywordLike("DELETE") || p.Token.IsKeywordLike("UPDATE"):
return p.parseDMLInternal(hint)
return p.parseDMLInternal(hint, false)
case hint != nil:
panic(p.errorfAtPosition(hint.Pos(), p.Token.End, "statement hint is only permitted before query or DML, but got: %s", p.Token.Raw))
case p.Token.Kind == "CREATE" || p.Token.IsKeywordLike("ALTER") || p.Token.IsKeywordLike("DROP") ||
Expand Down Expand Up @@ -5014,6 +5014,8 @@ func (p *Parser) parseIfExists() bool {
//
// ================================================================================

// parseDML parses non-nested DML with optional hints.
// This function is parseStatements friendly.
func (p *Parser) parseDML() (dml ast.DML) {
l := p.Lexer.Clone()
defer func() {
Expand All @@ -5024,10 +5026,13 @@ func (p *Parser) parseDML() (dml ast.DML) {
}()

hint := p.tryParseHint()
return p.parseDMLInternal(hint)
return p.parseDMLInternal(hint, false)
}

func (p *Parser) parseDMLInternal(hint *ast.Hint) (dml ast.DML) {
// parseDMLInternal can parse nested and non-nested DML with parsed hints.
// The behavior is controlled by nested flag.
// Note: It is recommended to use parseDML if you want to parse a complete DML statement.
func (p *Parser) parseDMLInternal(hint *ast.Hint, nested bool) (dml ast.DML) {
l := p.Lexer.Clone()
defer func() {
if r := recover(); r != nil {
Expand All @@ -5042,7 +5047,7 @@ func (p *Parser) parseDMLInternal(hint *ast.Hint) (dml ast.DML) {
pos := id.Pos
switch {
case id.IsKeywordLike("INSERT"):
return p.parseInsert(pos, hint)
return p.parseInsert(pos, hint, nested)
case id.IsKeywordLike("DELETE"):
return p.parseDelete(pos, hint)
case id.IsKeywordLike("UPDATE"):
Expand Down Expand Up @@ -5085,7 +5090,7 @@ func (p *Parser) tryParseThenReturn() *ast.ThenReturn {
}
}

func (p *Parser) parseInsert(pos token.Pos, hint *ast.Hint) *ast.Insert {
func (p *Parser) parseInsert(pos token.Pos, hint *ast.Hint, nested bool) *ast.Insert {
var insertOrType ast.InsertOrType
if p.Token.Kind == "OR" {
p.nextToken()
Expand All @@ -5106,18 +5111,22 @@ func (p *Parser) parseInsert(pos token.Pos, hint *ast.Hint) *ast.Insert {

name := p.parsePath()

p.expect("(")
// Column list is optional only when nested update(not top-level DML).
// Note: There is a ambiguity between column list and parenthesized query.
var columns []*ast.Ident
if p.Token.Kind != ")" {
for p.Token.Kind != token.TokenEOF {
columns = append(columns, p.parseIdent())
if p.Token.Kind != "," {
break
if !nested || (p.Token.Kind == "(" && !p.lookaheadSubQuery()) {
p.expect("(")
if p.Token.Kind != ")" {
for p.Token.Kind != token.TokenEOF {
columns = append(columns, p.parseIdent())
if p.Token.Kind != "," {
break
}
p.nextToken()
}
p.nextToken()
}
p.expect(")")
}
p.expect(")")

var input ast.InsertInput
if p.Token.IsKeywordLike("VALUES") {
Expand Down Expand Up @@ -5237,12 +5246,23 @@ func (p *Parser) parseUpdate(pos token.Pos, hint *ast.Hint) *ast.Update {
}
}

func (p *Parser) parseUpdateItem() *ast.UpdateItem {
func (p *Parser) parseUpdateItem() ast.UpdateItem {
if p.Token.Kind == "(" {
lparen := p.expect("(").Pos
dml := p.parseDMLInternal(nil, true)
rparen := p.expect(")").Pos
return &ast.UpdateItemDML{
Lparen: lparen,
Rparen: rparen,
DML: dml,
}
}

path := p.parseIdentOrPath()
p.expect("=")
defaultExpr := p.parseDefaultExpr()

return &ast.UpdateItem{
return &ast.UpdateItemSetValue{
Path: path,
DefaultExpr: defaultExpr,
}
Expand Down
6 changes: 6 additions & 0 deletions testdata/input/dml/nested_update_delete_update_insert.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
UPDATE Singers s
SET
(DELETE FROM s.SingerInfo.Residence r WHERE r.City = 'Seattle'),
(UPDATE s.Albums.Song song SET song.songtitle = 'No, This Is Rubbish' WHERE song.songtitle = 'This Is Pretty Good'),
(INSERT s.Albums.Song VALUES ("songtitle: 'The Second Best Song'"))
WHERE SingerId = 3 AND s.Albums.title = 'Go! Go! Go!'
4 changes: 4 additions & 0 deletions testdata/input/dml/nested_update_insert_song.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
UPDATE Singers s
SET (INSERT s.AlbumInfo.Song(Song)
VALUES ("songtitle: 'Bonus Track', length: 180"))
WHERE s.SingerId = 5 AND s.AlbumInfo.title = "Fire is Hot"
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- In nested query, column list is optional so there is ambiguity between parenthesized query input and column list.
-- I believe Spanner hasn't yet supported this kind of query, but it can be parsed.
UPDATE Singers s
SET (INSERT s.AlbumInfo.Song (SELECT AS VALUE CAST("songtitle: 'The Second Best Song'" AS googlesql.example.Album.Song)))
WHERE TRUE
5 changes: 5 additions & 0 deletions testdata/input/dml/nested_update_insert_song_path.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
UPDATE Singers s
SET (INSERT s.AlbumInfo.Song
VALUES ('''songtitle: 'Bonus Track', length:180''')),
s.Albums.tracks = 16
WHERE s.SingerId = 5 and s.AlbumInfo.title = "Fire is Hot"
4 changes: 4 additions & 0 deletions testdata/input/dml/nested_update_insert_string.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
UPDATE Singers s
SET (INSERT s.AlbumInfo.comments
VALUES ("Groovy!"))
WHERE s.SingerId = 5 AND s.AlbumInfo.title = "Fire is Hot"
6 changes: 6 additions & 0 deletions testdata/input/dml/nested_update_update_nested_insert.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
UPDATE Singers s
SET (UPDATE s.AlbumInfo.Song so
SET (INSERT INTO so.Chart
VALUES ("chartname: 'Galaxy Top 100', rank: 5"))
WHERE so.songtitle = "Bonus Track")
WHERE s.SingerId = 5
7 changes: 7 additions & 0 deletions testdata/input/dml/nested_update_update_nested_update.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
UPDATE Singers s
SET (UPDATE s.AlbumInfo.Song so
SET (UPDATE so.Chart c
SET c.rank = 2
WHERE c.chartname = "Galaxy Top 100")
WHERE so.songtitle = "Bonus Track")
WHERE s.SingerId = 5
Loading
Loading