diff --git a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt index 6fc2ab0..8ad9d06 100644 --- a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt +++ b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt @@ -310,3 +310,166 @@ class DiskExecutorTest { run("INSERT INTO orders VALUES (1, 1, 100), (2, 1, 50), (3, 2, 200)") } + // Adds an order referencing a customer id that does not exist, so RIGHT/FULL JOIN has an + // unmatched *right* row to pad with NULLs on top of grace's unmatched left row from seedOrders. + private fun Database.seedOrdersWithOrphan() { + seedOrders() + run("INSERT INTO orders VALUES (4, 99, 999)") + } + + @Test + fun rightJoinPadsUnmatchedRightRowsWithNulls() { + val db = newDatabase() + db.seedOrdersWithOrphan() + val result = db.select( + "SELECT customers.name, orders.amount FROM customers RIGHT JOIN orders ON customers.id = orders.customer_id ORDER BY orders.id" + ) + assertEquals( + listOf( + listOf(text("ada"), int(100)), + listOf(text("ada"), int(50)), + listOf(text("linus"), int(200)), + listOf(Value.NullValue, int(999)) + ), + result.rows + ) + } + + @Test + fun fullJoinKeepsBothUnmatchedLeftAndRightRows() { + val db = newDatabase() + db.seedOrdersWithOrphan() + val result = db.select( + "SELECT customers.name, orders.amount FROM customers FULL JOIN orders ON customers.id = orders.customer_id" + ) + assertEquals( + setOf( + listOf(text("ada"), int(100)), + listOf(text("ada"), int(50)), + listOf(text("linus"), int(200)), + listOf(text("grace"), Value.NullValue), + listOf(Value.NullValue, int(999)) + ), + result.rows.toSet() + ) + assertEquals(5, result.rows.size) + } + + @Test + fun crossJoinProducesTheFullCartesianProductWithNoOnClause() { + val db = newDatabase() + db.run("CREATE TABLE colors (name TEXT)") + db.run("CREATE TABLE sizes (label TEXT)") + db.run("INSERT INTO colors VALUES ('red'), ('blue')") + db.run("INSERT INTO sizes VALUES ('s'), ('m')") + val result = db.select("SELECT colors.name, sizes.label FROM colors CROSS JOIN sizes ORDER BY colors.name, sizes.label") + assertEquals( + listOf( + listOf(text("blue"), text("m")), listOf(text("blue"), text("s")), + listOf(text("red"), text("m")), listOf(text("red"), text("s")) + ), + result.rows + ) + } + + @Test + fun rightOuterAndFullOuterJoinSpellingsAreAccepted() { + val db = newDatabase() + db.seedOrders() + val right = db.select( + "SELECT customers.name FROM customers RIGHT OUTER JOIN orders ON customers.id = orders.customer_id ORDER BY orders.id" + ) + assertEquals(listOf(listOf(text("ada")), listOf(text("ada")), listOf(text("linus"))), right.rows) + val full = db.select( + "SELECT customers.name FROM customers FULL OUTER JOIN orders ON customers.id = orders.customer_id ORDER BY customers.id" + ) + assertEquals(4, full.rows.size) + } + + @Test + fun innerJoinCombinesMatchingRowsAndDropsUnmatchedLeftRows() { + val db = newDatabase() + db.seedOrders() + val result = db.select( + "SELECT customers.name, orders.amount FROM customers JOIN orders ON customers.id = orders.customer_id ORDER BY orders.id" + ) + assertEquals(listOf("name", "amount"), result.columns) + assertEquals( + listOf(listOf(text("ada"), int(100)), listOf(text("ada"), int(50)), listOf(text("linus"), int(200))), + result.rows + ) + } + + @Test + fun leftJoinPadsUnmatchedLeftRowsWithNulls() { + val db = newDatabase() + db.seedOrders() + val result = db.select( + "SELECT customers.name, orders.amount FROM customers LEFT JOIN orders ON customers.id = orders.customer_id ORDER BY customers.id, orders.id" + ) + assertEquals( + listOf( + listOf(text("ada"), int(100)), + listOf(text("ada"), int(50)), + listOf(text("linus"), int(200)), + listOf(text("grace"), Value.NullValue) + ), + result.rows + ) + } + + @Test + fun joinUsesTableAliasesToQualifyColumnsWithTheSameName() { + val db = newDatabase() + db.run("CREATE TABLE a (id INTEGER PRIMARY KEY, tag TEXT NOT NULL)") + db.run("CREATE TABLE b (id INTEGER PRIMARY KEY, tag TEXT NOT NULL)") + db.run("INSERT INTO a VALUES (1, 'left')") + db.run("INSERT INTO b VALUES (1, 'right')") + val result = db.select("SELECT x.tag, y.tag FROM a x JOIN b y ON x.id = y.id") + assertEquals(listOf(listOf(text("left"), text("right"))), result.rows) + } + + @Test + fun anUnqualifiedColumnPresentInBothJoinedTablesIsRejectedAsAmbiguous() { + val db = newDatabase() + db.run("CREATE TABLE a (id INTEGER PRIMARY KEY)") + db.run("CREATE TABLE b (id INTEGER PRIMARY KEY)") + db.run("INSERT INTO a VALUES (1)") + db.run("INSERT INTO b VALUES (1)") + val failure = assertFailsWith { db.select("SELECT id FROM a JOIN b ON a.id = b.id") } + assertTrue(failure.message!!.contains("ambiguous")) + } + + @Test + fun aDuplicateTableAliasAcrossJoinedSourcesIsRejected() { + val db = newDatabase() + db.run("CREATE TABLE a (id INTEGER PRIMARY KEY)") + val failure = assertFailsWith { db.select("SELECT * FROM a x JOIN a x ON x.id = x.id") } + assertTrue(failure.message!!.contains("duplicate table alias")) + } + + @Test + fun joiningThreeTablesChainsLeftToRight() { + val db = newDatabase() + db.seedOrders() + db.run("CREATE TABLE shipments (id INTEGER PRIMARY KEY, order_id INTEGER NOT NULL, carrier TEXT NOT NULL)") + db.run("INSERT INTO shipments VALUES (1, 1, 'ups')") + val result = db.select( + "SELECT customers.name, orders.amount, shipments.carrier " + + "FROM customers JOIN orders ON customers.id = orders.customer_id " + + "JOIN shipments ON orders.id = shipments.order_id" + ) + assertEquals(listOf(listOf(text("ada"), int(100), text("ups"))), result.rows) + } + + @Test + fun joinAgainstADerivedTableWorks() { + val db = newDatabase() + db.run("CREATE TABLE a (id INTEGER PRIMARY KEY, name TEXT NOT NULL)") + db.run("CREATE TABLE b (id INTEGER PRIMARY KEY)") + db.run("INSERT INTO a VALUES (1, 'ada'), (2, 'linus')") + db.run("INSERT INTO b VALUES (1)") + val result = db.select("SELECT a.name FROM a JOIN (SELECT id FROM b) sub ON a.id = sub.id") + assertEquals(listOf(listOf(text("ada"))), result.rows) + } +