test: cover createUniqueIndexRejectsADuplicateOnInsert
This commit is contained in:
1 parent
641dd74a09
commit
587771b525
2 files changed
+191
No files matched your search
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user