blob: 0f3a2ee13f8e4e59f2d1c7a40e1f942ff2753f1b [file] [log] [blame]
/*
Copyright 2019 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package spansql
import (
"fmt"
"math"
"reflect"
"testing"
"time"
"cloud.google.com/go/civil"
)
func TestParseQuery(t *testing.T) {
tests := []struct {
in string
want Query
}{
{`SELECT 17`, Query{Select: Select{List: []Expr{IntegerLiteral(17)}}}},
{
`SELECT Alias AS aka From Characters WHERE Age < @ageLimit AND Alias IS NOT NULL ORDER BY Age DESC LIMIT @limit OFFSET 3` + "\n\t",
Query{
Select: Select{
List: []Expr{ID("Alias")},
From: []SelectFrom{SelectFromTable{
Table: "Characters",
}},
Where: LogicalOp{
Op: And,
LHS: ComparisonOp{
LHS: ID("Age"),
Op: Lt,
RHS: Param("ageLimit"),
},
RHS: IsOp{
LHS: ID("Alias"),
Neg: true,
RHS: Null,
},
},
ListAliases: []ID{"aka"},
},
Order: []Order{{
Expr: ID("Age"),
Desc: true,
}},
Limit: Param("limit"),
Offset: IntegerLiteral(3),
},
},
{
`SELECT COUNT(*) FROM Packages`,
Query{
Select: Select{
List: []Expr{
Func{
Name: "COUNT",
Args: []Expr{Star},
},
},
From: []SelectFrom{SelectFromTable{Table: "Packages"}},
},
},
},
{
`SELECT * FROM Packages`,
Query{
Select: Select{
List: []Expr{Star},
From: []SelectFrom{SelectFromTable{Table: "Packages"}},
},
},
},
{
`SELECT date, timestamp as timestamp FROM Packages WHERE date = DATE '2014-09-27' AND timestamp = TIMESTAMP '2014-09-27 12:30:00'`,
Query{
Select: Select{
List: []Expr{ID("date"), ID("timestamp")},
From: []SelectFrom{SelectFromTable{Table: "Packages"}},
Where: LogicalOp{
Op: And,
LHS: ComparisonOp{
Op: Eq,
LHS: ID("date"),
RHS: DateLiteral{Year: 2014, Month: 9, Day: 27},
},
RHS: ComparisonOp{
Op: Eq,
LHS: ID("timestamp"),
RHS: TimestampLiteral(timef(t, "2006-01-02 15:04:05", "2014-09-27 12:30:00")),
},
},
ListAliases: []ID{"", "timestamp"},
},
},
},
{
`SELECT UNIX_DATE(DATE "2008-12-25")`,
Query{
Select: Select{
List: []Expr{Func{Name: "UNIX_DATE", Args: []Expr{DateLiteral{Year: 2008, Month: 12, Day: 25}}}},
},
},
},
{
`SELECT * FROM Foo WHERE STARTS_WITH(Bar, 'B')`,
Query{
Select: Select{
List: []Expr{Star},
From: []SelectFrom{SelectFromTable{Table: "Foo"}},
Where: Func{Name: "STARTS_WITH", Args: []Expr{ID("Bar"), StringLiteral("B")}},
},
},
},
{
`SELECT * FROM Foo WHERE CAST(Bar AS STRING)='Bar'`,
Query{
Select: Select{
List: []Expr{Star},
From: []SelectFrom{SelectFromTable{Table: "Foo"}},
Where: ComparisonOp{
Op: Eq,
LHS: Func{Name: "CAST", Args: []Expr{TypedExpr{Expr: ID("Bar"), Type: Type{Base: String}}}},
RHS: StringLiteral("Bar"),
},
},
},
},
{
`SELECT SUM(PointsScored) AS total_points, FirstName, LastName AS surname FROM PlayerStats GROUP BY FirstName, LastName`,
Query{
Select: Select{
List: []Expr{
Func{Name: "SUM", Args: []Expr{ID("PointsScored")}},
ID("FirstName"),
ID("LastName"),
},
From: []SelectFrom{SelectFromTable{Table: "PlayerStats"}},
GroupBy: []Expr{ID("FirstName"), ID("LastName")},
ListAliases: []ID{"total_points", "", "surname"},
},
},
},
// https://github.com/googleapis/google-cloud-go/issues/1973
{
`SELECT COUNT(*) AS count FROM Lists AS l WHERE l.user_id=@userID`,
Query{
Select: Select{
List: []Expr{
Func{Name: "COUNT", Args: []Expr{Star}},
},
From: []SelectFrom{SelectFromTable{Table: "Lists", Alias: "l"}},
Where: ComparisonOp{
Op: Eq,
LHS: PathExp{"l", "user_id"},
RHS: Param("userID"),
},
ListAliases: []ID{"count"},
},
},
},
// with single table hint
{
`SELECT * FROM Packages@{FORCE_INDEX=PackagesIdx} WHERE package_idx=@packageIdx`,
Query{
Select: Select{
List: []Expr{Star},
From: []SelectFrom{SelectFromTable{Table: "Packages", Hints: map[string]string{"FORCE_INDEX": "PackagesIdx"}}},
Where: ComparisonOp{
Op: Eq,
LHS: ID("package_idx"),
RHS: Param("packageIdx"),
},
},
},
},
// with multiple table hints
{
`SELECT * FROM Packages@{ FORCE_INDEX=PackagesIdx, GROUPBY_SCAN_OPTIMIZATION=TRUE } WHERE package_idx=@packageIdx`,
Query{
Select: Select{
List: []Expr{Star},
From: []SelectFrom{SelectFromTable{Table: "Packages", Hints: map[string]string{"FORCE_INDEX": "PackagesIdx", "GROUPBY_SCAN_OPTIMIZATION": "TRUE"}}},
Where: ComparisonOp{
Op: Eq,
LHS: ID("package_idx"),
RHS: Param("packageIdx"),
},
},
},
},
{
`SELECT * FROM A INNER JOIN B ON A.w = B.y`,
Query{
Select: Select{
List: []Expr{Star},
From: []SelectFrom{SelectFromJoin{
Type: InnerJoin,
LHS: SelectFromTable{Table: "A"},
RHS: SelectFromTable{Table: "B"},
On: ComparisonOp{
Op: Eq,
LHS: PathExp{"A", "w"},
RHS: PathExp{"B", "y"},
},
}},
},
},
},
{
`SELECT * FROM A INNER JOIN B USING (x)`,
Query{
Select: Select{
List: []Expr{Star},
From: []SelectFrom{SelectFromJoin{
Type: InnerJoin,
LHS: SelectFromTable{Table: "A"},
RHS: SelectFromTable{Table: "B"},
Using: []ID{"x"},
}},
},
},
},
{
`SELECT Roster . LastName, TeamMascot.Mascot FROM Roster JOIN TeamMascot ON Roster.SchoolID = TeamMascot.SchoolID`,
Query{
Select: Select{
List: []Expr{
PathExp{"Roster", "LastName"},
PathExp{"TeamMascot", "Mascot"},
},
From: []SelectFrom{SelectFromJoin{
Type: InnerJoin,
LHS: SelectFromTable{Table: "Roster"},
RHS: SelectFromTable{Table: "TeamMascot"},
On: ComparisonOp{
Op: Eq,
LHS: PathExp{"Roster", "SchoolID"},
RHS: PathExp{"TeamMascot", "SchoolID"},
},
}},
},
},
},
// Joins with hints.
{
`SELECT * FROM A HASH JOIN B USING (x)`,
Query{
Select: Select{
List: []Expr{Star},
From: []SelectFrom{SelectFromJoin{
Type: InnerJoin,
LHS: SelectFromTable{Table: "A"},
RHS: SelectFromTable{Table: "B"},
Using: []ID{"x"},
Hints: map[string]string{"JOIN_METHOD": "HASH_JOIN"},
}},
},
},
},
{
`SELECT * FROM A JOIN @{ JOIN_METHOD=HASH_JOIN } B USING (x)`,
Query{
Select: Select{
List: []Expr{Star},
From: []SelectFrom{SelectFromJoin{
Type: InnerJoin,
LHS: SelectFromTable{Table: "A"},
RHS: SelectFromTable{Table: "B"},
Using: []ID{"x"},
Hints: map[string]string{"JOIN_METHOD": "HASH_JOIN"},
}},
},
},
},
{
`SELECT * FROM UNNEST ([1, 2, 3]) AS data`,
Query{
Select: Select{
List: []Expr{Star},
From: []SelectFrom{SelectFromUnnest{
Expr: Array{
IntegerLiteral(1),
IntegerLiteral(2),
IntegerLiteral(3),
},
Alias: ID("data"),
}},
},
},
},
}
for _, test := range tests {
got, err := ParseQuery(test.in)
if err != nil {
t.Errorf("ParseQuery(%q): %v", test.in, err)
continue
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("ParseQuery(%q) incorrect.\n got %#v\nwant %#v", test.in, got, test.want)
}
}
}
func TestParseDMLStmt(t *testing.T) {
tests := []struct {
in string
want DMLStmt
}{
{
"INSERT Singers (SingerId, FirstName, LastName) VALUES (1, 'Marc', 'Richards')",
&Insert{
Table: "Singers",
Columns: []ID{ID("SingerId"), ID("FirstName"), ID("LastName")},
Input: Values{{IntegerLiteral(1), StringLiteral("Marc"), StringLiteral("Richards")}},
},
},
{
"INSERT INTO Singers (SingerId, FirstName, LastName) VALUES (1, 'Marc', 'Richards')",
&Insert{
Table: "Singers",
Columns: []ID{ID("SingerId"), ID("FirstName"), ID("LastName")},
Input: Values{{IntegerLiteral(1), StringLiteral("Marc"), StringLiteral("Richards")}},
},
},
{
"INSERT Singers (SingerId, FirstName, LastName) SELECT * FROM UNNEST ([1, 2, 3]) AS data",
&Insert{
Table: "Singers",
Columns: []ID{ID("SingerId"), ID("FirstName"), ID("LastName")},
Input: Select{
List: []Expr{Star},
From: []SelectFrom{SelectFromUnnest{
Expr: Array{
IntegerLiteral(1),
IntegerLiteral(2),
IntegerLiteral(3),
},
Alias: ID("data"),
}},
},
},
},
}
for _, test := range tests {
got, err := ParseDMLStmt(test.in)
if err != nil {
t.Errorf("ParseDMLStmt(%q): %v", test.in, err)
continue
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("ParseDMLStmt(%q) incorrect.\n got %#v\nwant %#v", test.in, got, test.want)
}
}
}
func TestParseExpr(t *testing.T) {
tests := []struct {
in string
want Expr
}{
{`17`, IntegerLiteral(17)},
{`-1`, IntegerLiteral(-1)},
{fmt.Sprintf(`%d`, math.MaxInt64), IntegerLiteral(math.MaxInt64)},
{fmt.Sprintf(`%d`, math.MinInt64), IntegerLiteral(math.MinInt64)},
{"1.797693134862315708145274237317043567981e+308", FloatLiteral(math.MaxFloat64)},
{`4.940656458412465441765687928682213723651e-324`, FloatLiteral(math.SmallestNonzeroFloat64)},
{`0xf00d`, IntegerLiteral(0xf00d)},
{`-0xbeef`, IntegerLiteral(-0xbeef)},
{`0XabCD`, IntegerLiteral(0xabcd)},
{`-0XBEEF`, IntegerLiteral(-0xbeef)},
{`123.456e-67`, FloatLiteral(123.456e-67)},
{`-123.456e-67`, FloatLiteral(-123.456e-67)},
{`.1E4`, FloatLiteral(0.1e4)},
{`58.`, FloatLiteral(58)},
{`4e2`, FloatLiteral(4e2)},
{`X + Y * Z`, ArithOp{LHS: ID("X"), Op: Add, RHS: ArithOp{LHS: ID("Y"), Op: Mul, RHS: ID("Z")}}},
{`X + Y + Z`, ArithOp{LHS: ArithOp{LHS: ID("X"), Op: Add, RHS: ID("Y")}, Op: Add, RHS: ID("Z")}},
{`+X * -Y`, ArithOp{LHS: ArithOp{Op: Plus, RHS: ID("X")}, Op: Mul, RHS: ArithOp{Op: Neg, RHS: ID("Y")}}},
// Don't require space around +/- operators.
{`ID+100`, ArithOp{LHS: ID("ID"), Op: Add, RHS: IntegerLiteral(100)}},
{`ID-100`, ArithOp{LHS: ID("ID"), Op: Sub, RHS: IntegerLiteral(100)}},
{`ID&0x3fff`, ArithOp{LHS: ID("ID"), Op: BitAnd, RHS: IntegerLiteral(0x3fff)}},
{`SHA1("Hello" || " " || "World")`, Func{Name: "SHA1", Args: []Expr{ArithOp{LHS: ArithOp{LHS: StringLiteral("Hello"), Op: Concat, RHS: StringLiteral(" ")}, Op: Concat, RHS: StringLiteral("World")}}}},
{`Count > 0`, ComparisonOp{LHS: ID("Count"), Op: Gt, RHS: IntegerLiteral(0)}},
{`Name LIKE "Eve %"`, ComparisonOp{LHS: ID("Name"), Op: Like, RHS: StringLiteral("Eve %")}},
{`Speech NOT LIKE "_oo"`, ComparisonOp{LHS: ID("Speech"), Op: NotLike, RHS: StringLiteral("_oo")}},
{`A AND NOT B`, LogicalOp{LHS: ID("A"), Op: And, RHS: LogicalOp{Op: Not, RHS: ID("B")}}},
{`X BETWEEN Y AND Z`, ComparisonOp{LHS: ID("X"), Op: Between, RHS: ID("Y"), RHS2: ID("Z")}},
{`@needle IN UNNEST(@haystack)`, InOp{LHS: Param("needle"), RHS: []Expr{Param("haystack")}, Unnest: true}},
{`@needle NOT IN UNNEST(@haystack)`, InOp{LHS: Param("needle"), Neg: true, RHS: []Expr{Param("haystack")}, Unnest: true}},
// Functions
{`STARTS_WITH(Bar, 'B')`, Func{Name: "STARTS_WITH", Args: []Expr{ID("Bar"), StringLiteral("B")}}},
{`CAST(Bar AS STRING)`, Func{Name: "CAST", Args: []Expr{TypedExpr{Expr: ID("Bar"), Type: Type{Base: String}}}}},
{`SAFE_CAST(Bar AS INT64)`, Func{Name: "SAFE_CAST", Args: []Expr{TypedExpr{Expr: ID("Bar"), Type: Type{Base: Int64}}}}},
{`EXTRACT(DATE FROM TIMESTAMP AT TIME ZONE "America/Los_Angeles")`, Func{Name: "EXTRACT", Args: []Expr{ExtractExpr{Part: "DATE", Type: Type{Base: Date}, Expr: AtTimeZoneExpr{Expr: ID("TIMESTAMP"), Zone: "America/Los_Angeles", Type: Type{Base: Timestamp}}}}}},
{`EXTRACT(DAY FROM DATE)`, Func{Name: "EXTRACT", Args: []Expr{ExtractExpr{Part: "DAY", Expr: ID("DATE"), Type: Type{Base: Int64}}}}},
{`DATE_ADD(CURRENT_DATE(), INTERVAL 1 DAY)`, Func{Name: "DATE_ADD", Args: []Expr{Func{Name: "CURRENT_DATE"}, IntervalExpr{Expr: IntegerLiteral(1), DatePart: "DAY"}}}},
{`DATE_SUB(CURRENT_DATE(), INTERVAL 1 WEEK)`, Func{Name: "DATE_SUB", Args: []Expr{Func{Name: "CURRENT_DATE"}, IntervalExpr{Expr: IntegerLiteral(1), DatePart: "WEEK"}}}},
{`GENERATE_DATE_ARRAY('2022-01-01', CURRENT_DATE(), INTERVAL 1 MONTH)`, Func{Name: "GENERATE_DATE_ARRAY", Args: []Expr{StringLiteral("2022-01-01"), Func{Name: "CURRENT_DATE"}, IntervalExpr{Expr: IntegerLiteral(1), DatePart: "MONTH"}}}},
{`TIMESTAMP_ADD(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)`, Func{Name: "TIMESTAMP_ADD", Args: []Expr{Func{Name: "CURRENT_TIMESTAMP"}, IntervalExpr{Expr: IntegerLiteral(1), DatePart: "HOUR"}}}},
{`TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 MINUTE)`, Func{Name: "TIMESTAMP_SUB", Args: []Expr{Func{Name: "CURRENT_TIMESTAMP"}, IntervalExpr{Expr: IntegerLiteral(1), DatePart: "MINUTE"}}}},
// Conditional expressions
{
`CASE X WHEN 1 THEN "X" WHEN 2 THEN "Y" ELSE NULL END`,
Case{
Expr: ID("X"),
WhenClauses: []WhenClause{
{Cond: IntegerLiteral(1), Result: StringLiteral("X")},
{Cond: IntegerLiteral(2), Result: StringLiteral("Y")},
},
ElseResult: Null,
},
},
{
`CASE WHEN TRUE THEN "X" WHEN FALSE THEN "Y" END`,
Case{
WhenClauses: []WhenClause{
{Cond: True, Result: StringLiteral("X")},
{Cond: False, Result: StringLiteral("Y")},
},
},
},
{
`COALESCE(NULL, "B", "C")`,
Coalesce{ExprList: []Expr{Null, StringLiteral("B"), StringLiteral("C")}},
},
{
`IF(A < B, TRUE, FALSE)`,
If{
Expr: ComparisonOp{LHS: ID("A"), Op: Lt, RHS: ID("B")},
TrueResult: True,
ElseResult: False,
},
},
{
`IFNULL(NULL, TRUE)`,
IfNull{
Expr: Null,
NullResult: True,
},
},
{
`NULLIF("a", "b")`,
NullIf{
Expr: StringLiteral("a"),
ExprToMatch: StringLiteral("b"),
},
},
// String literal:
// Accept double quote and single quote.
{`"hello"`, StringLiteral("hello")},
{`'hello'`, StringLiteral("hello")},
// Accept triple-quote.
{`""" "hello" "world" """`, StringLiteral(` "hello" "world" `)},
{"''' 'hello'\n'world' '''", StringLiteral(" 'hello'\n'world' ")},
// Simple escape sequence
{`"\a\b\f\n\r\t\v\\\?\"\'"`, StringLiteral("\a\b\f\n\r\t\v\\?\"'")},
{`'\a\b\f\n\r\t\v\\\?\"\''`, StringLiteral("\a\b\f\n\r\t\v\\?\"'")},
{"'\\`'", StringLiteral("`")},
// Hex and unicode escape sequence
{`"\060\x30\X30\u0030\U00000030"`, StringLiteral("00000")},
{`'\060\x30\X30\u0030\U00000030'`, StringLiteral("00000")},
{`"\uBEAF\ubeaf"`, StringLiteral("\ubeaf\ubeaf")},
{`'\uBEAF\ubeaf'`, StringLiteral("\ubeaf\ubeaf")},
// Escape sequence in triple quote is allowed.
{`"""\u0030"""`, StringLiteral("0")},
{`'''\u0030'''`, StringLiteral("0")},
// Raw string literal
{`R"\\"`, StringLiteral("\\\\")},
{`R'\\'`, StringLiteral("\\\\")},
{`r"\\"`, StringLiteral("\\\\")},
{`r'\\'`, StringLiteral("\\\\")},
{`R"\\\""`, StringLiteral("\\\\\\\"")},
{`R"""\\//\\//"""`, StringLiteral("\\\\//\\\\//")},
{"R'''\\\\//\n\\\\//'''", StringLiteral("\\\\//\n\\\\//")},
// Bytes literal:
{`B"hello"`, BytesLiteral("hello")},
{`B'hello'`, BytesLiteral("hello")},
{`b"hello"`, BytesLiteral("hello")},
{`b'hello'`, BytesLiteral("hello")},
{`B""" "hello" "world" """`, BytesLiteral(` "hello" "world" `)},
{`B''' 'hello' 'world' '''`, BytesLiteral(` 'hello' 'world' `)},
{`B"\a\b\f\n\r\t\v\\\?\"\'"`, BytesLiteral("\a\b\f\n\r\t\v\\?\"'")},
{`B'\a\b\f\n\r\t\v\\\?\"\''`, BytesLiteral("\a\b\f\n\r\t\v\\?\"'")},
{"B'''\n'''", BytesLiteral("\n")},
{`br"\\"`, BytesLiteral("\\\\")},
{`br'\\'`, BytesLiteral("\\\\")},
{`rb"\\"`, BytesLiteral("\\\\")},
{`rb'\\'`, BytesLiteral("\\\\")},
{`RB"\\"`, BytesLiteral("\\\\")},
{`RB'\\'`, BytesLiteral("\\\\")},
{`BR"\\"`, BytesLiteral("\\\\")},
{`BR'\\'`, BytesLiteral("\\\\")},
{`RB"""\\//\\//"""`, BytesLiteral("\\\\//\\\\//")},
{"RB'''\\\\//\n\\\\//'''", BytesLiteral("\\\\//\n\\\\//")},
// Date and timestamp literals:
{`DATE '2014-09-27'`, DateLiteral(civil.Date{Year: 2014, Month: time.September, Day: 27})},
{`TIMESTAMP '2014-09-27 12:30:00'`, TimestampLiteral(timef(t, "2006-01-02 15:04:05", "2014-09-27 12:30:00"))},
// date and timestamp funclit
{`DATE('2014-09-27')`, Func{Name: "DATE", Args: []Expr{StringLiteral("2014-09-27")}}},
{`TIMESTAMP('2014-09-27 12:30:00')`, Func{Name: "TIMESTAMP", Args: []Expr{StringLiteral("2014-09-27 12:30:00")}}},
// date and timestamp identifier
{`DATE = '2014-09-27'`, ComparisonOp{LHS: ID("DATE"), Op: Eq, RHS: StringLiteral("2014-09-27")}},
{`TIMESTAMP = '2014-09-27 12:30:00'`, ComparisonOp{LHS: ID("TIMESTAMP"), Op: Eq, RHS: StringLiteral("2014-09-27 12:30:00")}},
// Array literals:
// https://cloud.google.com/spanner/docs/lexical#array_literals
{`[1, 2, 3]`, Array{IntegerLiteral(1), IntegerLiteral(2), IntegerLiteral(3)}},
{`['x', 'y', 'xy']`, Array{StringLiteral("x"), StringLiteral("y"), StringLiteral("xy")}},
{`ARRAY[1, 2, 3]`, Array{IntegerLiteral(1), IntegerLiteral(2), IntegerLiteral(3)}},
// JSON literals:
// https://cloud.google.com/spanner/docs/reference/standard-sql/lexical#json_literals
{`JSON '{"a": 1}'`, JSONLiteral(`{"a": 1}`)},
// OR is lower precedence than AND.
{`A AND B OR C`, LogicalOp{LHS: LogicalOp{LHS: ID("A"), Op: And, RHS: ID("B")}, Op: Or, RHS: ID("C")}},
{`A OR B AND C`, LogicalOp{LHS: ID("A"), Op: Or, RHS: LogicalOp{LHS: ID("B"), Op: And, RHS: ID("C")}}},
// Parens to override normal precedence.
{`A OR (B AND C)`, LogicalOp{LHS: ID("A"), Op: Or, RHS: Paren{Expr: LogicalOp{LHS: ID("B"), Op: And, RHS: ID("C")}}}},
// This is the same as the WHERE clause from the test in ParseQuery.
{
`Age < @ageLimit AND Alias IS NOT NULL`,
LogicalOp{
LHS: ComparisonOp{LHS: ID("Age"), Op: Lt, RHS: Param("ageLimit")},
Op: And,
RHS: IsOp{LHS: ID("Alias"), Neg: true, RHS: Null},
},
},
// This used to be broken because the lexer didn't reset the token type.
{
`C < "whelp" AND D IS NOT NULL`,
LogicalOp{
LHS: ComparisonOp{LHS: ID("C"), Op: Lt, RHS: StringLiteral("whelp")},
Op: And,
RHS: IsOp{LHS: ID("D"), Neg: true, RHS: Null},
},
},
// Reserved keywords.
{`TRUE AND FALSE`, LogicalOp{LHS: True, Op: And, RHS: False}},
{`NULL`, Null},
}
for _, test := range tests {
p := newParser("test-file", test.in)
got, err := p.parseExpr()
if err != nil {
t.Errorf("[%s]: %v", test.in, err)
continue
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("[%s]: incorrect parse\n got <%T> %#v\nwant <%T> %#v", test.in, got, got, test.want, test.want)
}
if rem := p.Rem(); rem != "" {
t.Errorf("[%s]: Unparsed [%s]", test.in, rem)
}
}
}
func TestParseDDL(t *testing.T) {
tests := []struct {
in string
want *DDL
}{
{`CREATE TABLE FooBar (
System STRING(MAX) NOT NULL, # This is a comment.
RepoPath STRING(MAX) NOT NULL, -- This is another comment.
Count INT64, /* This is a
* multiline comment. */
UpdatedAt TIMESTAMP OPTIONS (allow_commit_timestamp = true),
) PRIMARY KEY(System, RepoPath);
CREATE UNIQUE INDEX MyFirstIndex ON FooBar (
Count DESC
) STORING (Count), INTERLEAVE IN SomeTable;
CREATE TABLE FooBarAux (
System STRING(MAX) NOT NULL,
CONSTRAINT Con1 FOREIGN KEY (System) REFERENCES FooBar (System),
RepoPath STRING(MAX) NOT NULL,
FOREIGN KEY (System, RepoPath) REFERENCES Stranger (Sys, RPath), -- unnamed foreign key
Author STRING(MAX) NOT NULL,
CONSTRAINT BOOL, -- not a constraint
CONSTRAINT Con4 CHECK (System != ""),
CHECK (RepoPath != ""),
) PRIMARY KEY(System, RepoPath, Author),
INTERLEAVE IN PARENT FooBar ON DELETE CASCADE;
ALTER TABLE FooBar ADD COLUMN TZ BYTES(20);
ALTER TABLE FooBar DROP COLUMN TZ;
ALTER TABLE FooBar ADD CONSTRAINT Con2 FOREIGN KEY (RepoPath) REFERENCES Repos (RPath);
ALTER TABLE FooBar DROP CONSTRAINT Con3;
ALTER TABLE FooBar SET ON DELETE NO ACTION;
ALTER TABLE FooBar ALTER COLUMN Author STRING(MAX) NOT NULL;
DROP INDEX MyFirstIndex;
DROP TABLE FooBar;
-- This table has some commentary
-- that spans multiple lines.
CREATE TABLE NonScalars (
Dummy INT64 NOT NULL, -- dummy comment
Ids ARRAY<INT64>, -- comment on ids
-- leading multi comment immediately after inline comment
BCol BOOL,
Names ARRAY<STRING(MAX)>,
) PRIMARY KEY (Dummy);
-- Table with generated column.
CREATE TABLE GenCol (
Name STRING(MAX) NOT NULL,
NameLen INT64 AS (char_length(Name)) STORED,
) PRIMARY KEY (Name);
-- Table with row deletion policy.
CREATE TABLE WithRowDeletionPolicy (
Name STRING(MAX) NOT NULL,
DelTimestamp TIMESTAMP NOT NULL,
) PRIMARY KEY (Name)
, ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY ));
ALTER TABLE WithRowDeletionPolicy DROP ROW DELETION POLICY;
ALTER TABLE WithRowDeletionPolicy ADD ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY ));
ALTER TABLE WithRowDeletionPolicy REPLACE ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY ));
CREATE VIEW SingersView
SQL SECURITY INVOKER
AS SELECT SingerId, FullName
FROM Singers
ORDER BY LastName, FirstName;
CREATE TABLE users (
user_id STRING(36) NOT NULL,
some_string STRING(16) NOT NULL,
some_time TIMESTAMP NOT NULL,
number_key INT64 AS (SAFE_CAST(SUBSTR(some_string, 2) AS INT64)) STORED,
generated_date DATE AS (EXTRACT(DATE FROM some_time AT TIME ZONE "CET")) STORED,
shard_id INT64 AS (MOD(FARM_FINGERPRINT(user_id), 19)) STORED,
) PRIMARY KEY(user_id);
-- Table has a column with a default value.
CREATE TABLE DefaultCol (
Name STRING(MAX) NOT NULL,
Age INT64 DEFAULT (0),
) PRIMARY KEY (Name);
ALTER TABLE DefaultCol ALTER COLUMN Age DROP DEFAULT;
ALTER TABLE DefaultCol ALTER COLUMN Age SET DEFAULT (0);
ALTER TABLE DefaultCol ALTER COLUMN Age STRING(MAX) DEFAULT ("0");
CREATE ROLE TestRole;
GRANT SELECT ON TABLE employees TO ROLE hr_rep;
GRANT SELECT(name, address, phone) ON TABLE contractors TO ROLE hr_rep;
GRANT SELECT, UPDATE(location), DELETE ON TABLE employees TO ROLE hr_manager;
GRANT SELECT(name, level, location), UPDATE(location) ON TABLE employees, contractors TO ROLE hr_manager;
GRANT ROLE pii_access, pii_update TO ROLE hr_manager, hr_director;
GRANT EXECUTE ON TABLE FUNCTION tvf_name_one, tvf_name_two TO ROLE hr_manager, hr_director;
GRANT SELECT ON VIEW view_name_one, view_name_two TO ROLE hr_manager, hr_director;
GRANT SELECT ON CHANGE STREAM cs_name_one, cs_name_two TO ROLE hr_manager, hr_director;
REVOKE SELECT ON TABLE employees FROM ROLE hr_rep;
REVOKE SELECT(name, address, phone) ON TABLE contractors FROM ROLE hr_rep;
REVOKE SELECT, UPDATE(location), DELETE ON TABLE employees FROM ROLE hr_manager;
REVOKE SELECT(name, level, location), UPDATE(location) ON TABLE employees, contractors FROM ROLE hr_manager;
REVOKE ROLE pii_access, pii_update FROM ROLE hr_manager, hr_director;
REVOKE EXECUTE ON TABLE FUNCTION tvf_name_one, tvf_name_two FROM ROLE hr_manager, hr_director;
REVOKE SELECT ON VIEW view_name_one, view_name_two FROM ROLE hr_manager, hr_director;
REVOKE SELECT ON CHANGE STREAM cs_name_one, cs_name_two FROM ROLE hr_manager, hr_director;
ALTER INDEX MyFirstIndex ADD STORED COLUMN UpdatedAt;
ALTER INDEX MyFirstIndex DROP STORED COLUMN UpdatedAt;
-- Trailing comment at end of file.
`, &DDL{Filename: "filename", List: []DDLStmt{
&CreateTable{
Name: "FooBar",
Columns: []ColumnDef{
{Name: "System", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(2)},
{Name: "RepoPath", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(3)},
{Name: "Count", Type: Type{Base: Int64}, Position: line(4)},
{Name: "UpdatedAt", Type: Type{Base: Timestamp}, Options: ColumnOptions{AllowCommitTimestamp: boolAddr(true)}, Position: line(6)},
},
PrimaryKey: []KeyPart{
{Column: "System"},
{Column: "RepoPath"},
},
Position: line(1),
},
&CreateIndex{
Name: "MyFirstIndex",
Table: "FooBar",
Columns: []KeyPart{{Column: "Count", Desc: true}},
Unique: true,
Storing: []ID{"Count"},
Interleave: "SomeTable",
Position: line(8),
},
&CreateTable{
Name: "FooBarAux",
Columns: []ColumnDef{
{Name: "System", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(12)},
{Name: "RepoPath", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(14)},
{Name: "Author", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(16)},
{Name: "CONSTRAINT", Type: Type{Base: Bool}, Position: line(17)},
},
Constraints: []TableConstraint{
{
Name: "Con1",
Constraint: ForeignKey{
Columns: []ID{"System"},
RefTable: "FooBar",
RefColumns: []ID{"System"},
Position: line(13),
},
Position: line(13),
},
{
Constraint: ForeignKey{
Columns: []ID{"System", "RepoPath"},
RefTable: "Stranger",
RefColumns: []ID{"Sys", "RPath"},
Position: line(15),
},
Position: line(15),
},
{
Name: "Con4",
Constraint: Check{
Expr: ComparisonOp{LHS: ID("System"), Op: Ne, RHS: StringLiteral("")},
Position: line(18),
},
Position: line(18),
},
{
Constraint: Check{
Expr: ComparisonOp{LHS: ID("RepoPath"), Op: Ne, RHS: StringLiteral("")},
Position: line(19),
},
Position: line(19),
},
},
PrimaryKey: []KeyPart{
{Column: "System"},
{Column: "RepoPath"},
{Column: "Author"},
},
Interleave: &Interleave{
Parent: "FooBar",
OnDelete: CascadeOnDelete,
},
Position: line(11),
},
&AlterTable{
Name: "FooBar",
Alteration: AddColumn{Def: ColumnDef{Name: "TZ", Type: Type{Base: Bytes, Len: 20}, Position: line(23)}},
Position: line(23),
},
&AlterTable{
Name: "FooBar",
Alteration: DropColumn{Name: "TZ"},
Position: line(24),
},
&AlterTable{
Name: "FooBar",
Alteration: AddConstraint{Constraint: TableConstraint{
Name: "Con2",
Constraint: ForeignKey{
Columns: []ID{"RepoPath"},
RefTable: "Repos",
RefColumns: []ID{"RPath"},
Position: line(25),
},
Position: line(25),
}},
Position: line(25),
},
&AlterTable{
Name: "FooBar",
Alteration: DropConstraint{Name: "Con3"},
Position: line(26),
},
&AlterTable{
Name: "FooBar",
Alteration: SetOnDelete{Action: NoActionOnDelete},
Position: line(27),
},
&AlterTable{
Name: "FooBar",
Alteration: AlterColumn{
Name: "Author",
Alteration: SetColumnType{
Type: Type{Base: String, Len: MaxLen},
NotNull: true,
},
},
Position: line(28),
},
&DropIndex{Name: "MyFirstIndex", Position: line(30)},
&DropTable{Name: "FooBar", Position: line(31)},
&CreateTable{
Name: "NonScalars",
Columns: []ColumnDef{
{Name: "Dummy", Type: Type{Base: Int64}, NotNull: true, Position: line(36)},
{Name: "Ids", Type: Type{Array: true, Base: Int64}, Position: line(37)},
{Name: "BCol", Type: Type{Base: Bool}, Position: line(39)},
{Name: "Names", Type: Type{Array: true, Base: String, Len: MaxLen}, Position: line(40)},
},
PrimaryKey: []KeyPart{{Column: "Dummy"}},
Position: line(35),
},
&CreateTable{
Name: "GenCol",
Columns: []ColumnDef{
{Name: "Name", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(45)},
{
Name: "NameLen", Type: Type{Base: Int64},
Generated: Func{Name: "CHAR_LENGTH", Args: []Expr{ID("Name")}},
Position: line(46),
},
},
PrimaryKey: []KeyPart{{Column: "Name"}},
Position: line(44),
},
&CreateTable{
Name: "WithRowDeletionPolicy",
Columns: []ColumnDef{
{Name: "Name", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(51)},
{Name: "DelTimestamp", Type: Type{Base: Timestamp}, NotNull: true, Position: line(52)},
},
PrimaryKey: []KeyPart{{Column: "Name"}},
RowDeletionPolicy: &RowDeletionPolicy{
Column: ID("DelTimestamp"),
NumDays: 30,
},
Position: line(50),
},
&AlterTable{
Name: "WithRowDeletionPolicy",
Alteration: DropRowDeletionPolicy{},
Position: line(56),
},
&AlterTable{
Name: "WithRowDeletionPolicy",
Alteration: AddRowDeletionPolicy{
RowDeletionPolicy: RowDeletionPolicy{
Column: ID("DelTimestamp"),
NumDays: 30,
},
},
Position: line(57),
},
&AlterTable{
Name: "WithRowDeletionPolicy",
Alteration: ReplaceRowDeletionPolicy{
RowDeletionPolicy: RowDeletionPolicy{
Column: ID("DelTimestamp"),
NumDays: 30,
},
},
Position: line(58),
},
&CreateView{
Name: "SingersView",
OrReplace: false,
Query: Query{
Select: Select{
List: []Expr{ID("SingerId"), ID("FullName")},
From: []SelectFrom{SelectFromTable{
Table: "Singers",
}},
},
Order: []Order{
{Expr: ID("LastName")},
{Expr: ID("FirstName")},
},
},
Position: line(60),
},
// CREATE TABLE users (
// user_id STRING(36) NOT NULL,
// some_string STRING(16) NOT NULL,
// number_key INT64 AS (SAFE_CAST(SUBSTR(some_string, 2) AS INT64)) STORED,
//) PRIMARY KEY(user_id);
&CreateTable{
Name: "users",
Columns: []ColumnDef{
{Name: "user_id", Type: Type{Base: String, Len: 36}, NotNull: true, Position: line(67)},
{Name: "some_string", Type: Type{Base: String, Len: 16}, NotNull: true, Position: line(68)},
{Name: "some_time", Type: Type{Base: Timestamp}, NotNull: true, Position: line(69)},
{
Name: "number_key", Type: Type{Base: Int64},
Generated: Func{Name: "SAFE_CAST", Args: []Expr{
TypedExpr{Expr: Func{Name: "SUBSTR", Args: []Expr{ID("some_string"), IntegerLiteral(2)}}, Type: Type{Base: Int64}},
}},
Position: line(70),
},
{
Name: "generated_date", Type: Type{Base: Date},
Generated: Func{Name: "EXTRACT", Args: []Expr{
ExtractExpr{Part: "DATE", Type: Type{Base: Date}, Expr: AtTimeZoneExpr{Expr: ID("some_time"), Zone: "CET", Type: Type{Base: Timestamp}}},
}},
Position: line(71),
},
{
Name: "shard_id", Type: Type{Base: Int64},
Generated: Func{Name: "MOD", Args: []Expr{
Func{Name: "FARM_FINGERPRINT", Args: []Expr{ID("user_id")}}, IntegerLiteral(19),
}},
Position: line(72),
},
},
PrimaryKey: []KeyPart{{Column: "user_id"}},
Position: line(66),
},
&CreateTable{
Name: "DefaultCol",
Columns: []ColumnDef{
{Name: "Name", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(77)},
{
Name: "Age", Type: Type{Base: Int64},
Default: IntegerLiteral(0),
Position: line(78),
},
},
PrimaryKey: []KeyPart{{Column: "Name"}},
Position: line(76),
},
&AlterTable{
Name: "DefaultCol",
Alteration: AlterColumn{
Name: "Age",
Alteration: DropDefault{},
},
Position: line(81),
},
&AlterTable{
Name: "DefaultCol",
Alteration: AlterColumn{
Name: "Age",
Alteration: SetDefault{
Default: IntegerLiteral(0),
},
},
Position: line(82),
},
&AlterTable{
Name: "DefaultCol",
Alteration: AlterColumn{
Name: "Age",
Alteration: SetColumnType{
Type: Type{Base: String, Len: MaxLen},
Default: StringLiteral("0"),
},
},
Position: line(83),
},
&CreateRole{
Name: "TestRole",
Position: line(85),
},
&GrantRole{
ToRoleNames: []ID{"hr_rep"},
Privileges: []Privilege{
{Type: PrivilegeTypeSelect},
},
TableNames: []ID{"employees"},
Position: line(87),
},
&GrantRole{
ToRoleNames: []ID{"hr_rep"},
Privileges: []Privilege{
{Type: PrivilegeTypeSelect, Columns: []ID{"name", "address", "phone"}},
},
TableNames: []ID{"contractors"},
Position: line(88),
},
&GrantRole{
ToRoleNames: []ID{"hr_manager"},
Privileges: []Privilege{
{Type: PrivilegeTypeSelect},
{Type: PrivilegeTypeUpdate, Columns: []ID{"location"}},
{Type: PrivilegeTypeDelete},
},
TableNames: []ID{"employees"},
Position: line(89),
},
&GrantRole{
ToRoleNames: []ID{"hr_manager"},
Privileges: []Privilege{
{Type: PrivilegeTypeSelect, Columns: []ID{"name", "level", "location"}},
{Type: PrivilegeTypeUpdate, Columns: []ID{"location"}},
},
TableNames: []ID{"employees", "contractors"},
Position: line(90),
},
&GrantRole{
ToRoleNames: []ID{"hr_manager", "hr_director"},
GrantRoleNames: []ID{"pii_access", "pii_update"},
Position: line(91),
},
&GrantRole{
ToRoleNames: []ID{"hr_manager", "hr_director"},
TvfNames: []ID{"tvf_name_one", "tvf_name_two"},
Position: line(92),
},
&GrantRole{
ToRoleNames: []ID{"hr_manager", "hr_director"},
ViewNames: []ID{"view_name_one", "view_name_two"},
Position: line(93),
},
&GrantRole{
ToRoleNames: []ID{"hr_manager", "hr_director"},
ChangeStreamNames: []ID{"cs_name_one", "cs_name_two"},
Position: line(94),
},
&RevokeRole{
FromRoleNames: []ID{"hr_rep"},
Privileges: []Privilege{
{Type: PrivilegeTypeSelect},
},
TableNames: []ID{"employees"},
Position: line(96),
},
&RevokeRole{
FromRoleNames: []ID{"hr_rep"},
Privileges: []Privilege{
{Type: PrivilegeTypeSelect, Columns: []ID{"name", "address", "phone"}},
},
TableNames: []ID{"contractors"},
Position: line(97),
},
&RevokeRole{
FromRoleNames: []ID{"hr_manager"},
Privileges: []Privilege{
{Type: PrivilegeTypeSelect},
{Type: PrivilegeTypeUpdate, Columns: []ID{"location"}},
{Type: PrivilegeTypeDelete},
},
TableNames: []ID{"employees"},
Position: line(98),
},
&RevokeRole{
FromRoleNames: []ID{"hr_manager"},
Privileges: []Privilege{
{Type: PrivilegeTypeSelect, Columns: []ID{"name", "level", "location"}},
{Type: PrivilegeTypeUpdate, Columns: []ID{"location"}},
},
TableNames: []ID{"employees", "contractors"},
Position: line(99),
},
&RevokeRole{
FromRoleNames: []ID{"hr_manager", "hr_director"},
RevokeRoleNames: []ID{"pii_access", "pii_update"},
Position: line(100),
},
&RevokeRole{
FromRoleNames: []ID{"hr_manager", "hr_director"},
TvfNames: []ID{"tvf_name_one", "tvf_name_two"},
Position: line(101),
},
&RevokeRole{
FromRoleNames: []ID{"hr_manager", "hr_director"},
ViewNames: []ID{"view_name_one", "view_name_two"},
Position: line(102),
},
&RevokeRole{
FromRoleNames: []ID{"hr_manager", "hr_director"},
ChangeStreamNames: []ID{"cs_name_one", "cs_name_two"},
Position: line(103),
},
&AlterIndex{
Name: "MyFirstIndex",
Alteration: AddStoredColumn{Name: "UpdatedAt"},
Position: line(105),
},
&AlterIndex{
Name: "MyFirstIndex",
Alteration: DropStoredColumn{Name: "UpdatedAt"},
Position: line(106),
},
}, Comments: []*Comment{
{
Marker: "#", Start: line(2), End: line(2),
Text: []string{"This is a comment."},
},
{
Marker: "--", Start: line(3), End: line(3),
Text: []string{"This is another comment."},
},
{
Marker: "/*", Start: line(4), End: line(5),
Text: []string{" This is a", "\t\t\t\t\t\t * multiline comment."},
},
{
Marker: "--", Start: line(15), End: line(15),
Text: []string{"unnamed foreign key"},
},
{
Marker: "--", Start: line(17), End: line(17),
Text: []string{"not a constraint"},
},
{
Marker: "--", Isolated: true, Start: line(33), End: line(34),
Text: []string{"This table has some commentary", "that spans multiple lines."},
},
// These comments shouldn't get combined:
{Marker: "--", Start: line(36), End: line(36), Text: []string{"dummy comment"}},
{Marker: "--", Start: line(37), End: line(37), Text: []string{"comment on ids"}},
{Marker: "--", Isolated: true, Start: line(38), End: line(38), Text: []string{"leading multi comment immediately after inline comment"}},
{Marker: "--", Isolated: true, Start: line(43), End: line(43), Text: []string{"Table with generated column."}},
{Marker: "--", Isolated: true, Start: line(49), End: line(49), Text: []string{"Table with row deletion policy."}},
{Marker: "--", Isolated: true, Start: line(75), End: line(75), Text: []string{"Table has a column with a default value."}},
// Comment after everything else.
{Marker: "--", Isolated: true, Start: line(108), End: line(108), Text: []string{"Trailing comment at end of file."}},
}}},
// No trailing comma:
{`ALTER TABLE T ADD COLUMN C2 INT64`, &DDL{Filename: "filename", List: []DDLStmt{
&AlterTable{
Name: "T",
Alteration: AddColumn{Def: ColumnDef{Name: "C2", Type: Type{Base: Int64}, Position: line(1)}},
Position: line(1),
},
}}},
// Table and column names using reserved keywords.
{`CREATE TABLE ` + "`enum`" + ` (
` + "`With`" + ` STRING(MAX) NOT NULL,
) PRIMARY KEY(` + "`With`" + `);
`, &DDL{Filename: "filename", List: []DDLStmt{
&CreateTable{
Name: "enum",
Columns: []ColumnDef{
{Name: "With", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(2)},
},
PrimaryKey: []KeyPart{
{Column: "With"},
},
Position: line(1),
},
}}},
{
`ALTER DATABASE dbname SET OPTIONS (optimizer_version=2, optimizer_statistics_package='auto_20191128_14_47_22UTC', version_retention_period='7d', enable_key_visualizer=true, default_leader='europe-west1')`,
&DDL{
Filename: "filename", List: []DDLStmt{
&AlterDatabase{
Name: "dbname",
Alteration: SetDatabaseOptions{
Options: DatabaseOptions{
OptimizerVersion: func(i int) *int { return &i }(2),
OptimizerStatisticsPackage: func(s string) *string { return &s }("auto_20191128_14_47_22UTC"),
VersionRetentionPeriod: func(s string) *string { return &s }("7d"),
EnableKeyVisualizer: func(b bool) *bool { return &b }(true),
DefaultLeader: func(s string) *string { return &s }("europe-west1"),
},
},
Position: line(1),
},
},
},
},
{
`ALTER DATABASE dbname SET OPTIONS (optimizer_version=2, optimizer_statistics_package='auto_20191128_14_47_22UTC', version_retention_period='7d', enable_key_visualizer=true, default_leader='europe-west1'); CREATE TABLE users (UserId STRING(MAX) NOT NULL,) PRIMARY KEY (UserId);`,
&DDL{
Filename: "filename", List: []DDLStmt{
&AlterDatabase{
Name: "dbname",
Alteration: SetDatabaseOptions{
Options: DatabaseOptions{
OptimizerVersion: func(i int) *int { return &i }(2),
OptimizerStatisticsPackage: func(s string) *string { return &s }("auto_20191128_14_47_22UTC"),
VersionRetentionPeriod: func(s string) *string { return &s }("7d"),
EnableKeyVisualizer: func(b bool) *bool { return &b }(true),
DefaultLeader: func(s string) *string { return &s }("europe-west1"),
},
},
Position: line(1),
},
&CreateTable{
Name: "users", Columns: []ColumnDef{
{Name: "UserId", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(1)},
},
PrimaryKey: []KeyPart{
{Column: "UserId"},
},
Position: line(1),
},
},
},
},
{
`ALTER DATABASE dbname SET OPTIONS (optimizer_version=null, optimizer_statistics_package=null, version_retention_period=null, enable_key_visualizer=null, default_leader=null)`,
&DDL{
Filename: "filename", List: []DDLStmt{
&AlterDatabase{
Name: "dbname",
Alteration: SetDatabaseOptions{
Options: DatabaseOptions{
OptimizerVersion: func(i int) *int { return &i }(0),
OptimizerStatisticsPackage: func(s string) *string { return &s }(""),
VersionRetentionPeriod: func(s string) *string { return &s }(""),
EnableKeyVisualizer: func(b bool) *bool { return &b }(false),
DefaultLeader: func(s string) *string { return &s }(""),
},
},
Position: line(1),
},
},
},
},
{
"CREATE OR REPLACE VIEW `SingersView` SQL SECURITY INVOKER AS SELECT SingerId, FullName, Picture FROM Singers ORDER BY LastName, FirstName",
&DDL{
Filename: "filename", List: []DDLStmt{
&CreateView{
Name: "SingersView",
OrReplace: true,
Query: Query{
Select: Select{
List: []Expr{ID("SingerId"), ID("FullName"), ID("Picture")},
From: []SelectFrom{SelectFromTable{
Table: "Singers",
}},
},
Order: []Order{
{Expr: ID("LastName")},
{Expr: ID("FirstName")},
},
},
Position: line(1),
},
},
},
},
{
"DROP VIEW `SingersView`",
&DDL{
Filename: "filename", List: []DDLStmt{
&DropView{
Name: "SingersView",
Position: line(1),
},
},
},
},
{`ALTER TABLE products ADD COLUMN item STRING(MAX) AS (JSON_VALUE(itemDetails, '$.itemDetails')) STORED`, &DDL{Filename: "filename", List: []DDLStmt{
&AlterTable{
Name: "products",
Alteration: AddColumn{Def: ColumnDef{
Name: "item",
Type: Type{Base: String, Len: MaxLen},
Position: line(1),
Generated: Func{
Name: "JSON_VALUE",
Args: []Expr{ID("itemDetails"), StringLiteral("$.itemDetails")},
},
}},
Position: line(1),
},
}}},
{
`ALTER STATISTICS auto_20191128_14_47_22UTC SET OPTIONS (allow_gc=false)`,
&DDL{
Filename: "filename",
List: []DDLStmt{
&AlterStatistics{
Name: "auto_20191128_14_47_22UTC",
Alteration: SetStatisticsOptions{
Options: StatisticsOptions{
AllowGC: func(b bool) *bool { return &b }(false),
},
},
Position: line(1),
},
},
},
},
{
"DROP ROLE `TestRole`",
&DDL{
Filename: "filename", List: []DDLStmt{
&DropRole{
Name: "TestRole",
Position: line(1),
},
},
},
},
{
"GRANT SELECT(`name`, `level`, `location`), UPDATE(`location`) ON TABLE `employees`, `contractors` TO ROLE `hr_manager`;",
&DDL{
Filename: "filename", List: []DDLStmt{
&GrantRole{
ToRoleNames: []ID{"hr_manager"},
Privileges: []Privilege{
{Type: PrivilegeTypeSelect, Columns: []ID{"name", "level", "location"}},
{Type: PrivilegeTypeUpdate, Columns: []ID{"location"}},
},
TableNames: []ID{"employees", "contractors"},
Position: line(1),
},
},
},
},
{
"GRANT ROLE `pii_access`, `pii_update` TO ROLE `hr_manager`, `hr_director`;",
&DDL{
Filename: "filename", List: []DDLStmt{
&GrantRole{
ToRoleNames: []ID{"hr_manager", "hr_director"},
GrantRoleNames: []ID{"pii_access", "pii_update"},
Position: line(1),
},
},
},
},
{
"REVOKE SELECT(`name`, `level`, `location`), UPDATE(`location`) ON TABLE `employees`, `contractors` FROM ROLE `hr_manager`;",
&DDL{
Filename: "filename", List: []DDLStmt{
&RevokeRole{
FromRoleNames: []ID{"hr_manager"},
Privileges: []Privilege{
{Type: PrivilegeTypeSelect, Columns: []ID{"name", "level", "location"}},
{Type: PrivilegeTypeUpdate, Columns: []ID{"location"}},
},
TableNames: []ID{"employees", "contractors"},
Position: line(1),
},
},
},
},
{
"REVOKE ROLE `pii_access`, `pii_update` FROM ROLE `hr_manager`, `hr_director`;",
&DDL{
Filename: "filename", List: []DDLStmt{
&RevokeRole{
FromRoleNames: []ID{"hr_manager", "hr_director"},
RevokeRoleNames: []ID{"pii_access", "pii_update"},
Position: line(1),
},
},
},
},
{
`CREATE CHANGE STREAM csname;
CREATE CHANGE STREAM csname FOR ALL;
CREATE CHANGE STREAM csname FOR tname, tname2(cname);
CREATE CHANGE STREAM csname FOR ALL OPTIONS (retention_period = '36h', value_capture_type = 'NEW_VALUES');`,
&DDL{
Filename: "filename",
List: []DDLStmt{
&CreateChangeStream{
Name: "csname",
Position: line(1),
},
&CreateChangeStream{
Name: "csname",
WatchAllTables: true,
Position: line(2),
},
&CreateChangeStream{
Name: "csname",
Watch: []WatchDef{
{Table: "tname", WatchAllCols: true, Position: line(3)},
{Table: "tname2", Columns: []ID{ID("cname")}, Position: line(3)},
},
Position: line(3),
},
&CreateChangeStream{
Name: "csname",
WatchAllTables: true,
Position: line(4),
Options: ChangeStreamOptions{
RetentionPeriod: func(b string) *string { return &b }("36h"),
ValueCaptureType: func(b string) *string { return &b }("NEW_VALUES"),
},
},
},
},
},
{
`ALTER CHANGE STREAM csname SET FOR ALL;
ALTER CHANGE STREAM csname SET FOR tname, tname2(cname);
ALTER CHANGE STREAM csname DROP FOR ALL;
ALTER CHANGE STREAM csname SET OPTIONS (retention_period = '36h', value_capture_type = 'NEW_VALUES');`,
&DDL{
Filename: "filename",
List: []DDLStmt{
&AlterChangeStream{
Name: "csname",
Alteration: AlterWatch{
WatchAllTables: true,
},
Position: line(1),
},
&AlterChangeStream{
Name: "csname",
Alteration: AlterWatch{
Watch: []WatchDef{
{
Table: "tname",
WatchAllCols: true,
Position: Position{Line: 2, Offset: 78},
},
{
Table: "tname2",
Columns: []ID{"cname"},
Position: Position{Line: 2, Offset: 85},
},
},
},
Position: line(2),
},
&AlterChangeStream{
Name: "csname",
Alteration: DropChangeStreamWatch{},
Position: line(3),
},
&AlterChangeStream{
Name: "csname",
Alteration: AlterChangeStreamOptions{
Options: ChangeStreamOptions{
RetentionPeriod: func(b string) *string { return &b }("36h"),
ValueCaptureType: func(b string) *string { return &b }("NEW_VALUES"),
},
},
Position: line(4),
},
},
},
},
{
`DROP CHANGE STREAM csname`,
&DDL{
Filename: "filename",
List: []DDLStmt{
&DropChangeStream{
Name: "csname",
Position: line(1),
},
},
},
},
{
`CREATE TABLE IF NOT EXISTS tname (id INT64, name STRING(64)) PRIMARY KEY (id)`,
&DDL{
Filename: "filename",
List: []DDLStmt{
&CreateTable{
Name: "tname",
IfNotExists: true,
Columns: []ColumnDef{
{Name: "id", Type: Type{Base: Int64}, Position: line(1)},
{Name: "name", Type: Type{Base: String, Len: 64}, Position: line(1)},
},
PrimaryKey: []KeyPart{
{Column: "id"},
},
Position: line(1),
},
},
},
},
{
`CREATE INDEX IF NOT EXISTS iname ON tname (cname)`,
&DDL{
Filename: "filename",
List: []DDLStmt{
&CreateIndex{
Name: "iname",
IfNotExists: true,
Table: "tname",
Columns: []KeyPart{
{Column: "cname"},
},
Position: line(1),
},
},
},
},
{
`ALTER TABLE tname ADD COLUMN IF NOT EXISTS cname STRING(64)`,
&DDL{
Filename: "filename",
List: []DDLStmt{
&AlterTable{
Name: "tname",
Alteration: AddColumn{
IfNotExists: true,
Def: ColumnDef{Name: "cname", Type: Type{Base: String, Len: 64}, Position: line(1)},
},
Position: line(1),
},
},
},
},
{
`DROP TABLE IF EXISTS tname;
DROP INDEX IF EXISTS iname;`,
&DDL{
Filename: "filename",
List: []DDLStmt{
&DropTable{
Name: "tname",
IfExists: true,
Position: line(1),
},
&DropIndex{
Name: "iname",
IfExists: true,
Position: line(2),
},
},
},
},
{
`CREATE TABLE tname1 (col1 INT64, col2 INT64, CONSTRAINT con1 FOREIGN KEY (col2) REFERENCES tname2 (col3) ON DELETE CASCADE) PRIMARY KEY (col1);
CREATE TABLE tname1 (col1 INT64, col2 INT64, CONSTRAINT con1 FOREIGN KEY (col2) REFERENCES tname2 (col3) ON DELETE NO ACTION) PRIMARY KEY (col1);
ALTER TABLE tname1 ADD CONSTRAINT con1 FOREIGN KEY (col2) REFERENCES tname2 (col3) ON DELETE CASCADE;
ALTER TABLE tname1 ADD CONSTRAINT con1 FOREIGN KEY (col2) REFERENCES tname2 (col3) ON DELETE NO ACTION;`,
&DDL{
Filename: "filename",
List: []DDLStmt{
&CreateTable{
Name: "tname1",
Columns: []ColumnDef{
{Name: "col1", Type: Type{Base: Int64}, Position: line(1)},
{Name: "col2", Type: Type{Base: Int64}, Position: line(1)},
},
Constraints: []TableConstraint{
{Name: "con1", Constraint: ForeignKey{Columns: []ID{"col2"}, RefTable: "tname2", RefColumns: []ID{"col3"}, OnDelete: CascadeOnDelete, Position: line(1)}, Position: line(1)},
},
PrimaryKey: []KeyPart{
{Column: "col1"},
},
Position: line(1),
},
&CreateTable{
Name: "tname1",
Columns: []ColumnDef{
{Name: "col1", Type: Type{Base: Int64}, Position: line(2)},
{Name: "col2", Type: Type{Base: Int64}, Position: line(2)},
},
Constraints: []TableConstraint{
{Name: "con1", Constraint: ForeignKey{Columns: []ID{"col2"}, RefTable: "tname2", RefColumns: []ID{"col3"}, OnDelete: NoActionOnDelete, Position: line(2)}, Position: line(2)},
},
PrimaryKey: []KeyPart{
{Column: "col1"},
},
Position: line(2),
},
&AlterTable{
Name: "tname1",
Alteration: AddConstraint{
Constraint: TableConstraint{Name: "con1", Constraint: ForeignKey{Columns: []ID{"col2"}, RefTable: "tname2", RefColumns: []ID{"col3"}, OnDelete: CascadeOnDelete, Position: line(3)}, Position: line(3)},
},
Position: line(3),
},
&AlterTable{
Name: "tname1",
Alteration: AddConstraint{
Constraint: TableConstraint{Name: "con1", Constraint: ForeignKey{Columns: []ID{"col2"}, RefTable: "tname2", RefColumns: []ID{"col3"}, OnDelete: NoActionOnDelete, Position: line(4)}, Position: line(4)},
},
Position: line(4),
},
},
},
},
}
for _, test := range tests {
got, err := ParseDDL("filename", test.in)
if err != nil {
t.Errorf("ParseDDL(%q): %v", test.in, err)
continue
}
got.clearOffset()
if !reflect.DeepEqual(got, test.want) {
t.Errorf("ParseDDL(%q) incorrect.\n got %v\nwant %v", test.in, got, test.want)
// Also log the specific elements that don't match to make it easier to debug
// especially the large DDLs.
for i := range got.List {
if !reflect.DeepEqual(got.List[i], test.want.List[i]) {
t.Errorf("\tstatement %d mismatch:\n\t got %v\n\twant %v", i, got.List[i], test.want.List[i])
}
}
for i := range got.Comments {
if !reflect.DeepEqual(got.Comments[i], test.want.Comments[i]) {
t.Errorf("\tcomment %d mismatch:\n\t got %v\n\twant %v", i, got.Comments[i], test.want.Comments[i])
}
}
}
}
// Check the comment discovey helpers on the first DDL.
// Reparse it first so we get full position information.
ddl, err := ParseDDL("filename", tests[0].in)
if err != nil {
t.Fatal(err)
}
// The CreateTable for NonScalars has a leading comment.
com := ddl.LeadingComment(tableByName(t, ddl, "NonScalars"))
if com == nil {
t.Errorf("No leading comment found for NonScalars")
} else if com.Text[0] != "This table has some commentary" {
t.Errorf("LeadingComment returned the wrong comment for NonScalars")
}
// Second field of FooBar (RepoPath) has an inline comment.
cd := tableByName(t, ddl, "FooBar").Columns[1]
if com := ddl.InlineComment(cd); com == nil {
t.Errorf("No inline comment found for FooBar.RepoPath")
} else if com.Text[0] != "This is another comment." {
t.Errorf("InlineComment returned the wrong comment (%q) for FooBar.RepoPath", com.Text[0])
}
// There are no leading comments on the columns of NonScalars (except for BCol),
// even though there's often a comment on the previous line.
for _, cd := range tableByName(t, ddl, "NonScalars").Columns {
if cd.Name == "BCol" {
continue
}
if com := ddl.LeadingComment(cd); com != nil {
t.Errorf("Leading comment found for NonScalars.%s: %v", cd.Name, com)
}
}
}
func TestParseDML(t *testing.T) {
tests := []struct {
in string
want *DML
}{
{
`UPDATE FooBar SET Name = "foo"
WHERE ID = 0; # This is a comment.
Update FooBar SET Name = "foo" /* This is a
* multiline comment. */
WHERE ID = 0;
INSERT FooBar (ID, Name) VALUES (0, 'foo');
DELETE FROM FooBar WHERE Name = "foo"; -- This is another comment.
-- This is an isolated comment.
`, &DML{Filename: "filename", List: []DMLStmt{
&Update{
Table: "FooBar",
Items: []UpdateItem{
{Column: "Name", Value: StringLiteral("foo")},
},
Where: ComparisonOp{Op: 4, LHS: ID("ID"), RHS: IntegerLiteral(0), RHS2: nil},
},
&Update{
Table: "FooBar",
Items: []UpdateItem{
{Column: "Name", Value: StringLiteral("foo")},
},
Where: ComparisonOp{Op: 4, LHS: ID("ID"), RHS: IntegerLiteral(0), RHS2: nil},
},
&Insert{
Table: "FooBar",
Columns: []ID{"ID", "Name"},
Input: Values{[]Expr{IntegerLiteral(0), StringLiteral("foo")}},
},
&Delete{
Table: "FooBar",
Where: ComparisonOp{Op: 4, LHS: ID("Name"), RHS: StringLiteral("foo"), RHS2: nil},
},
}, Comments: []*Comment{
{
Marker: "#", Start: line(2), End: line(2),
Text: []string{"This is a comment."},
},
{
Marker: "/*", Start: line(3), End: line(4),
Text: []string{" This is a", "\t\t\t\t\t\t\t\t\t * multiline comment."},
Isolated: false,
},
{
Marker: "--", Start: line(7), End: line(7),
Text: []string{"This is another comment."},
Isolated: false,
},
{
Marker: "--", Start: line(8), End: line(8),
Text: []string{"This is an isolated comment."},
Isolated: true,
},
}},
},
// No trailing comma:
{`Update FooBar SET Name = "foo" WHERE ID = 0`, &DML{
Filename: "filename", List: []DMLStmt{
&Update{
Table: "FooBar",
Items: []UpdateItem{
{Column: "Name", Value: StringLiteral("foo")},
},
Where: ComparisonOp{Op: 4, LHS: ID("ID"), RHS: IntegerLiteral(0), RHS2: nil},
},
},
}},
}
for _, test := range tests {
got, err := ParseDML("filename", test.in)
if err != nil {
t.Errorf("ParseDML(%q): %v", test.in, err)
continue
}
got.clearOffset()
if !reflect.DeepEqual(got, test.want) {
t.Errorf("ParseDML(%q) incorrect.\n got %v\nwant %v", test.in, got, test.want)
// Also log the specific elements that don't match to make it easier to debug
// especially the large DMLs.
for i := range got.List {
if !reflect.DeepEqual(got.List[i], test.want.List[i]) {
t.Errorf("\tstatement %d mismatch:\n\t got %v\n\twant %v", i, got.List[i], test.want.List[i])
}
}
for i := range got.Comments {
if !reflect.DeepEqual(got.Comments[i], test.want.Comments[i]) {
t.Errorf("\tcomment %d mismatch:\n\t got %v\n\twant %v", i, got.Comments[i], test.want.Comments[i])
}
}
}
}
}
func line(n int) Position { return Position{Line: n} }
func tableByName(t *testing.T, ddl *DDL, name ID) *CreateTable {
t.Helper()
for _, stmt := range ddl.List {
if ct, ok := stmt.(*CreateTable); ok && ct.Name == name {
return ct
}
}
t.Fatalf("no table with name %q", name)
panic("unreachable")
}
func TestParseFailures(t *testing.T) {
expr := func(p *parser) error {
if _, pe := p.parseExpr(); pe != nil {
return pe
}
return nil
}
query := func(p *parser) error {
if _, pe := p.parseQuery(); pe != nil {
return pe
}
return nil
}
tests := []struct {
f func(p *parser) error
in string
desc string
}{
{expr, `0b337`, "binary literal"},
{expr, `"foo\`, "unterminated string"},
{expr, `"\i"`, "invalid escape sequence"},
{expr, `"\0"`, "invalid escape sequence"},
{expr, `"\099"`, "invalid escape sequence"},
{expr, `"\400"`, "invalid escape sequence: octal digits overflow"},
{expr, `"\x"`, "invalid escape sequence"},
{expr, `"\xFZ"`, "invalid escape sequence"},
{expr, `"\u"`, "invalid escape sequence"},
{expr, `"\uFFFZ"`, "invalid escape sequence"},
{expr, `"\uD800"`, "invalid unicode character (surrogate)"},
{expr, `"\U"`, "invalid escape sequence"},
{expr, `"\UFFFFFFFZ"`, "invalid escape sequence"},
{expr, `"\U00110000"`, "invalid unicode character (out of range)"},
{expr, "\"\n\"", "unterminated string by newline (double quote)"},
{expr, "'\n'", "unterminated string by newline (single quote)"},
{expr, "R\"\n\"", "unterminated raw string by newline (double quote)"},
{expr, "R'\n'", "unterminated raw string by newline (single quote)"},
{expr, `B"\u0030"`, "\\uXXXX sequence is not supported in bytes literal (double quote)"},
{expr, `B'\u0030'`, "\\uXXXX sequence is not supported in bytes literal (double quote)"},
{expr, `B"\U00000030"`, "\\UXXXXXXXX sequence is not supported in bytes literal (double quote)"},
{expr, `B'\U00000030'`, "\\UXXXXXXXX sequence is not supported in bytes literal (double quote)"},
{expr, `BB""`, "invalid string-like literal prefix"},
{expr, `rr""`, "invalid string-like literal prefix"},
{expr, `"""\"""`, "unterminated triple-quoted string by last backslash (double quote)"},
{expr, `'''\'''`, "unterminated triple-quoted string by last backslash (single quote)"},
{expr, `"foo" AND "bar"`, "logical operation on string literals"},
// Found by fuzzing.
// https://github.com/googleapis/google-cloud-go/issues/2196
{query, `/*/*/`, "invalid comment termination"},
}
for _, test := range tests {
p := newParser("f", test.in)
err := test.f(p)
if err == nil && p.Rem() == "" {
t.Errorf("%s: parsing [%s] succeeded, should have failed", test.desc, test.in)
}
}
}
func timef(t *testing.T, format, s string) time.Time {
ti, err := time.ParseInLocation(format, string(s), defaultLocation)
if err != nil {
t.Errorf("parsing %s [%s] time.ParseInLocation failed.", s, format)
}
return ti
}