diff --git a/spanner/spannertest/integration_test.go b/spanner/spannertest/integration_test.go index f2513e4444c..7477f71c969 100644 --- a/spanner/spannertest/integration_test.go +++ b/spanner/spannertest/integration_test.go @@ -652,12 +652,12 @@ func TestIntegration_ReadsAndQueries(t *testing.T) { ) PRIMARY KEY (LastName, OpponentID)`, // TODO: is this right? // JoinFoo are from https://cloud.google.com/spanner/docs/query-syntax#join_types. // They aren't consistently named in the docs. - `CREATE TABLE JoinA ( w INT64, x STRING(MAX) ) PRIMARY KEY (w, x)`, - `CREATE TABLE JoinB ( y INT64, z STRING(MAX) ) PRIMARY KEY (y, z)`, - `CREATE TABLE JoinC ( x INT64, y STRING(MAX) ) PRIMARY KEY (x, y)`, - `CREATE TABLE JoinD ( x INT64, z STRING(MAX) ) PRIMARY KEY (x, z)`, - `CREATE TABLE JoinE ( w INT64, x STRING(MAX) ) PRIMARY KEY (w, x)`, - `CREATE TABLE JoinF ( y INT64, z STRING(MAX) ) PRIMARY KEY (y, z)`, + `CREATE TABLE JoinA ( w INT64, x STRING(MAX), a STRING(MAX) ) PRIMARY KEY (w, x)`, + `CREATE TABLE JoinB ( y INT64, z STRING(MAX), b STRING(MAX) ) PRIMARY KEY (y, z)`, + `CREATE TABLE JoinC ( x INT64, y STRING(MAX), c STRING(MAX) ) PRIMARY KEY (x, y)`, + `CREATE TABLE JoinD ( x INT64, z STRING(MAX), d STRING(MAX) ) PRIMARY KEY (x, z)`, + `CREATE TABLE JoinE ( w INT64, x STRING(MAX), e STRING(MAX) ) PRIMARY KEY (w, x)`, + `CREATE TABLE JoinF ( y INT64, z STRING(MAX), f STRING(MAX) ) PRIMARY KEY (y, z)`, // Some other test tables. `CREATE TABLE SomeStrings ( i INT64, str STRING(MAX) ) PRIMARY KEY (i)`, `CREATE TABLE Updateable ( @@ -676,33 +676,33 @@ func TestIntegration_ReadsAndQueries(t *testing.T) { spanner.Insert("PlayerStats", []string{"LastName", "OpponentID", "PointsScored"}, []interface{}{"Adams", 52, 4}), spanner.Insert("PlayerStats", []string{"LastName", "OpponentID", "PointsScored"}, []interface{}{"Buchanan", 50, 13}), - spanner.Insert("JoinA", []string{"w", "x"}, []interface{}{1, "a"}), - spanner.Insert("JoinA", []string{"w", "x"}, []interface{}{2, "b"}), - spanner.Insert("JoinA", []string{"w", "x"}, []interface{}{3, "c"}), - spanner.Insert("JoinA", []string{"w", "x"}, []interface{}{3, "d"}), + spanner.Insert("JoinA", []string{"w", "x", "a"}, []interface{}{1, "a", "a1"}), + spanner.Insert("JoinA", []string{"w", "x", "a"}, []interface{}{2, "b", "a2"}), + spanner.Insert("JoinA", []string{"w", "x", "a"}, []interface{}{3, "c", "a3"}), + spanner.Insert("JoinA", []string{"w", "x", "a"}, []interface{}{3, "d", "a4"}), - spanner.Insert("JoinB", []string{"y", "z"}, []interface{}{2, "k"}), - spanner.Insert("JoinB", []string{"y", "z"}, []interface{}{3, "m"}), - spanner.Insert("JoinB", []string{"y", "z"}, []interface{}{3, "n"}), - spanner.Insert("JoinB", []string{"y", "z"}, []interface{}{4, "p"}), + spanner.Insert("JoinB", []string{"y", "z", "b"}, []interface{}{2, "k", "b1"}), + spanner.Insert("JoinB", []string{"y", "z", "b"}, []interface{}{3, "m", "b2"}), + spanner.Insert("JoinB", []string{"y", "z", "b"}, []interface{}{3, "n", "b3"}), + spanner.Insert("JoinB", []string{"y", "z", "b"}, []interface{}{4, "p", "b4"}), // JoinC and JoinD have the same contents as JoinA and JoinB; they have different column names. - spanner.Insert("JoinC", []string{"x", "y"}, []interface{}{1, "a"}), - spanner.Insert("JoinC", []string{"x", "y"}, []interface{}{2, "b"}), - spanner.Insert("JoinC", []string{"x", "y"}, []interface{}{3, "c"}), - spanner.Insert("JoinC", []string{"x", "y"}, []interface{}{3, "d"}), + spanner.Insert("JoinC", []string{"x", "y", "c"}, []interface{}{1, "a", "c1"}), + spanner.Insert("JoinC", []string{"x", "y", "c"}, []interface{}{2, "b", "c2"}), + spanner.Insert("JoinC", []string{"x", "y", "c"}, []interface{}{3, "c", "c3"}), + spanner.Insert("JoinC", []string{"x", "y", "c"}, []interface{}{3, "d", "c4"}), - spanner.Insert("JoinD", []string{"x", "z"}, []interface{}{2, "k"}), - spanner.Insert("JoinD", []string{"x", "z"}, []interface{}{3, "m"}), - spanner.Insert("JoinD", []string{"x", "z"}, []interface{}{3, "n"}), - spanner.Insert("JoinD", []string{"x", "z"}, []interface{}{4, "p"}), + spanner.Insert("JoinD", []string{"x", "z", "d"}, []interface{}{2, "k", "d1"}), + spanner.Insert("JoinD", []string{"x", "z", "d"}, []interface{}{3, "m", "d2"}), + spanner.Insert("JoinD", []string{"x", "z", "d"}, []interface{}{3, "n", "d3"}), + spanner.Insert("JoinD", []string{"x", "z", "d"}, []interface{}{4, "p", "d4"}), // JoinE and JoinF are used in the CROSS JOIN test. - spanner.Insert("JoinE", []string{"w", "x"}, []interface{}{1, "a"}), - spanner.Insert("JoinE", []string{"w", "x"}, []interface{}{2, "b"}), + spanner.Insert("JoinE", []string{"w", "x", "e"}, []interface{}{1, "a", "e1"}), + spanner.Insert("JoinE", []string{"w", "x", "e"}, []interface{}{2, "b", "e2"}), - spanner.Insert("JoinF", []string{"y", "z"}, []interface{}{2, "c"}), - spanner.Insert("JoinF", []string{"y", "z"}, []interface{}{3, "d"}), + spanner.Insert("JoinF", []string{"y", "z", "f"}, []interface{}{2, "c", "f1"}), + spanner.Insert("JoinF", []string{"y", "z", "f"}, []interface{}{3, "d", "f2"}), spanner.Insert("SomeStrings", []string{"i", "str"}, []interface{}{0, "afoo"}), spanner.Insert("SomeStrings", []string{"i", "str"}, []interface{}{1, "abar"}), @@ -1011,7 +1011,7 @@ func TestIntegration_ReadsAndQueries(t *testing.T) { }, // Joins. { - `SELECT * FROM JoinA INNER JOIN JoinB ON JoinA.w = JoinB.y ORDER BY w, x, y, z`, + `SELECT w, x, y, z FROM JoinA INNER JOIN JoinB ON JoinA.w = JoinB.y ORDER BY w, x, y, z`, nil, [][]interface{}{ {int64(2), "b", int64(2), "k"}, @@ -1022,7 +1022,7 @@ func TestIntegration_ReadsAndQueries(t *testing.T) { }, }, { - `SELECT * FROM JoinE CROSS JOIN JoinF ORDER BY w, x, y, z`, + `SELECT w, x, y, z FROM JoinE CROSS JOIN JoinF ORDER BY w, x, y, z`, nil, [][]interface{}{ {int64(1), "a", int64(2), "c"}, @@ -1033,7 +1033,7 @@ func TestIntegration_ReadsAndQueries(t *testing.T) { }, { // Same as in docs, but with a weird ORDER BY clause to match the row ordering. - `SELECT * FROM JoinA FULL OUTER JOIN JoinB ON JoinA.w = JoinB.y ORDER BY w IS NULL, w, x, y, z`, + `SELECT w, x, y, z FROM JoinA FULL OUTER JOIN JoinB ON JoinA.w = JoinB.y ORDER BY w IS NULL, w, x, y, z`, nil, [][]interface{}{ {int64(1), "a", nil, nil}, @@ -1047,7 +1047,7 @@ func TestIntegration_ReadsAndQueries(t *testing.T) { }, { // Same as the previous, but using a USING clause instead of an ON clause. - `SELECT * FROM JoinC FULL OUTER JOIN JoinD USING (x) ORDER BY x, y, z`, + `SELECT x, y, z FROM JoinC FULL OUTER JOIN JoinD USING (x) ORDER BY x, y, z`, nil, [][]interface{}{ {int64(1), "a", nil}, @@ -1060,7 +1060,7 @@ func TestIntegration_ReadsAndQueries(t *testing.T) { }, }, { - `SELECT * FROM JoinA LEFT OUTER JOIN JoinB AS B ON JoinA.w = B.y ORDER BY w, x, y, z`, + `SELECT w, x, y, z FROM JoinA LEFT OUTER JOIN JoinB AS B ON JoinA.w = B.y ORDER BY w, x, y, z`, nil, [][]interface{}{ {int64(1), "a", nil, nil}, @@ -1073,7 +1073,7 @@ func TestIntegration_ReadsAndQueries(t *testing.T) { }, { // Same as the previous, but using a USING clause instead of an ON clause. - `SELECT * FROM JoinC LEFT OUTER JOIN JoinD USING (x) ORDER BY x, y, z`, + `SELECT x, y, z FROM JoinC LEFT OUTER JOIN JoinD USING (x) ORDER BY x, y, z`, nil, [][]interface{}{ {int64(1), "a", nil}, @@ -1086,7 +1086,7 @@ func TestIntegration_ReadsAndQueries(t *testing.T) { }, { // Same as in docs, but with a weird ORDER BY clause to match the row ordering. - `SELECT * FROM JoinA RIGHT OUTER JOIN JoinB AS B ON JoinA.w = B.y ORDER BY w IS NULL, w, x, y, z`, + `SELECT w, x, y, z FROM JoinA RIGHT OUTER JOIN JoinB AS B ON JoinA.w = B.y ORDER BY w IS NULL, w, x, y, z`, nil, [][]interface{}{ {int64(2), "b", int64(2), "k"}, @@ -1098,7 +1098,7 @@ func TestIntegration_ReadsAndQueries(t *testing.T) { }, }, { - `SELECT * FROM JoinC RIGHT OUTER JOIN JoinD USING (x) ORDER BY x, y, z`, + `SELECT x, y, z FROM JoinC RIGHT OUTER JOIN JoinD USING (x) ORDER BY x, y, z`, nil, [][]interface{}{ {int64(2), "b", "k"}, @@ -1109,6 +1109,21 @@ func TestIntegration_ReadsAndQueries(t *testing.T) { {int64(4), nil, "p"}, }, }, + { + `SELECT a, b, c FROM JoinA JOIN JoinB ON JoinA.w = JoinB.y JOIN JoinC ON JoinA.w = JoinC.x WHERE JoinA.w = 2 ORDER BY x, y, z`, + nil, + [][]interface{}{ + {"a2", "b1", "c2"}, + }, + }, + { + `SELECT a, b, c FROM JoinA LEFT JOIN JoinB ON JoinA.w = JoinB.y JOIN JoinC ON JoinC.x = JoinA.w WHERE JoinA.w = 1 OR JoinA.w = 2 ORDER BY x, y, z`, + nil, + [][]interface{}{ + {"a1", nil, "c1"}, + {"a2", "b1", "c2"}, + }, + }, // Check the output of the UPDATE DML. { `SELECT id, first, last FROM Updateable ORDER BY id`, diff --git a/spanner/spansql/parser.go b/spanner/spansql/parser.go index 44fdfc7d82f..1d11394c0db 100644 --- a/spanner/spansql/parser.go +++ b/spanner/spansql/parser.go @@ -2101,28 +2101,7 @@ func (p *parser) parseSelectList() ([]Expr, []ID, *parseError) { return list, aliases, nil } -func (p *parser) parseSelectFrom() (SelectFrom, *parseError) { - debugf("parseSelectFrom: %v", p) - - /* - from_item: { - table_name [ table_hint_expr ] [ [ AS ] alias ] | - join | - ( query_expr ) [ table_hint_expr ] [ [ AS ] alias ] | - field_path | - { UNNEST( array_expression ) | UNNEST( array_path ) | array_path } - [ table_hint_expr ] [ [ AS ] alias ] [ WITH OFFSET [ [ AS ] alias ] ] | - with_query_name [ table_hint_expr ] [ [ AS ] alias ] - } - - join: - from_item [ join_type ] [ join_method ] JOIN [ join_hint_expr ] from_item - [ ON bool_expression | USING ( join_column [, ...] ) ] - - join_type: - { INNER | CROSS | FULL [OUTER] | LEFT [OUTER] | RIGHT [OUTER] } - */ - +func (p *parser) parseSelectFromTable() (SelectFrom, *parseError) { if p.eat("UNNEST") { if err := p.expect("("); err != nil { return nil, err @@ -2171,19 +2150,22 @@ func (p *parser) parseSelectFrom() (SelectFrom, *parseError) { } sf.Alias = alias } + return sf, nil +} +func (p *parser) parseSelectFromJoin(lhs SelectFrom) (SelectFrom, *parseError) { // Look ahead to see if this is a join. tok := p.next() if tok.err != nil { p.back() - return sf, nil + return nil, nil } var hashJoin bool // Special case for "HASH JOIN" syntax. if tok.caseEqual("HASH") { hashJoin = true tok = p.next() if tok.err != nil { - return nil, err + return nil, tok.err } } var jt JoinType @@ -2202,13 +2184,13 @@ func (p *parser) parseSelectFrom() (SelectFrom, *parseError) { return nil, err } } else { + // Not a join p.back() - return sf, nil + return nil, nil } - sfj := SelectFromJoin{ Type: jt, - LHS: sf, + LHS: lhs, } var hints map[string]string if hashJoin { @@ -2225,11 +2207,13 @@ func (p *parser) parseSelectFrom() (SelectFrom, *parseError) { } sfj.Hints = hints - sfj.RHS, err = p.parseSelectFrom() + rhs, err := p.parseSelectFromTable() if err != nil { return nil, err } + sfj.RHS = rhs + if p.eat("ON") { sfj.On, err = p.parseBoolExpr() if err != nil { @@ -2249,6 +2233,46 @@ func (p *parser) parseSelectFrom() (SelectFrom, *parseError) { return sfj, nil } +func (p *parser) parseSelectFrom() (SelectFrom, *parseError) { + debugf("parseSelectFrom: %v", p) + + /* + from_item: { + table_name [ table_hint_expr ] [ [ AS ] alias ] | + join | + ( query_expr ) [ table_hint_expr ] [ [ AS ] alias ] | + field_path | + { UNNEST( array_expression ) | UNNEST( array_path ) | array_path } + [ table_hint_expr ] [ [ AS ] alias ] [ WITH OFFSET [ [ AS ] alias ] ] | + with_query_name [ table_hint_expr ] [ [ AS ] alias ] + } + + join: + from_item [ join_type ] [ join_method ] JOIN [ join_hint_expr ] from_item + [ ON bool_expression | USING ( join_column [, ...] ) ] + + join_type: + { INNER | CROSS | FULL [OUTER] | LEFT [OUTER] | RIGHT [OUTER] } + */ + leftHandSide, err := p.parseSelectFromTable() + if err != nil { + return nil, err + } + // Lets keep consuming joins until we no longer find more joins + for { + sfj, err := p.parseSelectFromJoin(leftHandSide) + if err != nil { + return nil, err + } + if sfj == nil { + // There was no join to consume + break + } + leftHandSide = sfj + } + return leftHandSide, nil +} + var joinKeywords = map[string]JoinType{ "INNER": InnerJoin, "CROSS": CrossJoin, diff --git a/spanner/spansql/sql_test.go b/spanner/spansql/sql_test.go index dc26d520273..893de913e52 100644 --- a/spanner/spansql/sql_test.go +++ b/spanner/spansql/sql_test.go @@ -483,6 +483,34 @@ func TestSQL(t *testing.T) { "SELECT A, B FROM Table1 INNER JOIN Table2 ON Table1.A = Table2.A", reparseQuery, }, + { + Query{ + Select: Select{ + List: []Expr{ + ID("A"), ID("B"), + }, + From: []SelectFrom{ + SelectFromJoin{ + Type: InnerJoin, + LHS: SelectFromJoin{ + Type: InnerJoin, + LHS: SelectFromTable{Table: "Table1"}, + RHS: SelectFromTable{Table: "Table2"}, + On: ComparisonOp{ + LHS: PathExp{"Table1", "A"}, + Op: Eq, + RHS: PathExp{"Table2", "A"}, + }, + }, + RHS: SelectFromTable{Table: "Table3"}, + Using: []ID{"X"}, + }, + }, + }, + }, + "SELECT A, B FROM Table1 INNER JOIN Table2 ON Table1.A = Table2.A INNER JOIN Table3 USING (X)", + reparseQuery, + }, } for _, test := range tests { sql := test.data.SQL()