diff --git a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt index 5f8edb9..6fc2ab0 100644 --- a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt +++ b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt @@ -153,3 +153,160 @@ class DiskExecutorTest { assertTrue(listOf(int(1), text("ada"), int(36)) in result.rows) } + @Test + fun whereFiltersRowsUsingTheSharedEvaluator() { + val db = newDatabase() + db.seedUsers() + val result = db.select("SELECT name FROM users WHERE age > 40") + assertEquals(setOf(text("linus"), text("grace")), result.rows.map { it.single() }.toSet()) + } + + @Test + fun orderByAndLimitApplyAfterFilteringAndBeforeProjection() { + val db = newDatabase() + db.seedUsers() + val result = db.select("SELECT name, age FROM users ORDER BY age DESC LIMIT 2") + assertEquals(listOf(listOf(text("grace"), int(85)), listOf(text("linus"), int(54))), result.rows) + } + + @Test + fun offsetSkipsLeadingRowsWithoutALimit() { + val db = newDatabase() + db.seedUsers() + val result = db.select("SELECT name FROM users ORDER BY age ASC OFFSET 1") + assertEquals(listOf(listOf(text("linus")), listOf(text("grace"))), result.rows) + } + + @Test + fun limitAndOffsetTogetherPageThroughTheOrderedResult() { + val db = newDatabase() + db.seedUsers() + val result = db.select("SELECT name FROM users ORDER BY age ASC LIMIT 1 OFFSET 1") + assertEquals(listOf(listOf(text("linus"))), result.rows) + } + + @Test + fun offsetWrittenBeforeLimitStillAppliesLimitAfterTheSkip() { + val db = newDatabase() + db.seedUsers() + val result = db.select("SELECT name FROM users ORDER BY age ASC OFFSET 1 LIMIT 1") + assertEquals(listOf(listOf(text("linus"))), result.rows) + } + + @Test + fun offsetPastTheEndOfTheResultReturnsNoRows() { + val db = newDatabase() + db.seedUsers() + val result = db.select("SELECT name FROM users ORDER BY age ASC OFFSET 100") + assertEquals(emptyList>(), result.rows) + } + + @Test + fun offsetSurvivesCloseAndReopen() { + val db = newDatabase() + db.seedUsers() + db.reopen() + val result = db.select("SELECT name FROM users ORDER BY age ASC LIMIT 1 OFFSET 1") + assertEquals(listOf(listOf(text("linus"))), result.rows) + } + + @Test + fun expressionProjectionEvaluatesFunctionsAndUsesAliasesOrGeneratedLabels() { + val db = newDatabase() + db.seedUsers() + val result = db.select("SELECT UPPER(name) AS loud, age + 1 FROM users WHERE id = 1") + assertEquals(listOf("loud", "column2"), result.columns) + assertEquals(listOf(listOf(text("ADA"), int(37))), result.rows) + } + + @Test + fun updateAppliesExpressionsAgainstTheOldRowAndReportsMatchedCount() { + val db = newDatabase() + db.seedUsers() + val updated = db.run("UPDATE users SET age = age + 1 WHERE age > 40").single() + assertEquals(UpdateResult(2), updated) + val ages = db.select("SELECT name, age FROM users ORDER BY name").rows.associate { (it[0] as Value.TextValue).value to it[1] } + assertEquals(mapOf("ada" to int(36), "linus" to int(55), "grace" to int(86)), ages) + } + + @Test + fun updateSetToDefaultKeywordResetsTheColumn() { + val db = newDatabase() + db.seedUsers() + db.run("UPDATE users SET age = DEFAULT WHERE id = 2") + assertEquals(int(0), db.select("SELECT age FROM users WHERE id = 2").rows.single().single()) + } + + @Test + fun deleteRemovesMatchingRowsAndReportsCount() { + val db = newDatabase() + db.seedUsers() + val deleted = db.run("DELETE FROM users WHERE age > 40").single() + assertEquals(DeleteResult(2), deleted) + assertEquals(listOf(listOf(int(1), text("ada"), int(36))), db.select("SELECT * FROM users").rows) + } + + @Test + fun dropTableRemovesItFromTheCatalog() { + val db = newDatabase() + db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)") + val dropped = db.run("DROP TABLE users").single() + assertEquals(DropTableResult("users"), dropped) + assertFailsWith { db.select("SELECT * FROM users") } + } + + @Test + fun aFailedStatementInAMultiRowInsertLeavesNoPartialRowsBehind() { + val db = newDatabase() + db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)") + // The second row duplicates the first row's primary key: DiskTable rejects it mid-batch, + // so the whole INSERT statement (including the already-applied first row) must abort. + assertFailsWith { + db.run("INSERT INTO users VALUES (1, 'ada'), (1, 'ada-again')") + } + assertEquals(emptyList>(), db.select("SELECT * FROM users").rows) + } + + @Test + fun notNullViolationOnUpdateAbortsTheWholeStatement() { + val db = newDatabase() + db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)") + db.run("INSERT INTO users VALUES (1, 'ada'), (2, 'linus')") + assertFailsWith { db.run("UPDATE users SET name = NULL") } + // Neither row's name was actually cleared, because the failure aborted the transaction. + assertEquals(setOf(text("ada"), text("linus")), db.select("SELECT name FROM users").rows.map { it.single() }.toSet()) + } + + @Test + fun integerLiteralIsCoercedToRealForARealColumn() { + val db = newDatabase() + db.run("CREATE TABLE readings (id INTEGER PRIMARY KEY, value REAL NOT NULL)") + db.run("INSERT INTO readings VALUES (1, 5)") + assertEquals(Value.RealValue(5.0), db.select("SELECT value FROM readings").rows.single().single()) + } + + @Test + fun textLiteralIntoAnIntegerColumnIsRejectedWithATypeMismatch() { + val db = newDatabase() + db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)") + assertFailsWith { db.run("INSERT INTO users VALUES ('x', 'ada')") } + } + + @Test + fun dataSurvivesClosingAndReopeningTheCatalogInWhatActsAsAFreshProcess() { + val db = newDatabase() + db.seedUsers() + db.run("UPDATE users SET age = 100 WHERE id = 1") + db.reopen() + val ages = db.select("SELECT name, age FROM users ORDER BY name").rows + assertEquals(listOf(listOf(text("ada"), int(100)), listOf(text("grace"), int(85)), listOf(text("linus"), int(54))), ages) + } + + private fun Database.seedOrders() { + run("CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT NOT NULL)") + run("INSERT INTO customers VALUES (1, 'ada'), (2, 'linus'), (3, 'grace')") + run("CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER NOT NULL, amount INTEGER NOT NULL)") + // grace (id 3) has no orders, so LEFT JOIN has something to pad and INNER JOIN has something to drop. + run("INSERT INTO orders VALUES (1, 1, 100), (2, 1, 50), (3, 2, 200)") + } +