feat(spanner/spannertest): support INNER and CROSS JOIN (#3037)

These are similar to the existing LEFT JOIN.
Implementing FULL and RIGHT JOIN is harder.
diff --git a/spanner/spannertest/db_query.go b/spanner/spannertest/db_query.go
index d4a1786..d97eb2f 100644
--- a/spanner/spannertest/db_query.go
+++ b/spanner/spannertest/db_query.go
@@ -800,11 +800,92 @@
 }
 
 func (ji *joinIter) Next() (row, error) {
-	// TODO: More join types.
-	if ji.jt != spansql.LeftJoin {
+	// TODO: FULL and RIGHT joins; they'll need more structural work in joinIter.
+	switch ji.jt {
+	default:
 		return nil, fmt.Errorf("TODO: can't yet evaluate join of type %v", ji.jt)
+	case spansql.InnerJoin:
+		return ji.innerJoin()
+	case spansql.CrossJoin:
+		return ji.crossJoin()
+	case spansql.LeftJoin:
+		return ji.leftJoin()
+	}
+}
+
+// TODO: Refactor these individual JOIN implementations when they are complete.
+
+func (ji *joinIter) innerJoin() (row, error) {
+	/*
+		An INNER JOIN, or simply JOIN, effectively calculates the
+		Cartesian product of the two from_items and discards all rows
+		that do not meet the join condition.
+	*/
+	if ji.lhsRow == nil {
+		if err := ji.nextLeft(); err != nil {
+			return nil, err
+		}
 	}
 
+	for {
+		rhsRow, err := ji.rhs.Next()
+		if err == io.EOF {
+			// Finished the current LHS row;
+			// advance to next one.
+			if err := ji.nextLeft(); err != nil {
+				return nil, err
+			}
+			continue
+		}
+		if err != nil {
+			return nil, err
+		}
+		match, err := ji.cond(ji.lhsRow, rhsRow)
+		if err != nil {
+			return nil, err
+		}
+		if !match {
+			continue
+		}
+		return ji.ec.row, nil
+	}
+}
+
+func (ji *joinIter) crossJoin() (row, error) {
+	/*
+		CROSS JOIN returns the Cartesian product of the two from_items.
+		In other words, it combines each row from the first from_item
+		with each row from the second from_item.
+	*/
+	if ji.lhsRow == nil {
+		if err := ji.nextLeft(); err != nil {
+			return nil, err
+		}
+	}
+
+	for {
+		rhsRow, err := ji.rhs.Next()
+		if err == io.EOF {
+			// Finished the current LHS row;
+			// advance to next one.
+			if err := ji.nextLeft(); err != nil {
+				return nil, err
+			}
+			continue
+		}
+		if err != nil {
+			return nil, err
+		}
+		// The condition will be trivially true.
+		_, err = ji.cond(ji.lhsRow, rhsRow)
+		if err != nil {
+			return nil, err
+		}
+		return ji.ec.row, nil
+	}
+}
+
+func (ji *joinIter) leftJoin() (row, error) {
 	/*
 		The result of a LEFT OUTER JOIN (or simply LEFT JOIN) for two
 		from_items always retains all rows of the left from_item in the
diff --git a/spanner/spannertest/integration_test.go b/spanner/spannertest/integration_test.go
index bf30035..24becb5 100644
--- a/spanner/spannertest/integration_test.go
+++ b/spanner/spannertest/integration_test.go
@@ -407,10 +407,7 @@
 	allTables := []string{
 		"Staff",
 		"PlayerStats",
-		"JoinA",
-		"JoinB",
-		"JoinC",
-		"JoinD",
+		"JoinA", "JoinB", "JoinC", "JoinD", "JoinE", "JoinF",
 		"SomeStrings",
 	}
 	for _, table := range allTables {
@@ -600,6 +597,8 @@
 		`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)`,
 		// Some other test tables.
 		`CREATE TABLE SomeStrings ( i INT64, str STRING(MAX) ) PRIMARY KEY (i)`,
 	)
@@ -634,6 +633,13 @@
 		spanner.Insert("JoinD", []string{"x", "z"}, []interface{}{3, "n"}),
 		spanner.Insert("JoinD", []string{"x", "z"}, []interface{}{4, "p"}),
 
+		// 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("JoinF", []string{"y", "z"}, []interface{}{2, "c"}),
+		spanner.Insert("JoinF", []string{"y", "z"}, []interface{}{3, "d"}),
+
 		spanner.Insert("SomeStrings", []string{"i", "str"}, []interface{}{0, "afoo"}),
 		spanner.Insert("SomeStrings", []string{"i", "str"}, []interface{}{1, "abar"}),
 		spanner.Insert("SomeStrings", []string{"i", "str"}, []interface{}{2, nil}),
@@ -873,6 +879,27 @@
 		},
 		// Joins.
 		{
+			`SELECT * FROM JoinA INNER JOIN JoinB ON JoinA.w = JoinB.y ORDER BY w, x, y, z`,
+			nil,
+			[][]interface{}{
+				{int64(2), "b", int64(2), "k"},
+				{int64(3), "c", int64(3), "m"},
+				{int64(3), "c", int64(3), "n"},
+				{int64(3), "d", int64(3), "m"},
+				{int64(3), "d", int64(3), "n"},
+			},
+		},
+		{
+			`SELECT * FROM JoinE CROSS JOIN JoinF ORDER BY w, x, y, z`,
+			nil,
+			[][]interface{}{
+				{int64(1), "a", int64(2), "c"},
+				{int64(1), "a", int64(3), "d"},
+				{int64(2), "b", int64(2), "c"},
+				{int64(2), "b", int64(3), "d"},
+			},
+		},
+		{
 			`SELECT * FROM JoinA LEFT OUTER JOIN JoinB AS B ON JoinA.w = B.y ORDER BY w, x, y, z`,
 			nil,
 			[][]interface{}{