test(sql): cover DiskExecutorOnConflictTest and tempFile
This commit is contained in:
1 parent
4c1ba85a21
commit
f1061c5709
1 file changed
+163
@@ -0,0 +1,163 @@
|
|||||||
|
package kestrel.store.sql
|
||||||
|
|
||||||
|
import kestrel.KestrelException
|
||||||
|
import kestrel.engine.InsertResult
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `INSERT ... ON CONFLICT (...) DO NOTHING|DO UPDATE` through [DiskExecutor] against a real
|
||||||
|
* [DiskCatalog] backed by temp files - see [DiskExecutorTest] for the general end-to-end pattern
|
||||||
|
* this reuses. Scenarios are ported 1:1 from [kestrel.UpsertTest] (the in-memory engine's
|
||||||
|
* equivalent) wherever they translate; [conflictResolutionSurvivesAReopen] replaces that suite's
|
||||||
|
* dump/replay test (this executor has no [kestrel.persistence.SqlWriter] equivalent - durability
|
||||||
|
* here means surviving a real close/reopen of the backing files) and is new to this file.
|
||||||
|
*/
|
||||||
|
class DiskExecutorOnConflictTest {
|
||||||
|
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 exec(sql: String): StatementResult = run(sql).single()
|
||||||
|
fun select(sql: String): SelectResult = exec(sql) as SelectResult
|
||||||
|
fun insertCount(sql: String): Int = (exec(sql) as InsertResult).count
|
||||||
|
|
||||||
|
fun reopen() {
|
||||||
|
catalog.close()
|
||||||
|
catalog = DiskCatalog.open(dataFile, walFile)
|
||||||
|
executor = DiskExecutor(catalog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newDatabase(): Database = Database(tempFile("kestrel-disk-onconflict-data"), tempFile("kestrel-disk-onconflict-wal"))
|
||||||
|
|
||||||
|
private fun Database.seed() {
|
||||||
|
run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT NOT NULL, balance INTEGER NOT NULL)")
|
||||||
|
run("INSERT INTO accounts VALUES (1, 'ada', 100)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun noConflictInsertsNormallyAndReportsOneRow() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
val count = db.insertCount("INSERT INTO accounts VALUES (2, 'linus', 200) ON CONFLICT (id) DO NOTHING")
|
||||||
|
assertEquals(1, count)
|
||||||
|
assertEquals(2, db.select("SELECT * FROM accounts").rows.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun conflictOnPrimaryKeyDoesNothingAndReportsZero() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
val count = db.insertCount("INSERT INTO accounts VALUES (1, 'ignored', 999) ON CONFLICT (id) DO NOTHING")
|
||||||
|
assertEquals(0, count)
|
||||||
|
assertEquals(int(100), db.select("SELECT balance FROM accounts WHERE id = 1").rows[0][0])
|
||||||
|
assertEquals("ada", (db.select("SELECT name FROM accounts WHERE id = 1").rows[0][0] as Value.TextValue).value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun conflictWithBareOnConflictWithNoTargetCatchesAnyUniqueViolation() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
db.run("CREATE UNIQUE INDEX idxName ON accounts (name)")
|
||||||
|
val count = db.insertCount("INSERT INTO accounts VALUES (2, 'ada', 300) ON CONFLICT DO NOTHING")
|
||||||
|
assertEquals(0, count)
|
||||||
|
assertEquals(1, db.select("SELECT * FROM accounts").rows.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun conflictDoUpdateAppliesTheAssignments() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
db.insertCount("INSERT INTO accounts VALUES (1, 'ada', 999) ON CONFLICT (id) DO UPDATE SET balance = 999")
|
||||||
|
assertEquals(int(999), db.select("SELECT balance FROM accounts WHERE id = 1").rows[0][0])
|
||||||
|
assertEquals(1, db.select("SELECT * FROM accounts").rows.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun conflictDoUpdateCanReferenceExcludedForTheProposedRow() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
db.insertCount("INSERT INTO accounts VALUES (1, 'ada', 50) ON CONFLICT (id) DO UPDATE SET balance = accounts.balance + excluded.balance")
|
||||||
|
assertEquals(int(150), db.select("SELECT balance FROM accounts WHERE id = 1").rows[0][0])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun conflictDoUpdateWithFailingWhereBehavesLikeDoNothingForThatRow() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
val count = db.insertCount(
|
||||||
|
"INSERT INTO accounts VALUES (1, 'ada', 999) ON CONFLICT (id) DO UPDATE SET balance = 999 WHERE excluded.balance < 0"
|
||||||
|
)
|
||||||
|
assertEquals(0, count)
|
||||||
|
assertEquals(int(100), db.select("SELECT balance FROM accounts WHERE id = 1").rows[0][0])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun conflictDoUpdateWithPassingWhereApplies() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
val count = db.insertCount(
|
||||||
|
"INSERT INTO accounts VALUES (1, 'ada', 999) ON CONFLICT (id) DO UPDATE SET balance = 999 WHERE excluded.balance > 0"
|
||||||
|
)
|
||||||
|
assertEquals(1, count)
|
||||||
|
assertEquals(int(999), db.select("SELECT balance FROM accounts WHERE id = 1").rows[0][0])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun multiRowInsertHandlesEachRowsConflictIndependently() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
val count = db.insertCount(
|
||||||
|
"""
|
||||||
|
INSERT INTO accounts VALUES (1, 'ada', 5), (2, 'linus', 200), (3, 'grace', 300)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET balance = accounts.balance + excluded.balance
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
assertEquals(3, count)
|
||||||
|
assertEquals(int(105), db.select("SELECT balance FROM accounts WHERE id = 1").rows[0][0])
|
||||||
|
assertEquals(int(200), db.select("SELECT balance FROM accounts WHERE id = 2").rows[0][0])
|
||||||
|
assertEquals(int(300), db.select("SELECT balance FROM accounts WHERE id = 3").rows[0][0])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun laterRowInTheSameStatementSeesAnEarlierRowsInsert() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, hits INTEGER NOT NULL)")
|
||||||
|
val count = db.insertCount(
|
||||||
|
"""
|
||||||
|
INSERT INTO t VALUES (1, 1), (1, 1)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET hits = t.hits + excluded.hits
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
assertEquals(2, count)
|
||||||
|
assertEquals(int(2), db.select("SELECT hits FROM t WHERE id = 1").rows[0][0])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun conflictTargetMustMatchAnExistingUniqueOrPrimaryKey() {
|
||||||
|
val db = newDatabase()
|
||||||
|
db.seed()
|
||||||
|
val error = assertFailsWith<KestrelException> {
|
||||||
|
db.run("INSERT INTO accounts VALUES (2, 'linus', 200) ON CONFLICT (name) DO NOTHING")
|
||||||
|
}
|
||||||
|
assertTrue(error.message!!.contains("no unique or primary key constraint"), error.message!!)
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user