From 4682431a800050be5462cdad5e5e41b9387eca86 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Thu, 10 Sep 2026 19:27:21 +0200 Subject: [PATCH] test: cover conflictDoUpdateEnforcesCheckConstraintsOnTheResultingRow --- src/test/kotlin/kestrel/UpsertTest.kt | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/test/kotlin/kestrel/UpsertTest.kt b/src/test/kotlin/kestrel/UpsertTest.kt index ffd56f9..39cc28a 100644 --- a/src/test/kotlin/kestrel/UpsertTest.kt +++ b/src/test/kotlin/kestrel/UpsertTest.kt @@ -160,3 +160,75 @@ class UpsertTest { assertTrue(error.message!!.contains("NOT NULL"), error.message!!) } + @Test + fun conflictDoUpdateEnforcesCheckConstraintsOnTheResultingRow() { + run( + "CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER CHECK (balance >= 0))" + ) + run("INSERT INTO accounts VALUES (1, 100)") + val error = assertFailsWith { + run("INSERT INTO accounts VALUES (1, 100) ON CONFLICT (id) DO UPDATE SET balance = -5") + } + assertTrue(error.message!!.contains("CHECK constraint"), error.message!!) + } + + @Test + fun conflictDoUpdateRunsForeignKeyValidationOnTheUpdatedRow() { + run("CREATE TABLE departments (id INTEGER PRIMARY KEY)") + run("INSERT INTO departments VALUES (1)") + run( + "CREATE TABLE employees (id INTEGER PRIMARY KEY, dept INTEGER REFERENCES departments(id))" + ) + run("INSERT INTO employees VALUES (1, 1)") + val error = assertFailsWith { + run("INSERT INTO employees VALUES (1, 1) ON CONFLICT (id) DO UPDATE SET dept = 999") + } + assertTrue(error.message!!.contains("FOREIGN KEY"), error.message!!) + } + + @Test + fun conflictDoUpdateCascadesToChildTablesWhenTheReferencedKeyChanges() { + run("CREATE TABLE departments (id INTEGER PRIMARY KEY)") + run("INSERT INTO departments VALUES (1)") + run( + "CREATE TABLE employees (id INTEGER PRIMARY KEY, dept INTEGER REFERENCES departments(id) ON UPDATE CASCADE)" + ) + run("INSERT INTO employees VALUES (1, 1)") + insertCount("INSERT INTO departments VALUES (1) ON CONFLICT (id) DO UPDATE SET id = 2") + assertEquals(Value.IntegerValue(2), select("SELECT dept FROM employees WHERE id = 1").rows[0][0]) + } + + @Test + fun compositePrimaryKeyConflictTargetIsRecognised() { + run( + "CREATE TABLE grades (student INTEGER, course INTEGER, score INTEGER, PRIMARY KEY (student, course))" + ) + run("INSERT INTO grades VALUES (1, 10, 50)") + val count = insertCount( + "INSERT INTO grades VALUES (1, 10, 90) ON CONFLICT (student, course) DO UPDATE SET score = excluded.score" + ) + assertEquals(1, count) + assertEquals(Value.IntegerValue(90), select("SELECT score FROM grades WHERE student = 1 AND course = 10").rows[0][0]) + } + + @Test + fun conflictDoUpdateDefaultKeywordFallsBackToColumnDefault() { + run("CREATE TABLE t (id INTEGER PRIMARY KEY, note TEXT DEFAULT 'fresh')") + run("INSERT INTO t VALUES (1, 'stale')") + insertCount("INSERT INTO t VALUES (1, 'stale') ON CONFLICT (id) DO UPDATE SET note = DEFAULT") + assertEquals("fresh", (select("SELECT note FROM t WHERE id = 1").rows[0][0] as Value.TextValue).value) + } + + @Test + fun tableStateAfterAConflictResolutionStillDumpsAndReplaysCleanly() { + seed() + run("INSERT INTO accounts VALUES (2, 'linus', 200) ON CONFLICT (id) DO UPDATE SET balance = excluded.balance") + val dumped = kestrel.persistence.SqlWriter.dump(catalog) + val replayCatalog = Catalog() + val replayExecutor = Executor(replayCatalog) + Parser(Lexer(dumped).tokenize()).parseProgram().forEach { replayExecutor.execute(it) } + val replaySelect = Parser(Lexer("SELECT * FROM accounts").tokenize()).parseProgram() + .map { replayExecutor.execute(it) }.single() as SelectResult + assertEquals(2, replaySelect.rows.size) + } +}