diff --git a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt index f71f74f..f1514e4 100644 --- a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt +++ b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt @@ -1432,3 +1432,158 @@ class DiskExecutorTest { assertEquals(0, table.rangeScanCount) } + @Test + fun updateAndDeleteByFullCompositePrimaryKeyUsePointLookups() { + val db = newDatabase() + db.seedOrderLines() + val table = db.catalog.table("order_lines") + + val updated = db.run("UPDATE order_lines SET sku = 'sprocket' WHERE order_id = 1 AND line_no = 1").single() + assertEquals(UpdateResult(1), updated) + assertEquals(1, table.pointLookupCount) + + val deleted = db.run("DELETE FROM order_lines WHERE order_id = 2 AND line_no = 1").single() + assertEquals(DeleteResult(1), deleted) + assertEquals(2, table.pointLookupCount) + assertEquals(0, table.scanCount) + assertEquals(2, db.select("SELECT * FROM order_lines").rows.size) + } + + @Test + fun aTableCannotDeclareBothAnInlinePrimaryKeyColumnAndATableLevelCompositePrimaryKey() { + val db = newDatabase() + assertFailsWith { + db.run("CREATE TABLE bad (id INTEGER PRIMARY KEY, other INTEGER, PRIMARY KEY (id, other))") + } + } + + @Test + fun aCompositePrimaryKeyReferencingAnUnknownColumnIsRejectedWithAClearError() { + val db = newDatabase() + val failure = assertFailsWith { + db.run("CREATE TABLE bad (order_id INTEGER, line_no INTEGER, PRIMARY KEY (order_id, missing_column))") + } + assertTrue(failure.message!!.contains("missing_column")) + } + + @Test + fun compositePrimaryKeyDataAndUniquenessSurviveClosingAndReopeningTheCatalog() { + val db = newDatabase() + db.seedOrderLines() + db.reopen() + + assertFailsWith { db.run("INSERT INTO order_lines VALUES (1, 1, 'duplicate')") } + val result = db.select("SELECT sku FROM order_lines WHERE order_id = 1 AND line_no = 2") + assertEquals(listOf(listOf(text("gadget"))), result.rows) + val table = db.catalog.table("order_lines") + assertEquals(0, table.scanCount, "the point lookup above must not have fallen back to a full scan after reopening") + } + + private fun Database.seedDepartments() { + run("CREATE TABLE departments (id INTEGER PRIMARY KEY, name TEXT NOT NULL, budget INTEGER)") + run("CREATE TABLE employees (id INTEGER PRIMARY KEY, name TEXT NOT NULL, department_id INTEGER, salary INTEGER)") + run("INSERT INTO departments VALUES (1, 'eng', 1000), (2, 'sales', 500), (3, 'ops', 0)") + run( + "INSERT INTO employees VALUES " + + "(1, 'ada', 1, 120), (2, 'linus', 1, 90), (3, 'grace', 2, 80), (4, 'guido', 2, 70)" + ) + } + + @Test + fun scalarSubqueryComparesAgainstAnUncorrelatedAggregate() { + val db = newDatabase() + db.seedDepartments() + val result = db.select("SELECT name FROM employees WHERE salary > (SELECT AVG(salary) FROM employees) ORDER BY name") + assertEquals(listOf(listOf(text("ada"))), result.rows) + } + + @Test + fun scalarSubqueryReturningNoRowsEvaluatesToNull() { + val db = newDatabase() + db.seedDepartments() + val result = db.select("SELECT (SELECT id FROM departments WHERE name = 'nope') FROM departments WHERE id = 1") + assertEquals(listOf(listOf(Value.NullValue)), result.rows) + } + + @Test + fun scalarSubqueryReturningMoreThanOneRowFails() { + val db = newDatabase() + db.seedDepartments() + val failure = assertFailsWith { + db.select("SELECT (SELECT salary FROM employees) FROM departments WHERE id = 1") + } + assertTrue(failure.message!!.contains("more than one row")) + } + + @Test + fun scalarSubqueryReturningMoreThanOneColumnFails() { + val db = newDatabase() + db.seedDepartments() + val failure = assertFailsWith { + db.select("SELECT (SELECT id, name FROM departments) FROM departments WHERE id = 1") + } + assertTrue(failure.message!!.contains("exactly one column")) + } + + @Test + fun correlatedScalarSubqueryInProjectionCountsPerOuterRow() { + val db = newDatabase() + db.seedDepartments() + val result = db.select( + "SELECT d.name, (SELECT COUNT(*) FROM employees e WHERE e.department_id = d.id) " + + "FROM departments d ORDER BY d.id" + ) + assertEquals( + listOf(listOf(text("eng"), int(2)), listOf(text("sales"), int(2)), listOf(text("ops"), int(0))), + result.rows + ) + } + + @Test + fun correlatedScalarSubqueryInWhereComparesAgainstTheEmployeesOwnDepartmentAverage() { + val db = newDatabase() + db.seedDepartments() + val result = db.select( + "SELECT name FROM employees e WHERE e.salary > " + + "(SELECT AVG(salary) FROM employees e2 WHERE e2.department_id = e.department_id) ORDER BY name" + ) + assertEquals(listOf(listOf(text("ada")), listOf(text("grace"))), result.rows) + } + + @Test + fun inSubqueryMatchesAnUncorrelatedResultSet() { + val db = newDatabase() + db.seedDepartments() + val result = db.select("SELECT name FROM departments WHERE id IN (SELECT department_id FROM employees) ORDER BY name") + assertEquals(listOf(listOf(text("eng")), listOf(text("sales"))), result.rows) + } + + @Test + fun notInSubqueryExcludesMatchingRows() { + val db = newDatabase() + db.seedDepartments() + val result = db.select("SELECT name FROM departments WHERE id NOT IN (SELECT department_id FROM employees)") + assertEquals(listOf(listOf(text("ops"))), result.rows) + } + + @Test + fun notInSubqueryWithANullInTheResultSetExcludesEverythingByThreeValuedLogic() { + val db = newDatabase() + db.run("CREATE TABLE d (id INTEGER)") + db.run("CREATE TABLE e (dept INTEGER)") + db.run("INSERT INTO d VALUES (1), (2)") + db.run("INSERT INTO e VALUES (1), (NULL)") + val result = db.select("SELECT id FROM d WHERE id NOT IN (SELECT dept FROM e)") + assertEquals(emptyList(), result.rows) + } + + @Test + fun inSubqueryRequiresExactlyOneColumn() { + val db = newDatabase() + db.seedDepartments() + val failure = assertFailsWith { + db.select("SELECT name FROM departments WHERE id IN (SELECT id, name FROM departments)") + } + assertTrue(failure.message!!.contains("exactly one column")) + } +