From 31325dd53a7e155525f619452056694edd0bf7b1 Mon Sep 17 00:00:00 2001 From: apstndb <803393+apstndb@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:30:35 +0900 Subject: [PATCH] Implement CALL statement (#231) * Implement CALL statement * Update testdata --- ast/ast.go | 21 ++++ ast/pos.go | 8 ++ ast/sql.go | 10 ++ parser.go | 35 +++++- parser_test.go | 6 +- .../input/statement/call_cancel_query.sql | 1 + .../input/statement/call_complex_args.sql | 2 + testdata/input/statement/call_path.sql | 2 + .../statement/call_cancel_query.sql.txt | 27 +++++ .../statement/call_complex_args.sql.txt | 100 ++++++++++++++++++ testdata/result/statement/call_path.sql.txt | 25 +++++ 11 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 testdata/input/statement/call_cancel_query.sql create mode 100644 testdata/input/statement/call_complex_args.sql create mode 100644 testdata/input/statement/call_path.sql create mode 100644 testdata/result/statement/call_cancel_query.sql.txt create mode 100644 testdata/result/statement/call_complex_args.sql.txt create mode 100644 testdata/result/statement/call_path.sql.txt diff --git a/ast/ast.go b/ast/ast.go index 91da7ff0..005cc011 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -100,6 +100,7 @@ func (DropVectorIndex) isStatement() {} func (Insert) isStatement() {} func (Delete) isStatement() {} func (Update) isStatement() {} +func (Call) isStatement() {} // QueryExpr represents query expression, which can be body of QueryStatement or subqueries. // Select and FromQuery are leaf QueryExpr and others wrap other QueryExpr. @@ -3504,3 +3505,23 @@ type UpdateItem struct { Path []*Ident // len(Path) > 0 DefaultExpr *DefaultExpr } + +// ================================================================================ +// +// Procedural language +// +// ================================================================================ + +// Call is CALL statement. +// +// CALL {{.Name | sql}}({{.Args | sqlJoin ", "}}) +type Call struct { + // pos = Call + // end = Rparen +1 + + Call token.Pos + Rparen token.Pos + + Name *Path + Args []TVFArg // len(Args) > 0 +} diff --git a/ast/pos.go b/ast/pos.go index 6a27bf38..6dfabbf9 100644 --- a/ast/pos.go +++ b/ast/pos.go @@ -1765,3 +1765,11 @@ func (u *UpdateItem) Pos() token.Pos { func (u *UpdateItem) End() token.Pos { return nodeEnd(wrapNode(u.DefaultExpr)) } + +func (c *Call) Pos() token.Pos { + return c.Call +} + +func (c *Call) End() token.Pos { + return posAdd(c.Rparen, 1) +} diff --git a/ast/sql.go b/ast/sql.go index 5cb263f6..d389b83e 100644 --- a/ast/sql.go +++ b/ast/sql.go @@ -1194,3 +1194,13 @@ func (u *Update) SQL() string { func (u *UpdateItem) SQL() string { return sqlJoin(u.Path, ".") + " = " + u.DefaultExpr.SQL() } + +// ================================================================================ +// +// Procedural language +// +// ================================================================================ + +func (c *Call) SQL() string { + return "CALL " + c.Name.SQL() + "(" + sqlJoin(c.Args, ", ") + ")" +} diff --git a/parser.go b/parser.go index e44500d9..0cd66b58 100644 --- a/parser.go +++ b/parser.go @@ -218,11 +218,41 @@ func (p *Parser) parseStatement() ast.Statement { return p.parseDDL() case p.Token.IsKeywordLike("INSERT") || p.Token.IsKeywordLike("DELETE") || p.Token.IsKeywordLike("UPDATE"): return p.parseDML() + case p.Token.IsKeywordLike("CALL"): + return p.parseOtherStatement() } panic(p.errorfAtToken(&p.Token, "unexpected token: %s", p.Token.Kind)) } +func (p *Parser) parseOtherStatement() ast.Statement { + switch { + case p.Token.IsKeywordLike("CALL"): + return p.parseCall() + } + + panic(p.errorfAtToken(&p.Token, "unexpected token: %s", p.Token.Kind)) +} + +func (p *Parser) parseCall() *ast.Call { + pos := p.expectKeywordLike("CALL").Pos + name := p.parsePath() + p.expect("(") + + var args []ast.TVFArg + if p.Token.Kind != ")" { + args = parseCommaSeparatedList(p, p.parseTVFArg) + } + + rparen := p.expect(")").Pos + + return &ast.Call{ + Call: pos, + Rparen: rparen, + Name: name, + Args: args, + } +} func (p *Parser) parseStatements(doParse func()) { for p.Token.Kind != token.TokenEOF { if p.Token.Kind == ";" { @@ -1726,7 +1756,7 @@ func (p *Parser) parseLit() ast.Expr { p.nextToken() switch p.Token.Kind { case "(": - return p.parseCall(id) + return p.parseCallLike(id) case token.TokenString: if id.IsKeywordLike("DATE") { return p.parseDateLiteral(id) @@ -1751,7 +1781,8 @@ func (p *Parser) parseLit() ast.Expr { panic(p.errorfAtToken(&p.Token, "unexpected token: %s", p.Token.Kind)) } -func (p *Parser) parseCall(id token.Token) ast.Expr { +// parseCallLike parses after identifier part of function call like structures. +func (p *Parser) parseCallLike(id token.Token) ast.Expr { p.expect("(") if id.IsIdent("COUNT") && p.Token.Kind == "*" { p.nextToken() diff --git a/parser_test.go b/parser_test.go index ecc32f9d..6db86502 100644 --- a/parser_test.go +++ b/parser_test.go @@ -9,11 +9,12 @@ import ( "path/filepath" "testing" + "github.com/k0kubun/pp/v3" + "github.com/pmezard/go-difflib/difflib" + "github.com/cloudspannerecosystem/memefish" "github.com/cloudspannerecosystem/memefish/ast" "github.com/cloudspannerecosystem/memefish/token" - "github.com/k0kubun/pp/v3" - "github.com/pmezard/go-difflib/difflib" ) var update = flag.Bool("update", false, "update result files") @@ -168,6 +169,7 @@ func TestParseStatement(t *testing.T) { "./testdata/input/query", "./testdata/input/ddl", "./testdata/input/dml", + "./testdata/input/statement", } resultPath := "./testdata/result/statement" diff --git a/testdata/input/statement/call_cancel_query.sql b/testdata/input/statement/call_cancel_query.sql new file mode 100644 index 00000000..15f0b98b --- /dev/null +++ b/testdata/input/statement/call_cancel_query.sql @@ -0,0 +1 @@ +CALL cancel_query("12345") \ No newline at end of file diff --git a/testdata/input/statement/call_complex_args.sql b/testdata/input/statement/call_complex_args.sql new file mode 100644 index 00000000..163539ff --- /dev/null +++ b/testdata/input/statement/call_complex_args.sql @@ -0,0 +1,2 @@ +-- https://github.com/google/zetasql/blob/a516c6b26d183efc4f56293256bba92e243b7a61/zetasql/parser/testdata/call.test#L92C1-L93C1 +call myprocedure(TABLE my.table, (SELECT * FROM my.another_table), mytvf(1, 2)) \ No newline at end of file diff --git a/testdata/input/statement/call_path.sql b/testdata/input/statement/call_path.sql new file mode 100644 index 00000000..37dec9d5 --- /dev/null +++ b/testdata/input/statement/call_path.sql @@ -0,0 +1,2 @@ +-- https://github.com/google/zetasql/blob/a516c6b26d183efc4f56293256bba92e243b7a61/zetasql/parser/testdata/call.test#L15C1-L15C26 +call schema.myprocedure() \ No newline at end of file diff --git a/testdata/result/statement/call_cancel_query.sql.txt b/testdata/result/statement/call_cancel_query.sql.txt new file mode 100644 index 00000000..f2dde5e1 --- /dev/null +++ b/testdata/result/statement/call_cancel_query.sql.txt @@ -0,0 +1,27 @@ +--- call_cancel_query.sql +CALL cancel_query("12345") +--- AST +&ast.Call{ + Rparen: 25, + Name: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 5, + NameEnd: 17, + Name: "cancel_query", + }, + }, + }, + Args: []ast.TVFArg{ + &ast.ExprArg{ + Expr: &ast.StringLiteral{ + ValuePos: 18, + ValueEnd: 25, + Value: "12345", + }, + }, + }, +} + +--- SQL +CALL cancel_query("12345") diff --git a/testdata/result/statement/call_complex_args.sql.txt b/testdata/result/statement/call_complex_args.sql.txt new file mode 100644 index 00000000..929d3d61 --- /dev/null +++ b/testdata/result/statement/call_complex_args.sql.txt @@ -0,0 +1,100 @@ +--- call_complex_args.sql +-- https://github.com/google/zetasql/blob/a516c6b26d183efc4f56293256bba92e243b7a61/zetasql/parser/testdata/call.test#L92C1-L93C1 +call myprocedure(TABLE my.table, (SELECT * FROM my.another_table), mytvf(1, 2)) +--- AST +&ast.Call{ + Call: 129, + Rparen: 207, + Name: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 134, + NameEnd: 145, + Name: "myprocedure", + }, + }, + }, + Args: []ast.TVFArg{ + &ast.TableArg{ + Table: 146, + Name: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 152, + NameEnd: 154, + Name: "my", + }, + &ast.Ident{ + NamePos: 155, + NameEnd: 160, + Name: "table", + }, + }, + }, + }, + &ast.ExprArg{ + Expr: &ast.ScalarSubQuery{ + Lparen: 162, + Rparen: 193, + Query: &ast.Select{ + Select: 163, + Results: []ast.SelectItem{ + &ast.Star{ + Star: 170, + }, + }, + From: &ast.From{ + From: 172, + Source: &ast.PathTableExpr{ + Path: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 177, + NameEnd: 179, + Name: "my", + }, + &ast.Ident{ + NamePos: 180, + NameEnd: 193, + Name: "another_table", + }, + }, + }, + }, + }, + }, + }, + }, + &ast.ExprArg{ + Expr: &ast.CallExpr{ + Rparen: 206, + Func: &ast.Ident{ + NamePos: 196, + NameEnd: 201, + Name: "mytvf", + }, + Args: []ast.Arg{ + &ast.ExprArg{ + Expr: &ast.IntLiteral{ + ValuePos: 202, + ValueEnd: 203, + Base: 10, + Value: "1", + }, + }, + &ast.ExprArg{ + Expr: &ast.IntLiteral{ + ValuePos: 205, + ValueEnd: 206, + Base: 10, + Value: "2", + }, + }, + }, + }, + }, + }, +} + +--- SQL +CALL myprocedure(TABLE my.table, (SELECT * FROM my.another_table), mytvf(1, 2)) diff --git a/testdata/result/statement/call_path.sql.txt b/testdata/result/statement/call_path.sql.txt new file mode 100644 index 00000000..21062aa4 --- /dev/null +++ b/testdata/result/statement/call_path.sql.txt @@ -0,0 +1,25 @@ +--- call_path.sql +-- https://github.com/google/zetasql/blob/a516c6b26d183efc4f56293256bba92e243b7a61/zetasql/parser/testdata/call.test#L15C1-L15C26 +call schema.myprocedure() +--- AST +&ast.Call{ + Call: 130, + Rparen: 154, + Name: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 135, + NameEnd: 141, + Name: "schema", + }, + &ast.Ident{ + NamePos: 142, + NameEnd: 153, + Name: "myprocedure", + }, + }, + }, +} + +--- SQL +CALL schema.myprocedure()