diff --git a/ast/ast.go b/ast/ast.go index c2cf6183..caf16acc 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -1163,7 +1163,11 @@ type Join struct { Method JoinMethod Hint *Hint // optional Left, Right TableExpr - Cond JoinCondition // nil when Op is CrossJoin, otherwise it must be set. + + // nil when Op is CrossJoin + // optional when Right is PathTableExpr or Unnest + // otherwise it must be set. + Cond JoinCondition } // On is ON condition of JOIN expression. diff --git a/parser.go b/parser.go index 68e81e4b..737dbd3f 100644 --- a/parser.go +++ b/parser.go @@ -953,18 +953,16 @@ func (p *Parser) parseTableExpr(toplevel bool) ast.TableExpr { hint := p.tryParseHint() right := p.parseSimpleTableExpr() - if op == ast.CrossJoin || op == ast.CommaJoin { - join = &ast.Join{ - Op: op, - Method: method, - Hint: hint, - Left: join, - Right: right, + var cond ast.JoinCondition + if op != ast.CrossJoin && op != ast.CommaJoin { + switch right.(type) { + case *ast.PathTableExpr, *ast.Unnest: + cond = p.tryParseJoinCondition() + default: + cond = p.parseJoinCondition() } - continue } - cond := p.parseJoinCondition() join = &ast.Join{ Op: op, Method: method, @@ -1155,6 +1153,13 @@ func (p *Parser) parseJoinCondition() ast.JoinCondition { panic(p.errorfAtToken(&p.Token, "expected token: ON, USING, but: %s", p.Token.Kind)) } +func (p *Parser) tryParseJoinCondition() ast.JoinCondition { + if p.Token.Kind != "ON" && p.Token.Kind != "USING" { + return nil + } + return p.parseJoinCondition() +} + func (p *Parser) parseOn() *ast.On { pos := p.expect("ON").Pos expr := p.parseExpr() diff --git a/testdata/input/query/select_from_inner_join_path_table_expr.sql b/testdata/input/query/select_from_inner_join_path_table_expr.sql new file mode 100644 index 00000000..c2e250cd --- /dev/null +++ b/testdata/input/query/select_from_inner_join_path_table_expr.sql @@ -0,0 +1,13 @@ +-- https://cloud.google.com/spanner/docs/reference/standard-sql/query-syntax#correlated_join +SELECT A.name, item +FROM + UNNEST( + [ + STRUCT( + 'first' AS name, + [1, 2, 3, 4] AS items), + STRUCT( + 'second' AS name, + [] AS items)]) AS A + INNER JOIN + A.items AS item diff --git a/testdata/input/query/select_from_join_unnest.sql b/testdata/input/query/select_from_join_unnest.sql new file mode 100644 index 00000000..5db9305f --- /dev/null +++ b/testdata/input/query/select_from_join_unnest.sql @@ -0,0 +1,12 @@ +-- https://cloud.google.com/spanner/docs/reference/standard-sql/query-syntax#correlated_join +SELECT * +FROM + Roster + JOIN + UNNEST( + ARRAY( + SELECT AS STRUCT * + FROM PlayerStats + WHERE PlayerStats.OpponentID = Roster.SchoolID + )) AS PlayerMatches + ON PlayerMatches.LastName = 'Buchanan' diff --git a/testdata/input/query/select_from_left_join_path_table_expr.sql b/testdata/input/query/select_from_left_join_path_table_expr.sql new file mode 100644 index 00000000..e6aab198 --- /dev/null +++ b/testdata/input/query/select_from_left_join_path_table_expr.sql @@ -0,0 +1,13 @@ +-- https://cloud.google.com/spanner/docs/reference/standard-sql/query-syntax#correlated_join +SELECT A.name, item, ARRAY_LENGTH(A.items) item_count_for_name +FROM + UNNEST( + [ + STRUCT( + 'first' AS name, + [1, 2, 3, 4] AS items), + STRUCT( + 'second' AS name, + [] AS items)]) AS A + LEFT JOIN + A.items AS item diff --git a/testdata/result/query/select_from_inner_join_path_table_expr.sql.txt b/testdata/result/query/select_from_inner_join_path_table_expr.sql.txt new file mode 100644 index 00000000..bd1a9ff0 --- /dev/null +++ b/testdata/result/query/select_from_inner_join_path_table_expr.sql.txt @@ -0,0 +1,196 @@ +--- select_from_inner_join_path_table_expr.sql +-- https://cloud.google.com/spanner/docs/reference/standard-sql/query-syntax#correlated_join +SELECT A.name, item +FROM + UNNEST( + [ + STRUCT( + 'first' AS name, + [1, 2, 3, 4] AS items), + STRUCT( + 'second' AS name, + [] AS items)]) AS A + INNER JOIN + A.items AS item + +--- AST +&ast.QueryStatement{ + Query: &ast.Select{ + Select: 93, + Results: []ast.SelectItem{ + &ast.ExprSelectItem{ + Expr: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 100, + NameEnd: 101, + Name: "A", + }, + &ast.Ident{ + NamePos: 102, + NameEnd: 106, + Name: "name", + }, + }, + }, + }, + &ast.ExprSelectItem{ + Expr: &ast.Ident{ + NamePos: 108, + NameEnd: 112, + Name: "item", + }, + }, + }, + From: &ast.From{ + From: 113, + Source: &ast.Join{ + Op: "INNER JOIN", + Left: &ast.Unnest{ + Unnest: 120, + Rparen: 268, + Expr: &ast.ArrayLiteral{ + Array: -1, + Lbrack: 132, + Rbrack: 267, + Values: []ast.Expr{ + &ast.TypelessStructLiteral{ + Struct: 140, + Rparen: 202, + Values: []ast.TypelessStructLiteralArg{ + &ast.Alias{ + Expr: &ast.StringLiteral{ + ValuePos: 156, + ValueEnd: 163, + Value: "first", + }, + As: &ast.AsAlias{ + As: 164, + Alias: &ast.Ident{ + NamePos: 167, + NameEnd: 171, + Name: "name", + }, + }, + }, + &ast.Alias{ + Expr: &ast.ArrayLiteral{ + Array: -1, + Lbrack: 181, + Rbrack: 192, + Values: []ast.Expr{ + &ast.IntLiteral{ + ValuePos: 182, + ValueEnd: 183, + Base: 10, + Value: "1", + }, + &ast.IntLiteral{ + ValuePos: 185, + ValueEnd: 186, + Base: 10, + Value: "2", + }, + &ast.IntLiteral{ + ValuePos: 188, + ValueEnd: 189, + Base: 10, + Value: "3", + }, + &ast.IntLiteral{ + ValuePos: 191, + ValueEnd: 192, + Base: 10, + Value: "4", + }, + }, + }, + As: &ast.AsAlias{ + As: 194, + Alias: &ast.Ident{ + NamePos: 197, + NameEnd: 202, + Name: "items", + }, + }, + }, + }, + }, + &ast.TypelessStructLiteral{ + Struct: 211, + Rparen: 266, + Values: []ast.TypelessStructLiteralArg{ + &ast.Alias{ + Expr: &ast.StringLiteral{ + ValuePos: 229, + ValueEnd: 237, + Value: "second", + }, + As: &ast.AsAlias{ + As: 238, + Alias: &ast.Ident{ + NamePos: 241, + NameEnd: 245, + Name: "name", + }, + }, + }, + &ast.Alias{ + Expr: &ast.ArrayLiteral{ + Array: -1, + Lbrack: 255, + Rbrack: 256, + }, + As: &ast.AsAlias{ + As: 258, + Alias: &ast.Ident{ + NamePos: 261, + NameEnd: 266, + Name: "items", + }, + }, + }, + }, + }, + }, + }, + As: &ast.AsAlias{ + As: 270, + Alias: &ast.Ident{ + NamePos: 273, + NameEnd: 274, + Name: "A", + }, + }, + }, + Right: &ast.PathTableExpr{ + Path: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 292, + NameEnd: 293, + Name: "A", + }, + &ast.Ident{ + NamePos: 294, + NameEnd: 299, + Name: "items", + }, + }, + }, + As: &ast.AsAlias{ + As: 300, + Alias: &ast.Ident{ + NamePos: 303, + NameEnd: 307, + Name: "item", + }, + }, + }, + }, + }, + }, +} + +--- SQL +SELECT A.name, item FROM UNNEST([STRUCT("first" AS name, [1, 2, 3, 4] AS items), STRUCT("second" AS name, [] AS items)]) AS A INNER JOIN A.items AS item diff --git a/testdata/result/query/select_from_join_unnest.sql.txt b/testdata/result/query/select_from_join_unnest.sql.txt new file mode 100644 index 00000000..8838628b --- /dev/null +++ b/testdata/result/query/select_from_join_unnest.sql.txt @@ -0,0 +1,138 @@ +--- select_from_join_unnest.sql +-- https://cloud.google.com/spanner/docs/reference/standard-sql/query-syntax#correlated_join +SELECT * +FROM + Roster + JOIN + UNNEST( + ARRAY( + SELECT AS STRUCT * + FROM PlayerStats + WHERE PlayerStats.OpponentID = Roster.SchoolID + )) AS PlayerMatches + ON PlayerMatches.LastName = 'Buchanan' + +--- AST +&ast.QueryStatement{ + Query: &ast.Select{ + Select: 93, + Results: []ast.SelectItem{ + &ast.Star{ + Star: 100, + }, + }, + From: &ast.From{ + From: 102, + Source: &ast.Join{ + Op: "INNER JOIN", + Left: &ast.TableName{ + Table: &ast.Ident{ + NamePos: 109, + NameEnd: 115, + Name: "Roster", + }, + }, + Right: &ast.Unnest{ + Unnest: 127, + Rparen: 256, + Expr: &ast.ArraySubQuery{ + Array: 141, + Rparen: 255, + Query: &ast.Select{ + Select: 156, + As: &ast.AsStruct{ + As: 163, + Struct: 166, + }, + Results: []ast.SelectItem{ + &ast.Star{ + Star: 173, + }, + }, + From: &ast.From{ + From: 181, + Source: &ast.TableName{ + Table: &ast.Ident{ + NamePos: 186, + NameEnd: 197, + Name: "PlayerStats", + }, + }, + }, + Where: &ast.Where{ + Where: 204, + Expr: &ast.BinaryExpr{ + Op: "=", + Left: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 210, + NameEnd: 221, + Name: "PlayerStats", + }, + &ast.Ident{ + NamePos: 222, + NameEnd: 232, + Name: "OpponentID", + }, + }, + }, + Right: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 235, + NameEnd: 241, + Name: "Roster", + }, + &ast.Ident{ + NamePos: 242, + NameEnd: 250, + Name: "SchoolID", + }, + }, + }, + }, + }, + }, + }, + As: &ast.AsAlias{ + As: 258, + Alias: &ast.Ident{ + NamePos: 261, + NameEnd: 274, + Name: "PlayerMatches", + }, + }, + }, + Cond: &ast.On{ + On: 277, + Expr: &ast.BinaryExpr{ + Op: "=", + Left: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 280, + NameEnd: 293, + Name: "PlayerMatches", + }, + &ast.Ident{ + NamePos: 294, + NameEnd: 302, + Name: "LastName", + }, + }, + }, + Right: &ast.StringLiteral{ + ValuePos: 305, + ValueEnd: 315, + Value: "Buchanan", + }, + }, + }, + }, + }, + }, +} + +--- SQL +SELECT * FROM Roster INNER JOIN UNNEST(ARRAY(SELECT AS STRUCT * FROM PlayerStats WHERE PlayerStats.OpponentID = Roster.SchoolID)) AS PlayerMatches ON PlayerMatches.LastName = "Buchanan" diff --git a/testdata/result/query/select_from_left_join_path_table_expr.sql.txt b/testdata/result/query/select_from_left_join_path_table_expr.sql.txt new file mode 100644 index 00000000..ab108403 --- /dev/null +++ b/testdata/result/query/select_from_left_join_path_table_expr.sql.txt @@ -0,0 +1,236 @@ +--- select_from_left_join_path_table_expr.sql +-- https://cloud.google.com/spanner/docs/reference/standard-sql/query-syntax#correlated_join +SELECT A.name, item, ARRAY_LENGTH(A.items) item_count_for_name +FROM + UNNEST( + [ + STRUCT( + 'first' AS name, + [1, 2, 3, 4] AS items), + STRUCT( + 'second' AS name, + [] AS items)]) AS A + LEFT JOIN + A.items AS item + +--- AST +&ast.QueryStatement{ + Query: &ast.Select{ + Select: 93, + Results: []ast.SelectItem{ + &ast.ExprSelectItem{ + Expr: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 100, + NameEnd: 101, + Name: "A", + }, + &ast.Ident{ + NamePos: 102, + NameEnd: 106, + Name: "name", + }, + }, + }, + }, + &ast.ExprSelectItem{ + Expr: &ast.Ident{ + NamePos: 108, + NameEnd: 112, + Name: "item", + }, + }, + &ast.Alias{ + Expr: &ast.CallExpr{ + Rparen: 134, + Func: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 114, + NameEnd: 126, + Name: "ARRAY_LENGTH", + }, + }, + }, + Args: []ast.Arg{ + &ast.ExprArg{ + Expr: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 127, + NameEnd: 128, + Name: "A", + }, + &ast.Ident{ + NamePos: 129, + NameEnd: 134, + Name: "items", + }, + }, + }, + }, + }, + }, + As: &ast.AsAlias{ + As: -1, + Alias: &ast.Ident{ + NamePos: 136, + NameEnd: 155, + Name: "item_count_for_name", + }, + }, + }, + }, + From: &ast.From{ + From: 156, + Source: &ast.Join{ + Op: "LEFT OUTER JOIN", + Left: &ast.Unnest{ + Unnest: 163, + Rparen: 311, + Expr: &ast.ArrayLiteral{ + Array: -1, + Lbrack: 175, + Rbrack: 310, + Values: []ast.Expr{ + &ast.TypelessStructLiteral{ + Struct: 183, + Rparen: 245, + Values: []ast.TypelessStructLiteralArg{ + &ast.Alias{ + Expr: &ast.StringLiteral{ + ValuePos: 199, + ValueEnd: 206, + Value: "first", + }, + As: &ast.AsAlias{ + As: 207, + Alias: &ast.Ident{ + NamePos: 210, + NameEnd: 214, + Name: "name", + }, + }, + }, + &ast.Alias{ + Expr: &ast.ArrayLiteral{ + Array: -1, + Lbrack: 224, + Rbrack: 235, + Values: []ast.Expr{ + &ast.IntLiteral{ + ValuePos: 225, + ValueEnd: 226, + Base: 10, + Value: "1", + }, + &ast.IntLiteral{ + ValuePos: 228, + ValueEnd: 229, + Base: 10, + Value: "2", + }, + &ast.IntLiteral{ + ValuePos: 231, + ValueEnd: 232, + Base: 10, + Value: "3", + }, + &ast.IntLiteral{ + ValuePos: 234, + ValueEnd: 235, + Base: 10, + Value: "4", + }, + }, + }, + As: &ast.AsAlias{ + As: 237, + Alias: &ast.Ident{ + NamePos: 240, + NameEnd: 245, + Name: "items", + }, + }, + }, + }, + }, + &ast.TypelessStructLiteral{ + Struct: 254, + Rparen: 309, + Values: []ast.TypelessStructLiteralArg{ + &ast.Alias{ + Expr: &ast.StringLiteral{ + ValuePos: 272, + ValueEnd: 280, + Value: "second", + }, + As: &ast.AsAlias{ + As: 281, + Alias: &ast.Ident{ + NamePos: 284, + NameEnd: 288, + Name: "name", + }, + }, + }, + &ast.Alias{ + Expr: &ast.ArrayLiteral{ + Array: -1, + Lbrack: 298, + Rbrack: 299, + }, + As: &ast.AsAlias{ + As: 301, + Alias: &ast.Ident{ + NamePos: 304, + NameEnd: 309, + Name: "items", + }, + }, + }, + }, + }, + }, + }, + As: &ast.AsAlias{ + As: 313, + Alias: &ast.Ident{ + NamePos: 316, + NameEnd: 317, + Name: "A", + }, + }, + }, + Right: &ast.PathTableExpr{ + Path: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 334, + NameEnd: 335, + Name: "A", + }, + &ast.Ident{ + NamePos: 336, + NameEnd: 341, + Name: "items", + }, + }, + }, + As: &ast.AsAlias{ + As: 342, + Alias: &ast.Ident{ + NamePos: 345, + NameEnd: 349, + Name: "item", + }, + }, + }, + }, + }, + }, +} + +--- SQL +SELECT A.name, item, ARRAY_LENGTH(A.items) item_count_for_name FROM UNNEST([STRUCT("first" AS name, [1, 2, 3, 4] AS items), STRUCT("second" AS name, [] AS items)]) AS A LEFT OUTER JOIN A.items AS item diff --git a/testdata/result/statement/select_from_inner_join_path_table_expr.sql.txt b/testdata/result/statement/select_from_inner_join_path_table_expr.sql.txt new file mode 100644 index 00000000..bd1a9ff0 --- /dev/null +++ b/testdata/result/statement/select_from_inner_join_path_table_expr.sql.txt @@ -0,0 +1,196 @@ +--- select_from_inner_join_path_table_expr.sql +-- https://cloud.google.com/spanner/docs/reference/standard-sql/query-syntax#correlated_join +SELECT A.name, item +FROM + UNNEST( + [ + STRUCT( + 'first' AS name, + [1, 2, 3, 4] AS items), + STRUCT( + 'second' AS name, + [] AS items)]) AS A + INNER JOIN + A.items AS item + +--- AST +&ast.QueryStatement{ + Query: &ast.Select{ + Select: 93, + Results: []ast.SelectItem{ + &ast.ExprSelectItem{ + Expr: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 100, + NameEnd: 101, + Name: "A", + }, + &ast.Ident{ + NamePos: 102, + NameEnd: 106, + Name: "name", + }, + }, + }, + }, + &ast.ExprSelectItem{ + Expr: &ast.Ident{ + NamePos: 108, + NameEnd: 112, + Name: "item", + }, + }, + }, + From: &ast.From{ + From: 113, + Source: &ast.Join{ + Op: "INNER JOIN", + Left: &ast.Unnest{ + Unnest: 120, + Rparen: 268, + Expr: &ast.ArrayLiteral{ + Array: -1, + Lbrack: 132, + Rbrack: 267, + Values: []ast.Expr{ + &ast.TypelessStructLiteral{ + Struct: 140, + Rparen: 202, + Values: []ast.TypelessStructLiteralArg{ + &ast.Alias{ + Expr: &ast.StringLiteral{ + ValuePos: 156, + ValueEnd: 163, + Value: "first", + }, + As: &ast.AsAlias{ + As: 164, + Alias: &ast.Ident{ + NamePos: 167, + NameEnd: 171, + Name: "name", + }, + }, + }, + &ast.Alias{ + Expr: &ast.ArrayLiteral{ + Array: -1, + Lbrack: 181, + Rbrack: 192, + Values: []ast.Expr{ + &ast.IntLiteral{ + ValuePos: 182, + ValueEnd: 183, + Base: 10, + Value: "1", + }, + &ast.IntLiteral{ + ValuePos: 185, + ValueEnd: 186, + Base: 10, + Value: "2", + }, + &ast.IntLiteral{ + ValuePos: 188, + ValueEnd: 189, + Base: 10, + Value: "3", + }, + &ast.IntLiteral{ + ValuePos: 191, + ValueEnd: 192, + Base: 10, + Value: "4", + }, + }, + }, + As: &ast.AsAlias{ + As: 194, + Alias: &ast.Ident{ + NamePos: 197, + NameEnd: 202, + Name: "items", + }, + }, + }, + }, + }, + &ast.TypelessStructLiteral{ + Struct: 211, + Rparen: 266, + Values: []ast.TypelessStructLiteralArg{ + &ast.Alias{ + Expr: &ast.StringLiteral{ + ValuePos: 229, + ValueEnd: 237, + Value: "second", + }, + As: &ast.AsAlias{ + As: 238, + Alias: &ast.Ident{ + NamePos: 241, + NameEnd: 245, + Name: "name", + }, + }, + }, + &ast.Alias{ + Expr: &ast.ArrayLiteral{ + Array: -1, + Lbrack: 255, + Rbrack: 256, + }, + As: &ast.AsAlias{ + As: 258, + Alias: &ast.Ident{ + NamePos: 261, + NameEnd: 266, + Name: "items", + }, + }, + }, + }, + }, + }, + }, + As: &ast.AsAlias{ + As: 270, + Alias: &ast.Ident{ + NamePos: 273, + NameEnd: 274, + Name: "A", + }, + }, + }, + Right: &ast.PathTableExpr{ + Path: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 292, + NameEnd: 293, + Name: "A", + }, + &ast.Ident{ + NamePos: 294, + NameEnd: 299, + Name: "items", + }, + }, + }, + As: &ast.AsAlias{ + As: 300, + Alias: &ast.Ident{ + NamePos: 303, + NameEnd: 307, + Name: "item", + }, + }, + }, + }, + }, + }, +} + +--- SQL +SELECT A.name, item FROM UNNEST([STRUCT("first" AS name, [1, 2, 3, 4] AS items), STRUCT("second" AS name, [] AS items)]) AS A INNER JOIN A.items AS item diff --git a/testdata/result/statement/select_from_join_unnest.sql.txt b/testdata/result/statement/select_from_join_unnest.sql.txt new file mode 100644 index 00000000..8838628b --- /dev/null +++ b/testdata/result/statement/select_from_join_unnest.sql.txt @@ -0,0 +1,138 @@ +--- select_from_join_unnest.sql +-- https://cloud.google.com/spanner/docs/reference/standard-sql/query-syntax#correlated_join +SELECT * +FROM + Roster + JOIN + UNNEST( + ARRAY( + SELECT AS STRUCT * + FROM PlayerStats + WHERE PlayerStats.OpponentID = Roster.SchoolID + )) AS PlayerMatches + ON PlayerMatches.LastName = 'Buchanan' + +--- AST +&ast.QueryStatement{ + Query: &ast.Select{ + Select: 93, + Results: []ast.SelectItem{ + &ast.Star{ + Star: 100, + }, + }, + From: &ast.From{ + From: 102, + Source: &ast.Join{ + Op: "INNER JOIN", + Left: &ast.TableName{ + Table: &ast.Ident{ + NamePos: 109, + NameEnd: 115, + Name: "Roster", + }, + }, + Right: &ast.Unnest{ + Unnest: 127, + Rparen: 256, + Expr: &ast.ArraySubQuery{ + Array: 141, + Rparen: 255, + Query: &ast.Select{ + Select: 156, + As: &ast.AsStruct{ + As: 163, + Struct: 166, + }, + Results: []ast.SelectItem{ + &ast.Star{ + Star: 173, + }, + }, + From: &ast.From{ + From: 181, + Source: &ast.TableName{ + Table: &ast.Ident{ + NamePos: 186, + NameEnd: 197, + Name: "PlayerStats", + }, + }, + }, + Where: &ast.Where{ + Where: 204, + Expr: &ast.BinaryExpr{ + Op: "=", + Left: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 210, + NameEnd: 221, + Name: "PlayerStats", + }, + &ast.Ident{ + NamePos: 222, + NameEnd: 232, + Name: "OpponentID", + }, + }, + }, + Right: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 235, + NameEnd: 241, + Name: "Roster", + }, + &ast.Ident{ + NamePos: 242, + NameEnd: 250, + Name: "SchoolID", + }, + }, + }, + }, + }, + }, + }, + As: &ast.AsAlias{ + As: 258, + Alias: &ast.Ident{ + NamePos: 261, + NameEnd: 274, + Name: "PlayerMatches", + }, + }, + }, + Cond: &ast.On{ + On: 277, + Expr: &ast.BinaryExpr{ + Op: "=", + Left: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 280, + NameEnd: 293, + Name: "PlayerMatches", + }, + &ast.Ident{ + NamePos: 294, + NameEnd: 302, + Name: "LastName", + }, + }, + }, + Right: &ast.StringLiteral{ + ValuePos: 305, + ValueEnd: 315, + Value: "Buchanan", + }, + }, + }, + }, + }, + }, +} + +--- SQL +SELECT * FROM Roster INNER JOIN UNNEST(ARRAY(SELECT AS STRUCT * FROM PlayerStats WHERE PlayerStats.OpponentID = Roster.SchoolID)) AS PlayerMatches ON PlayerMatches.LastName = "Buchanan" diff --git a/testdata/result/statement/select_from_left_join_path_table_expr.sql.txt b/testdata/result/statement/select_from_left_join_path_table_expr.sql.txt new file mode 100644 index 00000000..ab108403 --- /dev/null +++ b/testdata/result/statement/select_from_left_join_path_table_expr.sql.txt @@ -0,0 +1,236 @@ +--- select_from_left_join_path_table_expr.sql +-- https://cloud.google.com/spanner/docs/reference/standard-sql/query-syntax#correlated_join +SELECT A.name, item, ARRAY_LENGTH(A.items) item_count_for_name +FROM + UNNEST( + [ + STRUCT( + 'first' AS name, + [1, 2, 3, 4] AS items), + STRUCT( + 'second' AS name, + [] AS items)]) AS A + LEFT JOIN + A.items AS item + +--- AST +&ast.QueryStatement{ + Query: &ast.Select{ + Select: 93, + Results: []ast.SelectItem{ + &ast.ExprSelectItem{ + Expr: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 100, + NameEnd: 101, + Name: "A", + }, + &ast.Ident{ + NamePos: 102, + NameEnd: 106, + Name: "name", + }, + }, + }, + }, + &ast.ExprSelectItem{ + Expr: &ast.Ident{ + NamePos: 108, + NameEnd: 112, + Name: "item", + }, + }, + &ast.Alias{ + Expr: &ast.CallExpr{ + Rparen: 134, + Func: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 114, + NameEnd: 126, + Name: "ARRAY_LENGTH", + }, + }, + }, + Args: []ast.Arg{ + &ast.ExprArg{ + Expr: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 127, + NameEnd: 128, + Name: "A", + }, + &ast.Ident{ + NamePos: 129, + NameEnd: 134, + Name: "items", + }, + }, + }, + }, + }, + }, + As: &ast.AsAlias{ + As: -1, + Alias: &ast.Ident{ + NamePos: 136, + NameEnd: 155, + Name: "item_count_for_name", + }, + }, + }, + }, + From: &ast.From{ + From: 156, + Source: &ast.Join{ + Op: "LEFT OUTER JOIN", + Left: &ast.Unnest{ + Unnest: 163, + Rparen: 311, + Expr: &ast.ArrayLiteral{ + Array: -1, + Lbrack: 175, + Rbrack: 310, + Values: []ast.Expr{ + &ast.TypelessStructLiteral{ + Struct: 183, + Rparen: 245, + Values: []ast.TypelessStructLiteralArg{ + &ast.Alias{ + Expr: &ast.StringLiteral{ + ValuePos: 199, + ValueEnd: 206, + Value: "first", + }, + As: &ast.AsAlias{ + As: 207, + Alias: &ast.Ident{ + NamePos: 210, + NameEnd: 214, + Name: "name", + }, + }, + }, + &ast.Alias{ + Expr: &ast.ArrayLiteral{ + Array: -1, + Lbrack: 224, + Rbrack: 235, + Values: []ast.Expr{ + &ast.IntLiteral{ + ValuePos: 225, + ValueEnd: 226, + Base: 10, + Value: "1", + }, + &ast.IntLiteral{ + ValuePos: 228, + ValueEnd: 229, + Base: 10, + Value: "2", + }, + &ast.IntLiteral{ + ValuePos: 231, + ValueEnd: 232, + Base: 10, + Value: "3", + }, + &ast.IntLiteral{ + ValuePos: 234, + ValueEnd: 235, + Base: 10, + Value: "4", + }, + }, + }, + As: &ast.AsAlias{ + As: 237, + Alias: &ast.Ident{ + NamePos: 240, + NameEnd: 245, + Name: "items", + }, + }, + }, + }, + }, + &ast.TypelessStructLiteral{ + Struct: 254, + Rparen: 309, + Values: []ast.TypelessStructLiteralArg{ + &ast.Alias{ + Expr: &ast.StringLiteral{ + ValuePos: 272, + ValueEnd: 280, + Value: "second", + }, + As: &ast.AsAlias{ + As: 281, + Alias: &ast.Ident{ + NamePos: 284, + NameEnd: 288, + Name: "name", + }, + }, + }, + &ast.Alias{ + Expr: &ast.ArrayLiteral{ + Array: -1, + Lbrack: 298, + Rbrack: 299, + }, + As: &ast.AsAlias{ + As: 301, + Alias: &ast.Ident{ + NamePos: 304, + NameEnd: 309, + Name: "items", + }, + }, + }, + }, + }, + }, + }, + As: &ast.AsAlias{ + As: 313, + Alias: &ast.Ident{ + NamePos: 316, + NameEnd: 317, + Name: "A", + }, + }, + }, + Right: &ast.PathTableExpr{ + Path: &ast.Path{ + Idents: []*ast.Ident{ + &ast.Ident{ + NamePos: 334, + NameEnd: 335, + Name: "A", + }, + &ast.Ident{ + NamePos: 336, + NameEnd: 341, + Name: "items", + }, + }, + }, + As: &ast.AsAlias{ + As: 342, + Alias: &ast.Ident{ + NamePos: 345, + NameEnd: 349, + Name: "item", + }, + }, + }, + }, + }, + }, +} + +--- SQL +SELECT A.name, item, ARRAY_LENGTH(A.items) item_count_for_name FROM UNNEST([STRUCT("first" AS name, [1, 2, 3, 4] AS items), STRUCT("second" AS name, [] AS items)]) AS A LEFT OUTER JOIN A.items AS item