test(sql): cover DiskExecutorGeneratedColumnTest and tempFile
This commit is contained in:
1 parent
aae2ff034b
commit
108a2845bc
1 file changed
+162
@@ -0,0 +1,162 @@
|
||||
package kestrel.store.sql
|
||||
|
||||
import kestrel.KestrelException
|
||||
import kestrel.engine.MergeResult
|
||||
import kestrel.engine.SelectResult
|
||||
import kestrel.engine.StatementResult
|
||||
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.assertTrue
|
||||
|
||||
/**
|
||||
* `GENERATED ALWAYS AS (expr) STORED` through [DiskExecutor] against a real [DiskCatalog] backed by
|
||||
* temp files - scenarios ported 1:1 from [kestrel.GeneratedColumnTest] (the in-memory engine's
|
||||
* equivalent) wherever they translate, plus [generatedColumnSurvivesAReopen] proving
|
||||
* [DirectoryEntryCodec] round-trips [kestrel.catalog.Column.generated] the same way it already did
|
||||
* [kestrel.catalog.Column.default].
|
||||
*/
|
||||
class DiskExecutorGeneratedColumnTest {
|
||||
private fun tempFile(prefix: String): File = Files.createTempFile(prefix, ".db").toFile().also { it.deleteOnExit() }
|
||||
|
||||
private fun int(value: Long) = Value.IntegerValue(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 exec(sql: String): StatementResult = run(sql).single()
|
||||
fun select(sql: String): SelectResult = exec(sql) as SelectResult
|
||||
|
||||
fun reopen() {
|
||||
catalog.close()
|
||||
catalog = DiskCatalog.open(dataFile, walFile)
|
||||
executor = DiskExecutor(catalog)
|
||||
}
|
||||
}
|
||||
|
||||
private fun newDatabase(): Database = Database(tempFile("kestrel-disk-generated-data"), tempFile("kestrel-disk-generated-wal"))
|
||||
|
||||
@Test
|
||||
fun generatedColumnIsComputedWhenOmittedFromInsert() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED)")
|
||||
db.run("INSERT INTO t (a) VALUES (5)")
|
||||
assertEquals(listOf(listOf(int(5), int(10))), db.select("SELECT a, b FROM t").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertRejectsAnExplicitValueForAGeneratedColumn() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED)")
|
||||
val error = assertFailsWith<KestrelException> { db.run("INSERT INTO t (a, b) VALUES (5, 99)") }
|
||||
assertTrue(error.message!!.contains("GENERATED"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updatingADependencyColumnRecomputesTheGeneratedColumn() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED)")
|
||||
db.run("INSERT INTO t (a) VALUES (5)")
|
||||
db.run("UPDATE t SET a = 9")
|
||||
assertEquals(listOf(listOf(int(18))), db.select("SELECT b FROM t").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateRejectsAnExplicitValueForAGeneratedColumn() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED)")
|
||||
db.run("INSERT INTO t (a) VALUES (5)")
|
||||
assertFailsWith<KestrelException> { db.run("UPDATE t SET b = 1") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onConflictDoUpdateRecomputesTheGeneratedColumn() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED)")
|
||||
db.run("INSERT INTO t (id, a) VALUES (1, 5)")
|
||||
db.run("INSERT INTO t (id, a) VALUES (1, 20) ON CONFLICT (id) DO UPDATE SET a = EXCLUDED.a")
|
||||
assertEquals(listOf(listOf(int(40))), db.select("SELECT b FROM t").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mergeInsertComputesTheGeneratedColumn() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED)")
|
||||
db.run("CREATE TABLE s (id INTEGER, a INTEGER)")
|
||||
db.run("INSERT INTO s VALUES (1, 5)")
|
||||
val result = db.exec(
|
||||
"MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN INSERT (id, a) VALUES (s.id, s.a)"
|
||||
) as MergeResult
|
||||
assertEquals(MergeResult(1, 0, 0), result)
|
||||
assertEquals(listOf(listOf(int(10))), db.select("SELECT b FROM t").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mergeUpdateRecomputesTheGeneratedColumn() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED)")
|
||||
db.run("CREATE TABLE s (id INTEGER, a INTEGER)")
|
||||
db.run("INSERT INTO t (id, a) VALUES (1, 5)")
|
||||
db.run("INSERT INTO s VALUES (1, 9)")
|
||||
db.exec("MERGE INTO t USING s ON t.id = s.id WHEN MATCHED THEN UPDATE SET a = s.a")
|
||||
assertEquals(listOf(listOf(int(18))), db.select("SELECT b FROM t").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsGeneratedExpressionReferencingANonexistentColumn() {
|
||||
val db = newDatabase()
|
||||
val error = assertFailsWith<KestrelException> {
|
||||
db.run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (missing) STORED)")
|
||||
}
|
||||
assertTrue(error.message!!.contains("no such column"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsAGeneratedColumnReferencingAnotherGeneratedColumn() {
|
||||
val db = newDatabase()
|
||||
val error = assertFailsWith<KestrelException> {
|
||||
db.run(
|
||||
"CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED, " +
|
||||
"c INTEGER GENERATED ALWAYS AS (b + 1) STORED)"
|
||||
)
|
||||
}
|
||||
assertTrue(error.message!!.contains("GENERATED"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun renameColumnPropagatesIntoADependentGeneratedExpression() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED)")
|
||||
db.run("ALTER TABLE t RENAME COLUMN a TO a2")
|
||||
db.run("INSERT INTO t (a2) VALUES (5)")
|
||||
assertEquals(listOf(listOf(int(10))), db.select("SELECT b FROM t").rows)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dropColumnRestrictsWhenAGeneratedColumnDependsOnIt() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED)")
|
||||
val error = assertFailsWith<KestrelException> { db.run("ALTER TABLE t DROP COLUMN a") }
|
||||
assertTrue(error.message!!.contains("GENERATED"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dropColumnCascadeRemovesTheDependentGeneratedColumnToo() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE t (a INTEGER, b INTEGER GENERATED ALWAYS AS (a * 2) STORED, c TEXT)")
|
||||
db.run("INSERT INTO t (a, c) VALUES (5, 'x')")
|
||||
db.run("ALTER TABLE t DROP COLUMN a CASCADE")
|
||||
val result = db.select("SELECT * FROM t")
|
||||
assertEquals(listOf("c"), result.columns)
|
||||
assertEquals(listOf(listOf(Value.TextValue("x"))), result.rows)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user