test(sql): cover DiskExecutorTemporaryTableTest and tempFile
This commit is contained in:
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())
|
||||
|
||||
Reference in new issue
Block a user