From f1061c5709200ac89d7a886af2ac8a34fa258ce3 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 21:40:07 +0200 Subject: [PATCH] test(sql): cover DiskExecutorOnConflictTest and tempFile --- .../store/sql/DiskExecutorOnConflictTest.kt | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 src/test/kotlin/kestrel/store/sql/DiskExecutorOnConflictTest.kt diff --git a/src/test/kotlin/kestrel/store/sql/DiskExecutorOnConflictTest.kt b/src/test/kotlin/kestrel/store/sql/DiskExecutorOnConflictTest.kt new file mode 100644 index 0000000..531841b --- /dev/null +++ b/src/test/kotlin/kestrel/store/sql/DiskExecutorOnConflictTest.kt @@ -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 = 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 { + 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!!) + } +