From 0b14ca4caa911de719a22dcae57677966e9d3a8f Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 16:02:37 +0200 Subject: [PATCH] test(sql): cover DiskExecutorTemporaryTableTest and tempFile --- .../sql/DiskExecutorTemporaryTableTest.kt | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/test/kotlin/kestrel/store/sql/DiskExecutorTemporaryTableTest.kt diff --git a/src/test/kotlin/kestrel/store/sql/DiskExecutorTemporaryTableTest.kt b/src/test/kotlin/kestrel/store/sql/DiskExecutorTemporaryTableTest.kt new file mode 100644 index 0000000..d77f42b --- /dev/null +++ b/src/test/kotlin/kestrel/store/sql/DiskExecutorTemporaryTableTest.kt @@ -0,0 +1,158 @@ +package kestrel.store.sql + +import kestrel.KestrelException +import kestrel.engine.CommitResult +import kestrel.engine.CreateTableResult +import kestrel.engine.DeleteResult +import kestrel.engine.DropTableResult +import kestrel.engine.InsertResult +import kestrel.engine.RollbackResult +import kestrel.engine.RollbackToSavepointResult +import kestrel.engine.SavepointResult +import kestrel.engine.SelectResult +import kestrel.engine.StatementResult +import kestrel.engine.UpdateResult +import kestrel.sql.lexer.Lexer +import kestrel.sql.parser.Parser +import kestrel.storage.Value +import java.io.File +import java.nio.file.Files +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * `CREATE TEMP TABLE`/`CREATE TEMPORARY TABLE` on [DiskExecutor] - the disk-backed counterpart of + * the in-memory engine's own feature (see [kestrel.ExecutorTest]'s temp-table cases, added by + * session #63), closing the scope-cut that session left named: "TEMP TABLE na silniku dyskowym + * pozostaje swiadomym scope-cut... tabela tymczasowa na silniku dyskowym zylaby WYLACZNIE w + * pamieci, nigdy dotykajac pager/WAL wcale". A temp table here is a plain [kestrel.storage.Table] + * living only in [DiskCatalog] (see its own KDoc), so it participates in WHERE/JOIN/subqueries/ + * ORDER BY exactly like a real table via [DiskExecutor.resolveFromSource]'s `CteRowSource` reuse, + * but FOREIGN KEY, `CREATE INDEX`, `CREATE TRIGGER`, and `INSERT ... ON CONFLICT` on one are all + * documented, explicit scope-cuts (see the rejection tests below) rather than a silent gap. + */ +class DiskExecutorTemporaryTableTest { + private fun tempFile(prefix: String): File = Files.createTempFile(prefix, ".db").toFile().also { it.deleteOnExit() } + + private fun int(value: Long) = Value.IntegerValue(value) + private fun text(value: String) = Value.TextValue(value) + + private class Database(val dataFile: File, val walFile: File) { + var catalog = DiskCatalog.open(dataFile, walFile) + private set + var executor = DiskExecutor(catalog) + private set + + fun run(sql: String): List = Parser(Lexer(sql).tokenize()).parseProgram().map { executor.execute(it) } + fun select(sql: String): SelectResult = run(sql).single() as SelectResult + fun rowCount(sql: String): Int = select(sql).rows.size + + /** Closes and reopens the catalog against the same files, simulating a fresh process. */ + fun reopen() { + catalog.close() + catalog = DiskCatalog.open(dataFile, walFile) + executor = DiskExecutor(catalog) + } + } + + private fun newDatabase(): Database = Database(tempFile("kestrel-disk-temp-data"), tempFile("kestrel-disk-temp-wal")) + + @Test + fun createTempTableStartsEmptyAndSupportsFullCrud() { + val db = newDatabase() + assertEquals( + CreateTableResult("scratch"), + db.run("CREATE TEMP TABLE scratch (id INTEGER PRIMARY KEY, note TEXT)").single() + ) + assertEquals(SelectResult(listOf("id", "note"), emptyList()), db.select("SELECT * FROM scratch")) + + assertEquals(InsertResult(2, null), db.run("INSERT INTO scratch VALUES (1, 'a'), (2, 'b')").single()) + assertEquals(2, db.rowCount("SELECT * FROM scratch")) + + assertEquals(UpdateResult(1, null), db.run("UPDATE scratch SET note = 'z' WHERE id = 2").single()) + assertEquals(listOf(int(2), text("z")), db.select("SELECT * FROM scratch WHERE id = 2").rows.single()) + + assertEquals(DeleteResult(1, null), db.run("DELETE FROM scratch WHERE id = 1").single()) + assertEquals(1, db.rowCount("SELECT * FROM scratch")) + + assertEquals(DropTableResult("scratch"), db.run("DROP TABLE scratch").single()) + assertFailsWith { db.select("SELECT * FROM scratch") } + } + + @Test + fun createTemporaryTableKeywordIsAnAliasForTemp() { + val db = newDatabase() + db.run("CREATE TEMPORARY TABLE scratch (id INTEGER)") + db.run("INSERT INTO scratch VALUES (1)") + assertEquals(1, db.rowCount("SELECT * FROM scratch")) + } + + @Test + fun aTempTableNeverGetsADirectoryEntrySoItDoesNotSurviveAReopen() { + val db = newDatabase() + db.run("CREATE TABLE regular (id INTEGER PRIMARY KEY)") + db.run("CREATE TEMP TABLE scratch (id INTEGER)") + db.run("INSERT INTO regular VALUES (1)") + db.run("INSERT INTO scratch VALUES (99)") + assertEquals(setOf("regular", "scratch"), db.catalog.tableNames.toSet()) + + db.reopen() + + assertEquals(listOf("regular"), db.catalog.tableNames) + assertEquals(1, db.rowCount("SELECT * FROM regular")) + assertFailsWith { db.select("SELECT * FROM scratch") } + } + + @Test + fun tableNamesListsBothRegularAndTemporaryTables() { + val db = newDatabase() + db.run("CREATE TABLE regular (id INTEGER)") + db.run("CREATE TEMP TABLE scratch (id INTEGER)") + assertEquals(setOf("regular", "scratch"), db.catalog.tableNames.toSet()) + } + + @Test + fun aTemporaryTableCannotShadowOrCollideWithAnExistingTableOrView() { + val db = newDatabase() + db.run("CREATE TABLE regular (id INTEGER)") + assertFailsWith { db.run("CREATE TEMP TABLE regular (id INTEGER)") } + + db.run("CREATE TEMP TABLE scratch (id INTEGER)") + assertFailsWith { db.run("CREATE TABLE scratch (id INTEGER)") } + assertFailsWith { db.run("CREATE TEMP TABLE scratch (id INTEGER)") } + + db.run("CREATE VIEW a_view AS SELECT * FROM regular") + assertFailsWith { db.run("CREATE TEMP TABLE a_view (id INTEGER)") } + } + + @Test + fun notNullPrimaryKeyUniqueAndCheckAreAllEnforcedOnATemporaryTable() { + val db = newDatabase() + db.run( + "CREATE TEMP TABLE scratch (" + + "id INTEGER PRIMARY KEY, sku TEXT UNIQUE, qty INTEGER NOT NULL, CHECK (qty >= 0))" + ) + db.run("INSERT INTO scratch VALUES (1, 'a', 5)") + + assertFailsWith { db.run("INSERT INTO scratch VALUES (1, 'b', 1)") } + assertFailsWith { db.run("INSERT INTO scratch VALUES (2, 'a', 1)") } + assertFailsWith { db.run("INSERT INTO scratch (id, sku) VALUES (3, 'c')") } + assertFailsWith { db.run("INSERT INTO scratch VALUES (4, 'd', -1)") } + + // Nothing from any failed multi-row batch partially landed. + assertEquals(1, db.rowCount("SELECT * FROM scratch")) + } + + @Test + fun defaultAndGeneratedColumnsAreEvaluatedOnATemporaryTable() { + val db = newDatabase() + db.run( + "CREATE TEMP TABLE scratch (" + + "id INTEGER, price INTEGER DEFAULT 10, doubled INTEGER GENERATED ALWAYS AS (price * 2) STORED)" + ) + db.run("INSERT INTO scratch (id) VALUES (1)") + assertEquals(listOf(int(1), int(10), int(20)), db.select("SELECT * FROM scratch").rows.single()) +