github.com/seashell-org/golang-migrate/v4@v4.15.3-0.20220722221203-6ab6c6c062d1/database/spanner/spansql/parser_test.go (about)

     1  /*
     2  Copyright 2019 Google LLC
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package spansql
    18  
    19  import (
    20  	"fmt"
    21  	"math"
    22  	"reflect"
    23  	"testing"
    24  	"time"
    25  
    26  	"cloud.google.com/go/civil"
    27  )
    28  
    29  func TestParseQuery(t *testing.T) {
    30  	tests := []struct {
    31  		in   string
    32  		want Query
    33  	}{
    34  		{`SELECT 17`, Query{Select: Select{List: []Expr{IntegerLiteral(17)}}}},
    35  		{`SELECT Alias AS aka From Characters WHERE Age < @ageLimit AND Alias IS NOT NULL ORDER BY Age DESC LIMIT @limit OFFSET 3` + "\n\t",
    36  			Query{
    37  				Select: Select{
    38  					List: []Expr{ID("Alias")},
    39  					From: []SelectFrom{SelectFromTable{
    40  						Table: "Characters",
    41  					}},
    42  					Where: LogicalOp{
    43  						Op: And,
    44  						LHS: ComparisonOp{
    45  							LHS: ID("Age"),
    46  							Op:  Lt,
    47  							RHS: Param("ageLimit"),
    48  						},
    49  						RHS: IsOp{
    50  							LHS: ID("Alias"),
    51  							Neg: true,
    52  							RHS: Null,
    53  						},
    54  					},
    55  					ListAliases: []ID{"aka"},
    56  				},
    57  				Order: []Order{{
    58  					Expr: ID("Age"),
    59  					Desc: true,
    60  				}},
    61  				Limit:  Param("limit"),
    62  				Offset: IntegerLiteral(3),
    63  			},
    64  		},
    65  		{`SELECT COUNT(*) FROM Packages`,
    66  			Query{
    67  				Select: Select{
    68  					List: []Expr{
    69  						Func{
    70  							Name: "COUNT",
    71  							Args: []Expr{Star},
    72  						},
    73  					},
    74  					From: []SelectFrom{SelectFromTable{Table: "Packages"}},
    75  				},
    76  			},
    77  		},
    78  		{`SELECT * FROM Packages`,
    79  			Query{
    80  				Select: Select{
    81  					List: []Expr{Star},
    82  					From: []SelectFrom{SelectFromTable{Table: "Packages"}},
    83  				},
    84  			},
    85  		},
    86  		{`SELECT date, timestamp as timestamp FROM Packages WHERE date = DATE '2014-09-27' AND timestamp = TIMESTAMP '2014-09-27 12:30:00'`,
    87  			Query{
    88  				Select: Select{
    89  					List: []Expr{ID("date"), ID("timestamp")},
    90  					From: []SelectFrom{SelectFromTable{Table: "Packages"}},
    91  					Where: LogicalOp{
    92  						Op: And,
    93  						LHS: ComparisonOp{
    94  							Op:  Eq,
    95  							LHS: ID("date"),
    96  							RHS: DateLiteral{Year: 2014, Month: 9, Day: 27},
    97  						},
    98  						RHS: ComparisonOp{
    99  							Op:  Eq,
   100  							LHS: ID("timestamp"),
   101  							RHS: TimestampLiteral(timef(t, "2006-01-02 15:04:05", "2014-09-27 12:30:00")),
   102  						},
   103  					},
   104  					ListAliases: []ID{"", "timestamp"},
   105  				},
   106  			},
   107  		},
   108  		{`SELECT UNIX_DATE(DATE "2008-12-25")`,
   109  			Query{
   110  				Select: Select{
   111  					List: []Expr{Func{Name: "UNIX_DATE", Args: []Expr{DateLiteral{Year: 2008, Month: 12, Day: 25}}}},
   112  				},
   113  			},
   114  		},
   115  		{`SELECT * FROM Foo WHERE STARTS_WITH(Bar, 'B')`,
   116  			Query{
   117  				Select: Select{
   118  					List:  []Expr{Star},
   119  					From:  []SelectFrom{SelectFromTable{Table: "Foo"}},
   120  					Where: Func{Name: "STARTS_WITH", Args: []Expr{ID("Bar"), StringLiteral("B")}},
   121  				},
   122  			},
   123  		},
   124  		{`SELECT * FROM Foo WHERE CAST(Bar AS STRING)='Bar'`,
   125  			Query{
   126  				Select: Select{
   127  					List: []Expr{Star},
   128  					From: []SelectFrom{SelectFromTable{Table: "Foo"}},
   129  					Where: ComparisonOp{
   130  						Op:  Eq,
   131  						LHS: Func{Name: "CAST", Args: []Expr{TypedExpr{Expr: ID("Bar"), Type: Type{Base: String}}}},
   132  						RHS: StringLiteral("Bar"),
   133  					},
   134  				},
   135  			},
   136  		},
   137  		{`SELECT SUM(PointsScored) AS total_points, FirstName, LastName AS surname FROM PlayerStats GROUP BY FirstName, LastName`,
   138  			Query{
   139  				Select: Select{
   140  					List: []Expr{
   141  						Func{Name: "SUM", Args: []Expr{ID("PointsScored")}},
   142  						ID("FirstName"),
   143  						ID("LastName"),
   144  					},
   145  					From:        []SelectFrom{SelectFromTable{Table: "PlayerStats"}},
   146  					GroupBy:     []Expr{ID("FirstName"), ID("LastName")},
   147  					ListAliases: []ID{"total_points", "", "surname"},
   148  				},
   149  			},
   150  		},
   151  		// https://github.com/googleapis/google-cloud-go/issues/1973
   152  		{`SELECT COUNT(*) AS count FROM Lists AS l WHERE l.user_id=@userID`,
   153  			Query{
   154  				Select: Select{
   155  					List: []Expr{
   156  						Func{Name: "COUNT", Args: []Expr{Star}},
   157  					},
   158  					From: []SelectFrom{SelectFromTable{Table: "Lists", Alias: "l"}},
   159  					Where: ComparisonOp{
   160  						Op:  Eq,
   161  						LHS: PathExp{"l", "user_id"},
   162  						RHS: Param("userID"),
   163  					},
   164  					ListAliases: []ID{"count"},
   165  				},
   166  			},
   167  		},
   168  		// with single table hint
   169  		{`SELECT * FROM Packages@{FORCE_INDEX=PackagesIdx} WHERE package_idx=@packageIdx`,
   170  			Query{
   171  				Select: Select{
   172  					List: []Expr{Star},
   173  					From: []SelectFrom{SelectFromTable{Table: "Packages", Hints: map[string]string{"FORCE_INDEX": "PackagesIdx"}}},
   174  					Where: ComparisonOp{
   175  						Op:  Eq,
   176  						LHS: ID("package_idx"),
   177  						RHS: Param("packageIdx"),
   178  					},
   179  				},
   180  			},
   181  		},
   182  		// with multiple table hints
   183  		{`SELECT * FROM Packages@{ FORCE_INDEX=PackagesIdx, GROUPBY_SCAN_OPTIMIZATION=TRUE } WHERE package_idx=@packageIdx`,
   184  			Query{
   185  				Select: Select{
   186  					List: []Expr{Star},
   187  					From: []SelectFrom{SelectFromTable{Table: "Packages", Hints: map[string]string{"FORCE_INDEX": "PackagesIdx", "GROUPBY_SCAN_OPTIMIZATION": "TRUE"}}},
   188  					Where: ComparisonOp{
   189  						Op:  Eq,
   190  						LHS: ID("package_idx"),
   191  						RHS: Param("packageIdx"),
   192  					},
   193  				},
   194  			},
   195  		},
   196  		{`SELECT * FROM A INNER JOIN B ON A.w = B.y`,
   197  			Query{
   198  				Select: Select{
   199  					List: []Expr{Star},
   200  					From: []SelectFrom{SelectFromJoin{
   201  						Type: InnerJoin,
   202  						LHS:  SelectFromTable{Table: "A"},
   203  						RHS:  SelectFromTable{Table: "B"},
   204  						On: ComparisonOp{
   205  							Op:  Eq,
   206  							LHS: PathExp{"A", "w"},
   207  							RHS: PathExp{"B", "y"},
   208  						},
   209  					}},
   210  				},
   211  			},
   212  		},
   213  		{`SELECT * FROM A INNER JOIN B USING (x)`,
   214  			Query{
   215  				Select: Select{
   216  					List: []Expr{Star},
   217  					From: []SelectFrom{SelectFromJoin{
   218  						Type:  InnerJoin,
   219  						LHS:   SelectFromTable{Table: "A"},
   220  						RHS:   SelectFromTable{Table: "B"},
   221  						Using: []ID{"x"},
   222  					}},
   223  				},
   224  			},
   225  		},
   226  		{`SELECT Roster . LastName, TeamMascot.Mascot FROM Roster JOIN TeamMascot ON Roster.SchoolID = TeamMascot.SchoolID`,
   227  			Query{
   228  				Select: Select{
   229  					List: []Expr{
   230  						PathExp{"Roster", "LastName"},
   231  						PathExp{"TeamMascot", "Mascot"},
   232  					},
   233  					From: []SelectFrom{SelectFromJoin{
   234  						Type: InnerJoin,
   235  						LHS:  SelectFromTable{Table: "Roster"},
   236  						RHS:  SelectFromTable{Table: "TeamMascot"},
   237  						On: ComparisonOp{
   238  							Op:  Eq,
   239  							LHS: PathExp{"Roster", "SchoolID"},
   240  							RHS: PathExp{"TeamMascot", "SchoolID"},
   241  						},
   242  					}},
   243  				},
   244  			},
   245  		},
   246  		// Joins with hints.
   247  		{`SELECT * FROM A HASH JOIN B USING (x)`,
   248  			Query{
   249  				Select: Select{
   250  					List: []Expr{Star},
   251  					From: []SelectFrom{SelectFromJoin{
   252  						Type:  InnerJoin,
   253  						LHS:   SelectFromTable{Table: "A"},
   254  						RHS:   SelectFromTable{Table: "B"},
   255  						Using: []ID{"x"},
   256  						Hints: map[string]string{"JOIN_METHOD": "HASH_JOIN"},
   257  					}},
   258  				},
   259  			},
   260  		},
   261  		{`SELECT * FROM A JOIN @{ JOIN_METHOD=HASH_JOIN } B USING (x)`,
   262  			Query{
   263  				Select: Select{
   264  					List: []Expr{Star},
   265  					From: []SelectFrom{SelectFromJoin{
   266  						Type:  InnerJoin,
   267  						LHS:   SelectFromTable{Table: "A"},
   268  						RHS:   SelectFromTable{Table: "B"},
   269  						Using: []ID{"x"},
   270  						Hints: map[string]string{"JOIN_METHOD": "HASH_JOIN"},
   271  					}},
   272  				},
   273  			},
   274  		},
   275  		{`SELECT * FROM UNNEST ([1, 2, 3]) AS data`,
   276  			Query{
   277  				Select: Select{
   278  					List: []Expr{Star},
   279  					From: []SelectFrom{SelectFromUnnest{
   280  						Expr: Array{
   281  							IntegerLiteral(1),
   282  							IntegerLiteral(2),
   283  							IntegerLiteral(3),
   284  						},
   285  						Alias: ID("data"),
   286  					}},
   287  				},
   288  			},
   289  		},
   290  	}
   291  	for _, test := range tests {
   292  		got, err := ParseQuery(test.in)
   293  		if err != nil {
   294  			t.Errorf("ParseQuery(%q): %v", test.in, err)
   295  			continue
   296  		}
   297  		if !reflect.DeepEqual(got, test.want) {
   298  			t.Errorf("ParseQuery(%q) incorrect.\n got %#v\nwant %#v", test.in, got, test.want)
   299  		}
   300  	}
   301  }
   302  
   303  func TestParseDMLStmt(t *testing.T) {
   304  	tests := []struct {
   305  		in   string
   306  		want DMLStmt
   307  	}{
   308  		{"INSERT Singers (SingerId, FirstName, LastName) VALUES (1, 'Marc', 'Richards')",
   309  			&Insert{
   310  				Table:   "Singers",
   311  				Columns: []ID{ID("SingerId"), ID("FirstName"), ID("LastName")},
   312  				Input:   Values{{IntegerLiteral(1), StringLiteral("Marc"), StringLiteral("Richards")}},
   313  			},
   314  		},
   315  		{"INSERT INTO Singers (SingerId, FirstName, LastName) VALUES (1, 'Marc', 'Richards')",
   316  			&Insert{
   317  				Table:   "Singers",
   318  				Columns: []ID{ID("SingerId"), ID("FirstName"), ID("LastName")},
   319  				Input:   Values{{IntegerLiteral(1), StringLiteral("Marc"), StringLiteral("Richards")}},
   320  			},
   321  		},
   322  		{"INSERT Singers (SingerId, FirstName, LastName) SELECT * FROM UNNEST ([1, 2, 3]) AS data",
   323  			&Insert{
   324  				Table:   "Singers",
   325  				Columns: []ID{ID("SingerId"), ID("FirstName"), ID("LastName")},
   326  				Input: Select{
   327  					List: []Expr{Star},
   328  					From: []SelectFrom{SelectFromUnnest{
   329  						Expr: Array{
   330  							IntegerLiteral(1),
   331  							IntegerLiteral(2),
   332  							IntegerLiteral(3),
   333  						},
   334  						Alias: ID("data"),
   335  					}},
   336  				},
   337  			},
   338  		},
   339  	}
   340  	for _, test := range tests {
   341  		got, err := ParseDMLStmt(test.in)
   342  		if err != nil {
   343  			t.Errorf("ParseDMLStmt(%q): %v", test.in, err)
   344  			continue
   345  		}
   346  		if !reflect.DeepEqual(got, test.want) {
   347  			t.Errorf("ParseDMLStmt(%q) incorrect.\n got %#v\nwant %#v", test.in, got, test.want)
   348  		}
   349  	}
   350  }
   351  
   352  func TestParseExpr(t *testing.T) {
   353  	tests := []struct {
   354  		in   string
   355  		want Expr
   356  	}{
   357  		{`17`, IntegerLiteral(17)},
   358  		{`-1`, IntegerLiteral(-1)},
   359  		{fmt.Sprintf(`%d`, math.MaxInt64), IntegerLiteral(math.MaxInt64)},
   360  		{fmt.Sprintf(`%d`, math.MinInt64), IntegerLiteral(math.MinInt64)},
   361  		{"1.797693134862315708145274237317043567981e+308", FloatLiteral(math.MaxFloat64)},
   362  		{`4.940656458412465441765687928682213723651e-324`, FloatLiteral(math.SmallestNonzeroFloat64)},
   363  		{`0xf00d`, IntegerLiteral(0xf00d)},
   364  		{`-0xbeef`, IntegerLiteral(-0xbeef)},
   365  		{`0XabCD`, IntegerLiteral(0xabcd)},
   366  		{`-0XBEEF`, IntegerLiteral(-0xbeef)},
   367  		{`123.456e-67`, FloatLiteral(123.456e-67)},
   368  		{`-123.456e-67`, FloatLiteral(-123.456e-67)},
   369  		{`.1E4`, FloatLiteral(0.1e4)},
   370  		{`58.`, FloatLiteral(58)},
   371  		{`4e2`, FloatLiteral(4e2)},
   372  		{`X + Y * Z`, ArithOp{LHS: ID("X"), Op: Add, RHS: ArithOp{LHS: ID("Y"), Op: Mul, RHS: ID("Z")}}},
   373  		{`X + Y + Z`, ArithOp{LHS: ArithOp{LHS: ID("X"), Op: Add, RHS: ID("Y")}, Op: Add, RHS: ID("Z")}},
   374  		{`+X * -Y`, ArithOp{LHS: ArithOp{Op: Plus, RHS: ID("X")}, Op: Mul, RHS: ArithOp{Op: Neg, RHS: ID("Y")}}},
   375  		// Don't require space around +/- operators.
   376  		{`ID+100`, ArithOp{LHS: ID("ID"), Op: Add, RHS: IntegerLiteral(100)}},
   377  		{`ID-100`, ArithOp{LHS: ID("ID"), Op: Sub, RHS: IntegerLiteral(100)}},
   378  		{`ID&0x3fff`, ArithOp{LHS: ID("ID"), Op: BitAnd, RHS: IntegerLiteral(0x3fff)}},
   379  		{`SHA1("Hello" || " " || "World")`, Func{Name: "SHA1", Args: []Expr{ArithOp{LHS: ArithOp{LHS: StringLiteral("Hello"), Op: Concat, RHS: StringLiteral(" ")}, Op: Concat, RHS: StringLiteral("World")}}}},
   380  		{`Count > 0`, ComparisonOp{LHS: ID("Count"), Op: Gt, RHS: IntegerLiteral(0)}},
   381  		{`Name LIKE "Eve %"`, ComparisonOp{LHS: ID("Name"), Op: Like, RHS: StringLiteral("Eve %")}},
   382  		{`Speech NOT LIKE "_oo"`, ComparisonOp{LHS: ID("Speech"), Op: NotLike, RHS: StringLiteral("_oo")}},
   383  		{`A AND NOT B`, LogicalOp{LHS: ID("A"), Op: And, RHS: LogicalOp{Op: Not, RHS: ID("B")}}},
   384  		{`X BETWEEN Y AND Z`, ComparisonOp{LHS: ID("X"), Op: Between, RHS: ID("Y"), RHS2: ID("Z")}},
   385  		{`@needle IN UNNEST(@haystack)`, InOp{LHS: Param("needle"), RHS: []Expr{Param("haystack")}, Unnest: true}},
   386  		{`@needle NOT IN UNNEST(@haystack)`, InOp{LHS: Param("needle"), Neg: true, RHS: []Expr{Param("haystack")}, Unnest: true}},
   387  
   388  		// Functions
   389  		{`STARTS_WITH(Bar, 'B')`, Func{Name: "STARTS_WITH", Args: []Expr{ID("Bar"), StringLiteral("B")}}},
   390  		{`CAST(Bar AS STRING)`, Func{Name: "CAST", Args: []Expr{TypedExpr{Expr: ID("Bar"), Type: Type{Base: String}}}}},
   391  		{`SAFE_CAST(Bar AS INT64)`, Func{Name: "SAFE_CAST", Args: []Expr{TypedExpr{Expr: ID("Bar"), Type: Type{Base: Int64}}}}},
   392  		{`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}}}}}},
   393  		{`EXTRACT(DAY FROM DATE)`, Func{Name: "EXTRACT", Args: []Expr{ExtractExpr{Part: "DAY", Expr: ID("DATE"), Type: Type{Base: Int64}}}}},
   394  
   395  		// Conditional expressions
   396  		{`CASE X WHEN 1 THEN "X" WHEN 2 THEN "Y" ELSE NULL END`,
   397  			Case{
   398  				Expr: ID("X"),
   399  				WhenClauses: []WhenClause{
   400  					{Cond: IntegerLiteral(1), Result: StringLiteral("X")},
   401  					{Cond: IntegerLiteral(2), Result: StringLiteral("Y")},
   402  				},
   403  				ElseResult: Null,
   404  			},
   405  		},
   406  		{`CASE WHEN TRUE THEN "X" WHEN FALSE THEN "Y" END`,
   407  			Case{
   408  				WhenClauses: []WhenClause{
   409  					{Cond: True, Result: StringLiteral("X")},
   410  					{Cond: False, Result: StringLiteral("Y")},
   411  				},
   412  			},
   413  		},
   414  
   415  		// String literal:
   416  		// Accept double quote and single quote.
   417  		{`"hello"`, StringLiteral("hello")},
   418  		{`'hello'`, StringLiteral("hello")},
   419  		// Accept triple-quote.
   420  		{`""" "hello" "world" """`, StringLiteral(` "hello" "world" `)},
   421  		{"''' 'hello'\n'world' '''", StringLiteral(" 'hello'\n'world' ")},
   422  		// Simple escape sequence
   423  		{`"\a\b\f\n\r\t\v\\\?\"\'"`, StringLiteral("\a\b\f\n\r\t\v\\?\"'")},
   424  		{`'\a\b\f\n\r\t\v\\\?\"\''`, StringLiteral("\a\b\f\n\r\t\v\\?\"'")},
   425  		{"'\\`'", StringLiteral("`")},
   426  		// Hex and unicode escape sequence
   427  		{`"\060\x30\X30\u0030\U00000030"`, StringLiteral("00000")},
   428  		{`'\060\x30\X30\u0030\U00000030'`, StringLiteral("00000")},
   429  		{`"\uBEAF\ubeaf"`, StringLiteral("\ubeaf\ubeaf")},
   430  		{`'\uBEAF\ubeaf'`, StringLiteral("\ubeaf\ubeaf")},
   431  		// Escape sequence in triple quote is allowed.
   432  		{`"""\u0030"""`, StringLiteral("0")},
   433  		{`'''\u0030'''`, StringLiteral("0")},
   434  		// Raw string literal
   435  		{`R"\\"`, StringLiteral("\\\\")},
   436  		{`R'\\'`, StringLiteral("\\\\")},
   437  		{`r"\\"`, StringLiteral("\\\\")},
   438  		{`r'\\'`, StringLiteral("\\\\")},
   439  		{`R"\\\""`, StringLiteral("\\\\\\\"")},
   440  		{`R"""\\//\\//"""`, StringLiteral("\\\\//\\\\//")},
   441  		{"R'''\\\\//\n\\\\//'''", StringLiteral("\\\\//\n\\\\//")},
   442  
   443  		// Bytes literal:
   444  		{`B"hello"`, BytesLiteral("hello")},
   445  		{`B'hello'`, BytesLiteral("hello")},
   446  		{`b"hello"`, BytesLiteral("hello")},
   447  		{`b'hello'`, BytesLiteral("hello")},
   448  		{`B""" "hello" "world" """`, BytesLiteral(` "hello" "world" `)},
   449  		{`B''' 'hello' 'world' '''`, BytesLiteral(` 'hello' 'world' `)},
   450  		{`B"\a\b\f\n\r\t\v\\\?\"\'"`, BytesLiteral("\a\b\f\n\r\t\v\\?\"'")},
   451  		{`B'\a\b\f\n\r\t\v\\\?\"\''`, BytesLiteral("\a\b\f\n\r\t\v\\?\"'")},
   452  		{"B'''\n'''", BytesLiteral("\n")},
   453  		{`br"\\"`, BytesLiteral("\\\\")},
   454  		{`br'\\'`, BytesLiteral("\\\\")},
   455  		{`rb"\\"`, BytesLiteral("\\\\")},
   456  		{`rb'\\'`, BytesLiteral("\\\\")},
   457  		{`RB"\\"`, BytesLiteral("\\\\")},
   458  		{`RB'\\'`, BytesLiteral("\\\\")},
   459  		{`BR"\\"`, BytesLiteral("\\\\")},
   460  		{`BR'\\'`, BytesLiteral("\\\\")},
   461  		{`RB"""\\//\\//"""`, BytesLiteral("\\\\//\\\\//")},
   462  		{"RB'''\\\\//\n\\\\//'''", BytesLiteral("\\\\//\n\\\\//")},
   463  
   464  		// Date and timestamp literals:
   465  		{`DATE '2014-09-27'`, DateLiteral(civil.Date{Year: 2014, Month: time.September, Day: 27})},
   466  		{`TIMESTAMP '2014-09-27 12:30:00'`, TimestampLiteral(timef(t, "2006-01-02 15:04:05", "2014-09-27 12:30:00"))},
   467  
   468  		// date and timestamp funclit
   469  		{`DATE('2014-09-27')`, Func{Name: "DATE", Args: []Expr{StringLiteral("2014-09-27")}}},
   470  		{`TIMESTAMP('2014-09-27 12:30:00')`, Func{Name: "TIMESTAMP", Args: []Expr{StringLiteral("2014-09-27 12:30:00")}}},
   471  		// date and timestamp identifier
   472  		{`DATE = '2014-09-27'`, ComparisonOp{LHS: ID("DATE"), Op: Eq, RHS: StringLiteral("2014-09-27")}},
   473  		{`TIMESTAMP = '2014-09-27 12:30:00'`, ComparisonOp{LHS: ID("TIMESTAMP"), Op: Eq, RHS: StringLiteral("2014-09-27 12:30:00")}},
   474  		// Array literals:
   475  		// https://cloud.google.com/spanner/docs/lexical#array_literals
   476  		{`[1, 2, 3]`, Array{IntegerLiteral(1), IntegerLiteral(2), IntegerLiteral(3)}},
   477  		{`['x', 'y', 'xy']`, Array{StringLiteral("x"), StringLiteral("y"), StringLiteral("xy")}},
   478  		{`ARRAY[1, 2, 3]`, Array{IntegerLiteral(1), IntegerLiteral(2), IntegerLiteral(3)}},
   479  		// JSON literals:
   480  		// https://cloud.google.com/spanner/docs/reference/standard-sql/lexical#json_literals
   481  		{`JSON '{"a": 1}'`, JSONLiteral(`{"a": 1}`)},
   482  
   483  		// OR is lower precedence than AND.
   484  		{`A AND B OR C`, LogicalOp{LHS: LogicalOp{LHS: ID("A"), Op: And, RHS: ID("B")}, Op: Or, RHS: ID("C")}},
   485  		{`A OR B AND C`, LogicalOp{LHS: ID("A"), Op: Or, RHS: LogicalOp{LHS: ID("B"), Op: And, RHS: ID("C")}}},
   486  		// Parens to override normal precedence.
   487  		{`A OR (B AND C)`, LogicalOp{LHS: ID("A"), Op: Or, RHS: Paren{Expr: LogicalOp{LHS: ID("B"), Op: And, RHS: ID("C")}}}},
   488  
   489  		// This is the same as the WHERE clause from the test in ParseQuery.
   490  		{`Age < @ageLimit AND Alias IS NOT NULL`,
   491  			LogicalOp{
   492  				LHS: ComparisonOp{LHS: ID("Age"), Op: Lt, RHS: Param("ageLimit")},
   493  				Op:  And,
   494  				RHS: IsOp{LHS: ID("Alias"), Neg: true, RHS: Null},
   495  			},
   496  		},
   497  
   498  		// This used to be broken because the lexer didn't reset the token type.
   499  		{`C < "whelp" AND D IS NOT NULL`,
   500  			LogicalOp{
   501  				LHS: ComparisonOp{LHS: ID("C"), Op: Lt, RHS: StringLiteral("whelp")},
   502  				Op:  And,
   503  				RHS: IsOp{LHS: ID("D"), Neg: true, RHS: Null},
   504  			},
   505  		},
   506  
   507  		// Reserved keywords.
   508  		{`TRUE AND FALSE`, LogicalOp{LHS: True, Op: And, RHS: False}},
   509  		{`NULL`, Null},
   510  	}
   511  	for _, test := range tests {
   512  		p := newParser("test-file", test.in)
   513  		got, err := p.parseExpr()
   514  		if err != nil {
   515  			t.Errorf("[%s]: %v", test.in, err)
   516  			continue
   517  		}
   518  		if !reflect.DeepEqual(got, test.want) {
   519  			t.Errorf("[%s]: incorrect parse\n got <%T> %#v\nwant <%T> %#v", test.in, got, got, test.want, test.want)
   520  		}
   521  		if rem := p.Rem(); rem != "" {
   522  			t.Errorf("[%s]: Unparsed [%s]", test.in, rem)
   523  		}
   524  	}
   525  }
   526  
   527  func TestParseDDL(t *testing.T) {
   528  	line := func(n int) Position { return Position{Line: n} }
   529  	tests := []struct {
   530  		in   string
   531  		want *DDL
   532  	}{
   533  		{`CREATE TABLE FooBar (
   534  			System STRING(MAX) NOT NULL,  # This is a comment.
   535  			RepoPath STRING(MAX) NOT NULL,  -- This is another comment.
   536  			Count INT64, /* This is a
   537  						  * multiline comment. */
   538  			UpdatedAt TIMESTAMP OPTIONS (allow_commit_timestamp = true),
   539  		) PRIMARY KEY(System, RepoPath);
   540  		CREATE UNIQUE INDEX MyFirstIndex ON FooBar (
   541  			Count DESC
   542  		) STORING (Count), INTERLEAVE IN SomeTable;
   543  		CREATE TABLE FooBarAux (
   544  			System STRING(MAX) NOT NULL,
   545  			CONSTRAINT Con1 FOREIGN KEY (System) REFERENCES FooBar (System),
   546  			RepoPath STRING(MAX) NOT NULL,
   547  			FOREIGN KEY (System, RepoPath) REFERENCES Stranger (Sys, RPath), -- unnamed foreign key
   548  			Author STRING(MAX) NOT NULL,
   549  			CONSTRAINT BOOL,  -- not a constraint
   550  			CONSTRAINT Con4 CHECK (System != ""),
   551  			CHECK (RepoPath != ""),
   552  		) PRIMARY KEY(System, RepoPath, Author),
   553  		  INTERLEAVE IN PARENT FooBar ON DELETE CASCADE;
   554  
   555  		ALTER TABLE FooBar ADD COLUMN TZ BYTES(20);
   556  		ALTER TABLE FooBar DROP COLUMN TZ;
   557  		ALTER TABLE FooBar ADD CONSTRAINT Con2 FOREIGN KEY (RepoPath) REFERENCES Repos (RPath);
   558  		ALTER TABLE FooBar DROP CONSTRAINT Con3;
   559  		ALTER TABLE FooBar SET ON DELETE NO ACTION;
   560  		ALTER TABLE FooBar ALTER COLUMN Author STRING(MAX) NOT NULL;
   561  
   562  		DROP INDEX MyFirstIndex;
   563  		DROP TABLE FooBar;
   564  
   565  		-- This table has some commentary
   566  		-- that spans multiple lines.
   567  		CREATE TABLE NonScalars (
   568  			Dummy INT64 NOT NULL, -- dummy comment
   569  			Ids ARRAY<INT64>, -- comment on ids
   570  			-- leading multi comment immediately after inline comment
   571  			BCol BOOL,
   572  			Names ARRAY<STRING(MAX)>,
   573  		) PRIMARY KEY (Dummy);
   574  
   575  		-- Table with generated column.
   576  		CREATE TABLE GenCol (
   577  			Name STRING(MAX) NOT NULL,
   578  			NameLen INT64 AS (char_length(Name)) STORED,
   579  		) PRIMARY KEY (Name);
   580  
   581  		-- Table with row deletion policy.
   582  		CREATE TABLE WithRowDeletionPolicy (
   583  			Name STRING(MAX) NOT NULL,
   584  			DelTimestamp TIMESTAMP NOT NULL,
   585  		) PRIMARY KEY (Name)
   586  		, ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY ));
   587  
   588  		ALTER TABLE WithRowDeletionPolicy DROP ROW DELETION POLICY;
   589  		ALTER TABLE WithRowDeletionPolicy ADD ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY ));
   590  		ALTER TABLE WithRowDeletionPolicy REPLACE ROW DELETION POLICY ( OLDER_THAN ( DelTimestamp, INTERVAL 30 DAY ));
   591  
   592  		CREATE VIEW SingersView
   593  		SQL SECURITY INVOKER
   594  		AS SELECT SingerId, FullName
   595  		FROM Singers
   596  		ORDER BY LastName, FirstName;
   597  
   598  		CREATE TABLE users (
   599  		  user_id      STRING(36) NOT NULL,
   600  		  some_string  STRING(16) NOT NULL,
   601  		  some_time TIMESTAMP NOT NULL,
   602  		  number_key   INT64 AS (SAFE_CAST(SUBSTR(some_string, 2) AS INT64)) STORED,
   603  		  generated_date DATE AS (EXTRACT(DATE FROM some_time AT TIME ZONE "CET")) STORED,
   604  		  shard_id  INT64 AS (MOD(FARM_FINGERPRINT(user_id), 19)) STORED,
   605  		) PRIMARY KEY(user_id);
   606  
   607  		-- Table has a column with a default value.
   608  		CREATE TABLE DefaultCol (
   609  			Name STRING(MAX) NOT NULL,
   610  			Age INT64 DEFAULT (0),
   611  		) PRIMARY KEY (Name);
   612  
   613  		ALTER TABLE DefaultCol ALTER COLUMN Age DROP DEFAULT;
   614  		ALTER TABLE DefaultCol ALTER COLUMN Age SET DEFAULT (0);
   615  		ALTER TABLE DefaultCol ALTER COLUMN Age STRING(MAX) DEFAULT ("0");
   616  
   617  		-- Trailing comment at end of file.
   618  		`, &DDL{Filename: "filename", List: []DDLStmt{
   619  			&CreateTable{
   620  				Name: "FooBar",
   621  				Columns: []ColumnDef{
   622  					{Name: "System", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(2)},
   623  					{Name: "RepoPath", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(3)},
   624  					{Name: "Count", Type: Type{Base: Int64}, Position: line(4)},
   625  					{Name: "UpdatedAt", Type: Type{Base: Timestamp}, Options: ColumnOptions{AllowCommitTimestamp: boolAddr(true)}, Position: line(6)},
   626  				},
   627  				PrimaryKey: []KeyPart{
   628  					{Column: "System"},
   629  					{Column: "RepoPath"},
   630  				},
   631  				Position: line(1),
   632  			},
   633  			&CreateIndex{
   634  				Name:       "MyFirstIndex",
   635  				Table:      "FooBar",
   636  				Columns:    []KeyPart{{Column: "Count", Desc: true}},
   637  				Unique:     true,
   638  				Storing:    []ID{"Count"},
   639  				Interleave: "SomeTable",
   640  				Position:   line(8),
   641  			},
   642  			&CreateTable{
   643  				Name: "FooBarAux",
   644  				Columns: []ColumnDef{
   645  					{Name: "System", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(12)},
   646  					{Name: "RepoPath", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(14)},
   647  					{Name: "Author", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(16)},
   648  					{Name: "CONSTRAINT", Type: Type{Base: Bool}, Position: line(17)},
   649  				},
   650  				Constraints: []TableConstraint{
   651  					{
   652  						Name: "Con1",
   653  						Constraint: ForeignKey{
   654  							Columns:    []ID{"System"},
   655  							RefTable:   "FooBar",
   656  							RefColumns: []ID{"System"},
   657  							Position:   line(13),
   658  						},
   659  						Position: line(13),
   660  					},
   661  					{
   662  						Constraint: ForeignKey{
   663  							Columns:    []ID{"System", "RepoPath"},
   664  							RefTable:   "Stranger",
   665  							RefColumns: []ID{"Sys", "RPath"},
   666  							Position:   line(15),
   667  						},
   668  						Position: line(15),
   669  					},
   670  					{
   671  						Name: "Con4",
   672  						Constraint: Check{
   673  							Expr:     ComparisonOp{LHS: ID("System"), Op: Ne, RHS: StringLiteral("")},
   674  							Position: line(18),
   675  						},
   676  						Position: line(18),
   677  					},
   678  					{
   679  						Constraint: Check{
   680  							Expr:     ComparisonOp{LHS: ID("RepoPath"), Op: Ne, RHS: StringLiteral("")},
   681  							Position: line(19),
   682  						},
   683  						Position: line(19),
   684  					},
   685  				},
   686  				PrimaryKey: []KeyPart{
   687  					{Column: "System"},
   688  					{Column: "RepoPath"},
   689  					{Column: "Author"},
   690  				},
   691  				Interleave: &Interleave{
   692  					Parent:   "FooBar",
   693  					OnDelete: CascadeOnDelete,
   694  				},
   695  				Position: line(11),
   696  			},
   697  			&AlterTable{
   698  				Name:       "FooBar",
   699  				Alteration: AddColumn{Def: ColumnDef{Name: "TZ", Type: Type{Base: Bytes, Len: 20}, Position: line(23)}},
   700  				Position:   line(23),
   701  			},
   702  			&AlterTable{
   703  				Name:       "FooBar",
   704  				Alteration: DropColumn{Name: "TZ"},
   705  				Position:   line(24),
   706  			},
   707  			&AlterTable{
   708  				Name: "FooBar",
   709  				Alteration: AddConstraint{Constraint: TableConstraint{
   710  					Name: "Con2",
   711  					Constraint: ForeignKey{
   712  						Columns:    []ID{"RepoPath"},
   713  						RefTable:   "Repos",
   714  						RefColumns: []ID{"RPath"},
   715  						Position:   line(25),
   716  					},
   717  					Position: line(25),
   718  				}},
   719  				Position: line(25),
   720  			},
   721  			&AlterTable{
   722  				Name:       "FooBar",
   723  				Alteration: DropConstraint{Name: "Con3"},
   724  				Position:   line(26),
   725  			},
   726  			&AlterTable{
   727  				Name:       "FooBar",
   728  				Alteration: SetOnDelete{Action: NoActionOnDelete},
   729  				Position:   line(27),
   730  			},
   731  			&AlterTable{
   732  				Name: "FooBar",
   733  				Alteration: AlterColumn{
   734  					Name: "Author",
   735  					Alteration: SetColumnType{
   736  						Type:    Type{Base: String, Len: MaxLen},
   737  						NotNull: true,
   738  					},
   739  				},
   740  				Position: line(28),
   741  			},
   742  			&DropIndex{Name: "MyFirstIndex", Position: line(30)},
   743  			&DropTable{Name: "FooBar", Position: line(31)},
   744  			&CreateTable{
   745  				Name: "NonScalars",
   746  				Columns: []ColumnDef{
   747  					{Name: "Dummy", Type: Type{Base: Int64}, NotNull: true, Position: line(36)},
   748  					{Name: "Ids", Type: Type{Array: true, Base: Int64}, Position: line(37)},
   749  					{Name: "BCol", Type: Type{Base: Bool}, Position: line(39)},
   750  					{Name: "Names", Type: Type{Array: true, Base: String, Len: MaxLen}, Position: line(40)},
   751  				},
   752  				PrimaryKey: []KeyPart{{Column: "Dummy"}},
   753  				Position:   line(35),
   754  			},
   755  			&CreateTable{
   756  				Name: "GenCol",
   757  				Columns: []ColumnDef{
   758  					{Name: "Name", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(45)},
   759  					{
   760  						Name: "NameLen", Type: Type{Base: Int64},
   761  						Generated: Func{Name: "CHAR_LENGTH", Args: []Expr{ID("Name")}},
   762  						Position:  line(46),
   763  					},
   764  				},
   765  				PrimaryKey: []KeyPart{{Column: "Name"}},
   766  				Position:   line(44),
   767  			},
   768  			&CreateTable{
   769  				Name: "WithRowDeletionPolicy",
   770  				Columns: []ColumnDef{
   771  					{Name: "Name", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(51)},
   772  					{Name: "DelTimestamp", Type: Type{Base: Timestamp}, NotNull: true, Position: line(52)},
   773  				},
   774  				PrimaryKey: []KeyPart{{Column: "Name"}},
   775  				RowDeletionPolicy: &RowDeletionPolicy{
   776  					Column:  ID("DelTimestamp"),
   777  					NumDays: 30,
   778  				},
   779  				Position: line(50),
   780  			},
   781  			&AlterTable{
   782  				Name:       "WithRowDeletionPolicy",
   783  				Alteration: DropRowDeletionPolicy{},
   784  				Position:   line(56),
   785  			},
   786  			&AlterTable{
   787  				Name: "WithRowDeletionPolicy",
   788  				Alteration: AddRowDeletionPolicy{
   789  					RowDeletionPolicy: RowDeletionPolicy{
   790  						Column:  ID("DelTimestamp"),
   791  						NumDays: 30,
   792  					},
   793  				},
   794  				Position: line(57),
   795  			},
   796  			&AlterTable{
   797  				Name: "WithRowDeletionPolicy",
   798  				Alteration: ReplaceRowDeletionPolicy{
   799  					RowDeletionPolicy: RowDeletionPolicy{
   800  						Column:  ID("DelTimestamp"),
   801  						NumDays: 30,
   802  					},
   803  				},
   804  				Position: line(58),
   805  			},
   806  			&CreateView{
   807  				Name:      "SingersView",
   808  				OrReplace: false,
   809  				Query: Query{
   810  					Select: Select{
   811  						List: []Expr{ID("SingerId"), ID("FullName")},
   812  						From: []SelectFrom{SelectFromTable{
   813  							Table: "Singers",
   814  						}},
   815  					},
   816  					Order: []Order{
   817  						{Expr: ID("LastName")},
   818  						{Expr: ID("FirstName")},
   819  					},
   820  				},
   821  				Position: line(60),
   822  			},
   823  
   824  			//	CREATE TABLE users (
   825  			//	user_id      STRING(36) NOT NULL,
   826  			//	some_string  STRING(16) NOT NULL,
   827  			//	number_key   INT64 AS (SAFE_CAST(SUBSTR(some_string, 2) AS INT64)) STORED,
   828  			//) PRIMARY KEY(user_id);
   829  			&CreateTable{
   830  				Name: "users",
   831  				Columns: []ColumnDef{
   832  					{Name: "user_id", Type: Type{Base: String, Len: 36}, NotNull: true, Position: line(67)},
   833  					{Name: "some_string", Type: Type{Base: String, Len: 16}, NotNull: true, Position: line(68)},
   834  					{Name: "some_time", Type: Type{Base: Timestamp}, NotNull: true, Position: line(69)},
   835  					{
   836  						Name: "number_key", Type: Type{Base: Int64},
   837  						Generated: Func{Name: "SAFE_CAST", Args: []Expr{
   838  							TypedExpr{Expr: Func{Name: "SUBSTR", Args: []Expr{ID("some_string"), IntegerLiteral(2)}}, Type: Type{Base: Int64}},
   839  						}},
   840  						Position: line(70),
   841  					},
   842  					{
   843  						Name: "generated_date", Type: Type{Base: Date},
   844  						Generated: Func{Name: "EXTRACT", Args: []Expr{
   845  							ExtractExpr{Part: "DATE", Type: Type{Base: Date}, Expr: AtTimeZoneExpr{Expr: ID("some_time"), Zone: "CET", Type: Type{Base: Timestamp}}},
   846  						}},
   847  						Position: line(71),
   848  					},
   849  					{
   850  						Name: "shard_id", Type: Type{Base: Int64},
   851  						Generated: Func{Name: "MOD", Args: []Expr{
   852  							Func{Name: "FARM_FINGERPRINT", Args: []Expr{ID("user_id")}}, IntegerLiteral(19),
   853  						}},
   854  						Position: line(72),
   855  					},
   856  				},
   857  				PrimaryKey: []KeyPart{{Column: "user_id"}},
   858  				Position:   line(66),
   859  			},
   860  
   861  			&CreateTable{
   862  				Name: "DefaultCol",
   863  				Columns: []ColumnDef{
   864  					{Name: "Name", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(77)},
   865  					{
   866  						Name: "Age", Type: Type{Base: Int64},
   867  						Default:  IntegerLiteral(0),
   868  						Position: line(78),
   869  					},
   870  				},
   871  				PrimaryKey: []KeyPart{{Column: "Name"}},
   872  				Position:   line(76),
   873  			},
   874  			&AlterTable{
   875  				Name: "DefaultCol",
   876  				Alteration: AlterColumn{
   877  					Name:       "Age",
   878  					Alteration: DropDefault{},
   879  				},
   880  				Position: line(81),
   881  			},
   882  			&AlterTable{
   883  				Name: "DefaultCol",
   884  				Alteration: AlterColumn{
   885  					Name: "Age",
   886  					Alteration: SetDefault{
   887  						Default: IntegerLiteral(0),
   888  					},
   889  				},
   890  				Position: line(82),
   891  			},
   892  			&AlterTable{
   893  				Name: "DefaultCol",
   894  				Alteration: AlterColumn{
   895  					Name: "Age",
   896  					Alteration: SetColumnType{
   897  						Type:    Type{Base: String, Len: MaxLen},
   898  						Default: StringLiteral("0"),
   899  					},
   900  				},
   901  				Position: line(83),
   902  			},
   903  		}, Comments: []*Comment{
   904  			{Marker: "#", Start: line(2), End: line(2),
   905  				Text: []string{"This is a comment."}},
   906  			{Marker: "--", Start: line(3), End: line(3),
   907  				Text: []string{"This is another comment."}},
   908  			{Marker: "/*", Start: line(4), End: line(5),
   909  				Text: []string{" This is a", "\t\t\t\t\t\t  * multiline comment."}},
   910  			{Marker: "--", Start: line(15), End: line(15),
   911  				Text: []string{"unnamed foreign key"}},
   912  			{Marker: "--", Start: line(17), End: line(17),
   913  				Text: []string{"not a constraint"}},
   914  			{Marker: "--", Isolated: true, Start: line(33), End: line(34),
   915  				Text: []string{"This table has some commentary", "that spans multiple lines."}},
   916  			// These comments shouldn't get combined:
   917  			{Marker: "--", Start: line(36), End: line(36), Text: []string{"dummy comment"}},
   918  			{Marker: "--", Start: line(37), End: line(37), Text: []string{"comment on ids"}},
   919  			{Marker: "--", Isolated: true, Start: line(38), End: line(38), Text: []string{"leading multi comment immediately after inline comment"}},
   920  
   921  			{Marker: "--", Isolated: true, Start: line(43), End: line(43), Text: []string{"Table with generated column."}},
   922  			{Marker: "--", Isolated: true, Start: line(49), End: line(49), Text: []string{"Table with row deletion policy."}},
   923  			{Marker: "--", Isolated: true, Start: line(75), End: line(75), Text: []string{"Table has a column with a default value."}},
   924  
   925  			// Comment after everything else.
   926  			{Marker: "--", Isolated: true, Start: line(85), End: line(85), Text: []string{"Trailing comment at end of file."}},
   927  		}}},
   928  		// No trailing comma:
   929  		{`ALTER TABLE T ADD COLUMN C2 INT64`, &DDL{Filename: "filename", List: []DDLStmt{
   930  			&AlterTable{
   931  				Name:       "T",
   932  				Alteration: AddColumn{Def: ColumnDef{Name: "C2", Type: Type{Base: Int64}, Position: line(1)}},
   933  				Position:   line(1),
   934  			},
   935  		}}},
   936  		// Table and column names using reserved keywords.
   937  		{`CREATE TABLE ` + "`enum`" + ` (
   938  			` + "`With`" + ` STRING(MAX) NOT NULL,
   939  		) PRIMARY KEY(` + "`With`" + `);
   940  		`, &DDL{Filename: "filename", List: []DDLStmt{
   941  			&CreateTable{
   942  				Name: "enum",
   943  				Columns: []ColumnDef{
   944  					{Name: "With", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(2)},
   945  				},
   946  				PrimaryKey: []KeyPart{
   947  					{Column: "With"},
   948  				},
   949  				Position: line(1),
   950  			},
   951  		}}},
   952  		{`ALTER DATABASE dbname SET OPTIONS (optimizer_version=2, version_retention_period='7d', enable_key_visualizer=true)`,
   953  			&DDL{Filename: "filename", List: []DDLStmt{
   954  				&AlterDatabase{
   955  					Name: "dbname",
   956  					Alteration: SetDatabaseOptions{
   957  						Options: DatabaseOptions{
   958  							OptimizerVersion:       func(i int) *int { return &i }(2),
   959  							VersionRetentionPeriod: func(s string) *string { return &s }("7d"),
   960  							EnableKeyVisualizer:    func(b bool) *bool { return &b }(true),
   961  						},
   962  					},
   963  					Position: line(1),
   964  				},
   965  			},
   966  			}},
   967  		{`ALTER DATABASE dbname SET OPTIONS (optimizer_version=2, version_retention_period='7d', enable_key_visualizer=true); CREATE TABLE users (UserId STRING(MAX) NOT NULL,) PRIMARY KEY (UserId);`,
   968  			&DDL{Filename: "filename", List: []DDLStmt{
   969  				&AlterDatabase{
   970  					Name: "dbname",
   971  					Alteration: SetDatabaseOptions{
   972  						Options: DatabaseOptions{
   973  							OptimizerVersion:       func(i int) *int { return &i }(2),
   974  							VersionRetentionPeriod: func(s string) *string { return &s }("7d"),
   975  							EnableKeyVisualizer:    func(b bool) *bool { return &b }(true),
   976  						},
   977  					},
   978  					Position: line(1),
   979  				},
   980  				&CreateTable{Name: "users", Columns: []ColumnDef{
   981  					{Name: "UserId", Type: Type{Base: String, Len: MaxLen}, NotNull: true, Position: line(1)},
   982  				},
   983  					PrimaryKey: []KeyPart{
   984  						{Column: "UserId"},
   985  					},
   986  					Position: line(1),
   987  				},
   988  			},
   989  			}},
   990  		{`ALTER DATABASE dbname SET OPTIONS (optimizer_version=null, version_retention_period=null, enable_key_visualizer=null)`,
   991  			&DDL{Filename: "filename", List: []DDLStmt{
   992  				&AlterDatabase{
   993  					Name: "dbname",
   994  					Alteration: SetDatabaseOptions{
   995  						Options: DatabaseOptions{
   996  							OptimizerVersion:       func(i int) *int { return &i }(0),
   997  							VersionRetentionPeriod: func(s string) *string { return &s }(""),
   998  							EnableKeyVisualizer:    func(b bool) *bool { return &b }(false),
   999  						},
  1000  					},
  1001  					Position: line(1),
  1002  				},
  1003  			},
  1004  			}},
  1005  		{"CREATE OR REPLACE VIEW `SingersView` SQL SECURITY INVOKER AS SELECT SingerId, FullName, Picture FROM Singers ORDER BY LastName, FirstName",
  1006  			&DDL{Filename: "filename", List: []DDLStmt{
  1007  				&CreateView{
  1008  					Name:      "SingersView",
  1009  					OrReplace: true,
  1010  					Query: Query{
  1011  						Select: Select{
  1012  							List: []Expr{ID("SingerId"), ID("FullName"), ID("Picture")},
  1013  							From: []SelectFrom{SelectFromTable{
  1014  								Table: "Singers",
  1015  							}},
  1016  						},
  1017  						Order: []Order{
  1018  							{Expr: ID("LastName")},
  1019  							{Expr: ID("FirstName")},
  1020  						},
  1021  					},
  1022  					Position: line(1),
  1023  				},
  1024  			},
  1025  			}},
  1026  		{"DROP VIEW `SingersView`",
  1027  			&DDL{Filename: "filename", List: []DDLStmt{
  1028  				&DropView{
  1029  					Name:     "SingersView",
  1030  					Position: line(1),
  1031  				},
  1032  			},
  1033  			}},
  1034  		{`ALTER TABLE products ADD COLUMN item STRING(MAX) AS (JSON_VALUE(itemDetails, '$.itemDetails')) STORED`, &DDL{Filename: "filename", List: []DDLStmt{
  1035  			&AlterTable{
  1036  				Name: "products",
  1037  				Alteration: AddColumn{Def: ColumnDef{
  1038  					Name:     "item",
  1039  					Type:     Type{Base: String, Len: MaxLen},
  1040  					Position: line(1),
  1041  					Generated: Func{
  1042  						Name: "JSON_VALUE",
  1043  						Args: []Expr{ID("itemDetails"), StringLiteral("$.itemDetails")},
  1044  					},
  1045  				}},
  1046  				Position: line(1),
  1047  			},
  1048  		}}},
  1049  	}
  1050  	for _, test := range tests {
  1051  		got, err := ParseDDL("filename", test.in)
  1052  		if err != nil {
  1053  			t.Errorf("ParseDDL(%q): %v", test.in, err)
  1054  			continue
  1055  		}
  1056  		got.clearOffset()
  1057  		if !reflect.DeepEqual(got, test.want) {
  1058  			t.Errorf("ParseDDL(%q) incorrect.\n got %v\nwant %v", test.in, got, test.want)
  1059  
  1060  			// Also log the specific elements that don't match to make it easier to debug
  1061  			// especially the large DDLs.
  1062  			for i := range got.List {
  1063  				if !reflect.DeepEqual(got.List[i], test.want.List[i]) {
  1064  					t.Errorf("\tstatement %d mismatch:\n\t got %v\n\twant %v", i, got.List[i], test.want.List[i])
  1065  				}
  1066  			}
  1067  			for i := range got.Comments {
  1068  				if !reflect.DeepEqual(got.Comments[i], test.want.Comments[i]) {
  1069  					t.Errorf("\tcomment %d mismatch:\n\t got %v\n\twant %v", i, got.Comments[i], test.want.Comments[i])
  1070  				}
  1071  			}
  1072  		}
  1073  	}
  1074  
  1075  	// Check the comment discovey helpers on the first DDL.
  1076  	// Reparse it first so we get full position information.
  1077  	ddl, err := ParseDDL("filename", tests[0].in)
  1078  	if err != nil {
  1079  		t.Fatal(err)
  1080  	}
  1081  	// The CreateTable for NonScalars has a leading comment.
  1082  	com := ddl.LeadingComment(tableByName(t, ddl, "NonScalars"))
  1083  	if com == nil {
  1084  		t.Errorf("No leading comment found for NonScalars")
  1085  	} else if com.Text[0] != "This table has some commentary" {
  1086  		t.Errorf("LeadingComment returned the wrong comment for NonScalars")
  1087  	}
  1088  	// Second field of FooBar (RepoPath) has an inline comment.
  1089  	cd := tableByName(t, ddl, "FooBar").Columns[1]
  1090  	if com := ddl.InlineComment(cd); com == nil {
  1091  		t.Errorf("No inline comment found for FooBar.RepoPath")
  1092  	} else if com.Text[0] != "This is another comment." {
  1093  		t.Errorf("InlineComment returned the wrong comment (%q) for FooBar.RepoPath", com.Text[0])
  1094  	}
  1095  	// There are no leading comments on the columns of NonScalars (except for BCol),
  1096  	// even though there's often a comment on the previous line.
  1097  	for _, cd := range tableByName(t, ddl, "NonScalars").Columns {
  1098  		if cd.Name == "BCol" {
  1099  			continue
  1100  		}
  1101  		if com := ddl.LeadingComment(cd); com != nil {
  1102  			t.Errorf("Leading comment found for NonScalars.%s: %v", cd.Name, com)
  1103  		}
  1104  	}
  1105  }
  1106  
  1107  func tableByName(t *testing.T, ddl *DDL, name ID) *CreateTable {
  1108  	t.Helper()
  1109  	for _, stmt := range ddl.List {
  1110  		if ct, ok := stmt.(*CreateTable); ok && ct.Name == name {
  1111  			return ct
  1112  		}
  1113  	}
  1114  	t.Fatalf("no table with name %q", name)
  1115  	panic("unreachable")
  1116  }
  1117  
  1118  func TestParseFailures(t *testing.T) {
  1119  	expr := func(p *parser) error {
  1120  		if _, pe := p.parseExpr(); pe != nil {
  1121  			return pe
  1122  		}
  1123  		return nil
  1124  	}
  1125  	query := func(p *parser) error {
  1126  		if _, pe := p.parseQuery(); pe != nil {
  1127  			return pe
  1128  		}
  1129  		return nil
  1130  	}
  1131  
  1132  	tests := []struct {
  1133  		f    func(p *parser) error
  1134  		in   string
  1135  		desc string
  1136  	}{
  1137  		{expr, `0b337`, "binary literal"},
  1138  		{expr, `"foo\`, "unterminated string"},
  1139  		{expr, `"\i"`, "invalid escape sequence"},
  1140  		{expr, `"\0"`, "invalid escape sequence"},
  1141  		{expr, `"\099"`, "invalid escape sequence"},
  1142  		{expr, `"\400"`, "invalid escape sequence: octal digits overflow"},
  1143  		{expr, `"\x"`, "invalid escape sequence"},
  1144  		{expr, `"\xFZ"`, "invalid escape sequence"},
  1145  		{expr, `"\u"`, "invalid escape sequence"},
  1146  		{expr, `"\uFFFZ"`, "invalid escape sequence"},
  1147  		{expr, `"\uD800"`, "invalid unicode character (surrogate)"},
  1148  		{expr, `"\U"`, "invalid escape sequence"},
  1149  		{expr, `"\UFFFFFFFZ"`, "invalid escape sequence"},
  1150  		{expr, `"\U00110000"`, "invalid unicode character (out of range)"},
  1151  		{expr, "\"\n\"", "unterminated string by newline (double quote)"},
  1152  		{expr, "'\n'", "unterminated string by newline (single quote)"},
  1153  		{expr, "R\"\n\"", "unterminated raw string by newline (double quote)"},
  1154  		{expr, "R'\n'", "unterminated raw string by newline (single quote)"},
  1155  		{expr, `B"\u0030"`, "\\uXXXX sequence is not supported in bytes literal (double quote)"},
  1156  		{expr, `B'\u0030'`, "\\uXXXX sequence is not supported in bytes literal (double quote)"},
  1157  		{expr, `B"\U00000030"`, "\\UXXXXXXXX sequence is not supported in bytes literal (double quote)"},
  1158  		{expr, `B'\U00000030'`, "\\UXXXXXXXX sequence is not supported in bytes literal (double quote)"},
  1159  		{expr, `BB""`, "invalid string-like literal prefix"},
  1160  		{expr, `rr""`, "invalid string-like literal prefix"},
  1161  		{expr, `"""\"""`, "unterminated triple-quoted string by last backslash (double quote)"},
  1162  		{expr, `'''\'''`, "unterminated triple-quoted string by last backslash (single quote)"},
  1163  		{expr, `"foo" AND "bar"`, "logical operation on string literals"},
  1164  		// Found by fuzzing.
  1165  		// https://github.com/googleapis/google-cloud-go/issues/2196
  1166  		{query, `/*/*/`, "invalid comment termination"},
  1167  	}
  1168  	for _, test := range tests {
  1169  		p := newParser("f", test.in)
  1170  		err := test.f(p)
  1171  		if err == nil && p.Rem() == "" {
  1172  			t.Errorf("%s: parsing [%s] succeeded, should have failed", test.desc, test.in)
  1173  		}
  1174  	}
  1175  }
  1176  
  1177  func timef(t *testing.T, format, s string) time.Time {
  1178  	ti, err := time.ParseInLocation(format, string(s), defaultLocation)
  1179  	if err != nil {
  1180  		t.Errorf("parsing %s [%s] time.ParseInLocation failed.", s, format)
  1181  	}
  1182  	return ti
  1183  }