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 }