test(sql): cover DiskExecutorTemporaryTableTest and tempFile

This commit is contained in:
dualfroz committed 2026-09-12 16:02:37 +02:00
1 parent 9c2ffa9e47
commit 0b14ca4caa
1 file changed
+158
@@ -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<StatementResult> = 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<KestrelException> { 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<KestrelException> { 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<KestrelException> { db.run("CREATE TEMP TABLE regular (id INTEGER)") }
db.run("CREATE TEMP TABLE scratch (id INTEGER)")
assertFailsWith<KestrelException> { db.run("CREATE TABLE scratch (id INTEGER)") }
assertFailsWith<KestrelException> { db.run("CREATE TEMP TABLE scratch (id INTEGER)") }
db.run("CREATE VIEW a_view AS SELECT * FROM regular")
assertFailsWith<KestrelException> { 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<KestrelException> { db.run("INSERT INTO scratch VALUES (1, 'b', 1)") }
assertFailsWith<KestrelException> { db.run("INSERT INTO scratch VALUES (2, 'a', 1)") }
assertFailsWith<KestrelException> { db.run("INSERT INTO scratch (id, sku) VALUES (3, 'c')") }
assertFailsWith<KestrelException> { 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())