test: cover createUniqueIndexRejectsADuplicateOnInsert

This commit is contained in:
dualfroz committed 2026-09-09 12:30:24 +02:00
1 parent 641dd74a09
commit 587771b525
2 files changed
+191

No files matched your search

+50
View File
@@ -112,4 +112,54 @@ class IndexTest {
fun droppingUnknownIndexIsRejected() { fun droppingUnknownIndexIsRejected() {
assertFailsWith<KestrelException> { run("DROP INDEX ghost") } assertFailsWith<KestrelException> { run("DROP INDEX ghost") }
} }
@Test
fun createUniqueIndexRejectsADuplicateOnInsert() {
seed()
run("CREATE UNIQUE INDEX idxName ON users (name)")
assertFailsWith<KestrelException> { run("INSERT INTO users VALUES (6, 'ada', 21)") }
}
@Test
fun createUniqueIndexRejectsADuplicateOnUpdate() {
seed()
run("CREATE UNIQUE INDEX idxName ON users (name)")
assertFailsWith<KestrelException> { run("UPDATE users SET name = 'ada' WHERE name = 'linus'") }
}
@Test
fun createUniqueIndexAllowsMultipleNulls() {
run("CREATE TABLE t (id INTEGER PRIMARY KEY, tag TEXT)")
run("CREATE UNIQUE INDEX idxTag ON t (tag)")
run("INSERT INTO t VALUES (1, NULL), (2, NULL)")
assertEquals(2, select("SELECT id FROM t").rows.size)
}
@Test
fun creatingAUniqueIndexOverExistingDuplicatesFails() {
seed()
run("INSERT INTO users VALUES (6, 'ada', 21)")
assertFailsWith<KestrelException> { run("CREATE UNIQUE INDEX idxName ON users (name)") }
// A failed creation must not leave a half-built index registered under the name.
run("CREATE INDEX idxName ON users (name)")
}
@Test
fun uniqueIndexStillAcceleratesEqualityLookups() {
seed()
run("CREATE UNIQUE INDEX idxName ON users (name)")
val plan = explain("EXPLAIN SELECT id FROM users WHERE name = 'ada'")
assertTrue(plan.any { it.contains("Index Scan using idxName") }, "expected index scan, got $plan")
}
@Test
fun rollbackAfterAFailedInsertLeavesTheUniqueIndexUsable() {
seed()
run("CREATE UNIQUE INDEX idxName ON users (name)")
run("BEGIN")
assertFailsWith<KestrelException> { run("INSERT INTO users VALUES (6, 'ada', 21)") }
run("ROLLBACK")
run("INSERT INTO users VALUES (6, 'zoe', 21)")
assertEquals(6, select("SELECT id FROM users").rows.size)
}
} }
+141
View File
@@ -59,6 +59,110 @@ class JoinTest {
assertEquals(listOf(text("ada"), int(100)), result.rows[3]) assertEquals(listOf(text("ada"), int(100)), result.rows[3])
} }
private fun seedWithOrphanOrder() {
seed()
// References a userId that does not exist, giving RIGHT/FULL JOIN an unmatched right row.
run("INSERT INTO orders VALUES (13, 99, 999)")
}
@Test
fun rightJoinKeepsUnmatchedRightRowsWithNulls() {
seedWithOrphanOrder()
val result = select(
"SELECT u.name, o.total FROM users u RIGHT JOIN orders o ON u.id = o.userId ORDER BY o.id ASC"
)
assertEquals(
listOf(
listOf(text("ada"), int(100)),
listOf(text("ada"), int(40)),
listOf(text("linus"), int(70)),
listOf(Value.NullValue, int(999))
),
result.rows
)
}
@Test
fun rightJoinDropsUnmatchedLeftRows() {
seedWithOrphanOrder()
val result = select("SELECT u.name FROM users u RIGHT JOIN orders o ON u.id = o.userId")
assertTrue(result.rows.none { it == listOf(text("grace")) }, "got ${result.rows}")
}
@Test
fun fullJoinKeepsBothUnmatchedSides() {
seedWithOrphanOrder()
val result = select("SELECT u.name, o.total FROM users u FULL JOIN orders o ON u.id = o.userId")
assertEquals(
setOf(
listOf(text("ada"), int(100)),
listOf(text("ada"), int(40)),
listOf(text("linus"), int(70)),
listOf(text("grace"), Value.NullValue),
listOf(Value.NullValue, int(999))
),
result.rows.toSet()
)
assertEquals(5, result.rows.size)
}
@Test
fun fullJoinHashAndNestedLoopProduceSameResult() {
seedWithOrphanOrder()
val hashed = select(
"SELECT u.name, o.total FROM users u FULL JOIN orders o ON u.id = o.userId"
).rows.toSet()
val nested = select(
"SELECT u.name, o.total FROM users u FULL JOIN orders o ON u.id >= o.userId AND u.id <= o.userId"
).rows.toSet()
assertEquals(hashed, nested)
}
@Test
fun rightJoinPadsWithFullWidthOfAllPrecedingTablesInAChain() {
seed()
run("CREATE TABLE items (orderId INTEGER, label TEXT)")
// orderId 99 matches no `orders` row, so it can only survive via the RIGHT JOIN's own
// unmatched-right padding - proving that padding spans BOTH preceding tables (users AND
// orders), not just orders (the immediate left side of this particular JOIN).
run("INSERT INTO items VALUES (10, 'book'), (99, 'orphanItem')")
val result = select(
"SELECT u.name, o.total, i.label FROM users u " +
"JOIN orders o ON u.id = o.userId " +
"RIGHT JOIN items i ON o.id = i.orderId"
)
assertEquals(
setOf(
listOf(text("ada"), int(100), text("book")),
listOf(Value.NullValue, Value.NullValue, text("orphanItem"))
),
result.rows.toSet()
)
assertEquals(2, result.rows.size)
}
@Test
fun rightAndFullJoinAcceptOptionalOuterKeyword() {
seed()
assertEquals(
select("SELECT u.name FROM users u RIGHT JOIN orders o ON u.id = o.userId").rows.toSet(),
select("SELECT u.name FROM users u RIGHT OUTER JOIN orders o ON u.id = o.userId").rows.toSet()
)
assertEquals(
select("SELECT u.name FROM users u FULL JOIN orders o ON u.id = o.userId").rows.toSet(),
select("SELECT u.name FROM users u FULL OUTER JOIN orders o ON u.id = o.userId").rows.toSet()
)
}
@Test
fun plannerDescribesRightAndFullJoinLabels() {
seed()
val rightNested = explain("EXPLAIN SELECT u.name FROM users u RIGHT JOIN orders o ON u.id < o.userId")
assertTrue(rightNested.any { it.contains("Nested Loop Right") }, "got $rightNested")
val fullHash = explain("EXPLAIN SELECT u.name FROM users u FULL JOIN orders o ON u.id = o.userId")
assertTrue(fullHash.any { it.contains("Hash Join") }, "got $fullHash")
}
@Test @Test
fun hashJoinAndNestedLoopProduceSameResult() { fun hashJoinAndNestedLoopProduceSameResult() {
seed() seed()
@@ -115,4 +219,41 @@ class JoinTest {
select("SELECT id FROM users u JOIN orders o ON u.id = o.userId") select("SELECT id FROM users u JOIN orders o ON u.id = o.userId")
} }
} }
@Test
fun crossJoinProducesTheFullCartesianProduct() {
run("CREATE TABLE colors (name TEXT)")
run("CREATE TABLE sizes (label TEXT)")
run("INSERT INTO colors VALUES ('red'), ('blue')")
run("INSERT INTO sizes VALUES ('s'), ('m'), ('l')")
val result = select("SELECT colors.name, sizes.label FROM colors CROSS JOIN sizes ORDER BY colors.name, sizes.label")
assertEquals(
listOf(
listOf(text("blue"), text("l")), listOf(text("blue"), text("m")), listOf(text("blue"), text("s")),
listOf(text("red"), text("l")), listOf(text("red"), text("m")), listOf(text("red"), text("s"))
),
result.rows
)
}
@Test
fun crossJoinNeverUsesHashJoinAndIsDescribedAsCrossInExplain() {
run("CREATE TABLE colors (name TEXT)")
run("CREATE TABLE sizes (label TEXT)")
val plan = explain("EXPLAIN SELECT colors.name FROM colors CROSS JOIN sizes")
assertTrue(plan.any { it.contains("Nested Loop Cross") }, "got $plan")
assertTrue(plan.none { it.contains("Hash Join") }, "got $plan")
}
@Test
fun crossJoinChainedWithAFilterActsLikeAnExplicitCartesianJoin() {
seed()
val cross = select(
"SELECT u.name, o.total FROM users u CROSS JOIN orders o WHERE u.id = o.userId ORDER BY o.total ASC"
).rows
val inner = select(
"SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.userId ORDER BY o.total ASC"
).rows
assertEquals(inner, cross)
}
} }