test(sql): cover conflictTargetMatchesADeclaredUniqueConstraintNotJus...

This commit is contained in:
dualfroz committed 2026-09-11 22:04:11 +02:00
1 parent f1061c5709
commit 83424e2d8e
1 file changed
+125
@@ -161,3 +161,128 @@ class DiskExecutorOnConflictTest {
assertTrue(error.message!!.contains("no unique or primary key constraint"), error.message!!) assertTrue(error.message!!.contains("no unique or primary key constraint"), error.message!!)
} }
@Test
fun conflictTargetMatchesADeclaredUniqueConstraintNotJustThePrimaryKey() {
val db = newDatabase()
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE, name TEXT)")
db.run("INSERT INTO users VALUES (1, 'ada@example.com', 'ada')")
val count = db.insertCount(
"INSERT INTO users VALUES (2, 'ada@example.com', 'ada2') ON CONFLICT (email) DO UPDATE SET name = excluded.name"
)
assertEquals(1, count)
assertEquals("ada2", (db.select("SELECT name FROM users WHERE email = 'ada@example.com'").rows[0][0] as Value.TextValue).value)
assertEquals(1, db.select("SELECT * FROM users").rows.size)
}
@Test
fun conflictOnADifferentUnrelatedUniqueColumnStillThrows() {
val db = newDatabase()
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE, tag TEXT UNIQUE)")
db.run("INSERT INTO users VALUES (1, 'ada@example.com', 'x')")
val error = assertFailsWith<KestrelException> {
db.run("INSERT INTO users VALUES (2, 'new@example.com', 'x') ON CONFLICT (email) DO NOTHING")
}
assertTrue(error.message!!.contains("UNIQUE constraint"), error.message!!)
}
@Test
fun conflictDoUpdateEnforcesNotNullOnTheResultingRow() {
val db = newDatabase()
db.seed()
val error = assertFailsWith<KestrelException> {
db.run("INSERT INTO accounts VALUES (1, 'ada', 100) ON CONFLICT (id) DO UPDATE SET name = NULL")
}
assertTrue(error.message!!.contains("NOT NULL"), error.message!!)
}
@Test
fun conflictDoUpdateEnforcesCheckConstraintsOnTheResultingRow() {
val db = newDatabase()
db.run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, balance INTEGER CHECK (balance >= 0))")
db.run("INSERT INTO accounts VALUES (1, 100)")
val error = assertFailsWith<KestrelException> {
db.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() {
val db = newDatabase()
db.run("CREATE TABLE departments (id INTEGER PRIMARY KEY)")
db.run("INSERT INTO departments VALUES (1)")
db.run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept INTEGER REFERENCES departments(id))")
db.run("INSERT INTO employees VALUES (1, 1)")
val error = assertFailsWith<KestrelException> {
db.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() {
val db = newDatabase()
db.run("CREATE TABLE departments (id INTEGER PRIMARY KEY)")
db.run("INSERT INTO departments VALUES (1)")
db.run("CREATE TABLE employees (id INTEGER PRIMARY KEY, dept INTEGER REFERENCES departments(id) ON UPDATE CASCADE)")
db.run("INSERT INTO employees VALUES (1, 1)")
db.insertCount("INSERT INTO departments VALUES (1) ON CONFLICT (id) DO UPDATE SET id = 2")
assertEquals(int(2), db.select("SELECT dept FROM employees WHERE id = 1").rows[0][0])
}
@Test
fun compositePrimaryKeyConflictTargetIsRecognised() {
val db = newDatabase()
db.run("CREATE TABLE grades (student INTEGER, course INTEGER, score INTEGER, PRIMARY KEY (student, course))")
db.run("INSERT INTO grades VALUES (1, 10, 50)")
val count = db.insertCount(
"INSERT INTO grades VALUES (1, 10, 90) ON CONFLICT (student, course) DO UPDATE SET score = excluded.score"
)
assertEquals(1, count)
assertEquals(int(90), db.select("SELECT score FROM grades WHERE student = 1 AND course = 10").rows[0][0])
}
@Test
fun conflictDoUpdateDefaultKeywordFallsBackToColumnDefault() {
val db = newDatabase()
db.run("CREATE TABLE t (id INTEGER PRIMARY KEY, note TEXT DEFAULT 'fresh')")
db.run("INSERT INTO t VALUES (1, 'stale')")
db.insertCount("INSERT INTO t VALUES (1, 'stale') ON CONFLICT (id) DO UPDATE SET note = DEFAULT")
assertEquals("fresh", (db.select("SELECT note FROM t WHERE id = 1").rows[0][0] as Value.TextValue).value)
}
@Test
fun onConflictInsideAnExplicitTransactionParticipatesInItsCommit() {
val db = newDatabase()
db.seed()
db.run("BEGIN")
db.insertCount("INSERT INTO accounts VALUES (1, 'ada', 999) ON CONFLICT (id) DO UPDATE SET balance = excluded.balance")
db.insertCount("INSERT INTO accounts VALUES (2, 'linus', 200) ON CONFLICT (id) DO NOTHING")
db.run("ROLLBACK")
assertEquals(int(100), db.select("SELECT balance FROM accounts WHERE id = 1").rows[0][0])
assertEquals(1, db.select("SELECT * FROM accounts").rows.size)
}
/**
* Durability proof unique to the disk-backed executor: a resolved conflict (one row inserted
* fresh, one row updated in place) must be readable after a real close/reopen of the backing
* files, not just correct in the live process's buffer pool - mirrors the reopen tests already
* established for SEQUENCE ([DiskExecutorSequenceTest.sequencePositionSurvivesAReopen]) and
* SAVEPOINT ([DiskExecutorSavepointTest.dataWrittenAfterASavepointRollbackSurvivesAReopenAfterCommit]).
*/
@Test
fun conflictResolutionSurvivesAReopen() {
val db = newDatabase()
db.seed()
db.insertCount(
"""
INSERT INTO accounts VALUES (1, 'ada', 999), (2, 'linus', 200)
ON CONFLICT (id) DO UPDATE SET balance = excluded.balance
""".trimIndent()
)
db.reopen()
assertEquals(int(999), 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(2, db.select("SELECT * FROM accounts").rows.size)
}
}