test(sql): cover uniqueSecondaryIndexRejectsAnUpdateThatWouldCollideW...

This commit is contained in:
dualfroz committed 2026-09-13 22:24:30 +02:00
1 parent 45745b9dec
commit a76bab5159
1 file changed
+175
@@ -311,3 +311,178 @@ class DiskTableTest {
catalog.commit(txn)
}
@Test
fun uniqueSecondaryIndexRejectsAnUpdateThatWouldCollideWithAnotherRow() {
val catalog = newCatalog()
val table = catalog.createTable("users", usersColumns)
catalog.createIndex("users", "idx_name", "name", unique = true)
val txn = catalog.beginTransaction()
val rid = table.insert(txn, row(1, "alice", 30))
table.insert(txn, row(2, "bob", 40))
assertFailsWith<DiskTableException> { table.update(txn, rid, row(1, "bob", 30)) }
assertEquals("alice", table.get(txn, rid).values[1].let { (it as Value.TextValue).value })
catalog.commit(txn)
}
@Test
fun uniqueSecondaryIndexAllowsAnUpdateThatDoesNotChangeTheConstrainedColumn() {
val catalog = newCatalog()
val table = catalog.createTable("users", usersColumns)
catalog.createIndex("users", "idx_name", "name", unique = true)
val txn = catalog.beginTransaction()
val rid = table.insert(txn, row(1, "alice", 30))
table.update(txn, rid, row(1, "alice", 31))
assertEquals(31L, (table.get(txn, rid).values[2] as Value.IntegerValue).value)
catalog.commit(txn)
}
@Test
fun creatingAUniqueIndexOverPreExistingDuplicateValuesFailsAndAttachesNoIndex() {
val catalog = newCatalog()
val table = catalog.createTable("users", usersColumns)
val txn = catalog.beginTransaction()
table.insert(txn, row(1, "alice", 30))
table.insert(txn, row(2, "alice", 40))
catalog.commit(txn)
assertFailsWith<DiskTableException> { catalog.createIndex("users", "idx_name", "name", unique = true) }
assertTrue(table.secondaryIndexes.isEmpty())
}
// --- composite (multi-column) secondary index: same B+Tree shape as a single-column one,
// keyed by KeyCodec.encodeKey over every listed column rather than a single value's encoding.
@Test
fun compositeSecondaryIndexFindsRowsByEqualityOnEveryColumn() {
val catalog = newCatalog()
val table = catalog.createTable("users", usersColumns)
catalog.createIndex("users", "idx_both", listOf("name", "age"))
val txn = catalog.beginTransaction()
val rid = table.insert(txn, row(1, "alice", 30))
table.insert(txn, row(2, "alice", 40))
val found = table.findBySecondary(txn, "idx_both", listOf(Value.TextValue("alice"), Value.IntegerValue(30)))
assertEquals(listOf(rid to row(1, "alice", 30)), found)
catalog.commit(txn)
}
@Test
fun uniqueCompositeSecondaryIndexRejectsADuplicateColumnCombinationOnInsertButAllowsAPartialOverlap() {
val catalog = newCatalog()
val table = catalog.createTable("registrations", registrationColumns)
catalog.createIndex("registrations", "idx_reg", listOf("event_id", "user_id"), unique = true)
val txn = catalog.beginTransaction()
table.insert(txn, registration(1, 100))
assertFailsWith<DiskTableException> { table.insert(txn, registration(1, 100)) }
table.insert(txn, registration(1, 200))
assertEquals(2, table.scan(txn).size)
catalog.commit(txn)
}
@Test
fun uniqueCompositeSecondaryIndexAllowsMultipleRowsWithANullInEitherColumn() {
val catalog = newCatalog()
val table = catalog.createTable("registrations", registrationColumns)
catalog.createIndex("registrations", "idx_reg", listOf("event_id", "user_id"), unique = true)
val txn = catalog.beginTransaction()
table.insert(txn, registration(1, null))
table.insert(txn, registration(1, null))
table.insert(txn, registration(null, 100))
table.insert(txn, registration(null, 100))
assertEquals(4, table.scan(txn).size)
catalog.commit(txn)
}
@Test
fun uniqueCompositeSecondaryIndexRejectsAnUpdateThatWouldCollideWithAnotherRowsCombination() {
val catalog = newCatalog()
val table = catalog.createTable("registrations", registrationColumns)
catalog.createIndex("registrations", "idx_reg", listOf("event_id", "user_id"), unique = true)
val txn = catalog.beginTransaction()
table.insert(txn, registration(1, 100))
val rid = table.insert(txn, registration(1, 200))
assertFailsWith<DiskTableException> { table.update(txn, rid, registration(1, 100)) }
catalog.commit(txn)
}
@Test
fun creatingAUniqueCompositeIndexOverPreExistingDuplicateCombinationsFailsAndAttachesNoIndex() {
val catalog = newCatalog()
val table = catalog.createTable("registrations", registrationColumns)
val txn = catalog.beginTransaction()
table.insert(txn, registration(1, 100))
table.insert(txn, registration(1, 100))
catalog.commit(txn)
assertFailsWith<DiskTableException> { catalog.createIndex("registrations", "idx_reg", listOf("event_id", "user_id"), unique = true) }
assertTrue(table.secondaryIndexes.isEmpty())
}
// --- declarative UNIQUE constraints (TableSchema.uniqueConstraints): no backing index,
// enforced by DiskTable with a full heap scan, so a single constraint can span multiple columns.
private val registrationColumns = listOf(
Column("event_id", ColumnType.Integer, notNull = false, primaryKey = false),
Column("user_id", ColumnType.Integer, notNull = false, primaryKey = false)
)
private fun registration(eventId: Long?, userId: Long?) = Row(
listOf(eventId?.let { Value.IntegerValue(it) } ?: Value.NullValue, userId?.let { Value.IntegerValue(it) } ?: Value.NullValue)
)
@Test
fun uniqueConstraintRejectsADuplicateColumnCombinationButAllowsPartialOverlap() {
val catalog = newCatalog()
val table = catalog.createTable(
"registrations", registrationColumns,
uniqueConstraints = listOf(UniqueConstraint(null, listOf("event_id", "user_id")))
)
val txn = catalog.beginTransaction()
table.insert(txn, registration(1, 100))
assertFailsWith<DiskTableException> { table.insert(txn, registration(1, 100)) }
table.insert(txn, registration(1, 200))
table.insert(txn, registration(2, 100))
assertEquals(3, table.scan(txn).size)
catalog.commit(txn)
}
@Test
fun uniqueConstraintExemptsRowsWhereAnyConstrainedColumnIsNull() {
val catalog = newCatalog()
val table = catalog.createTable(
"registrations", registrationColumns,
uniqueConstraints = listOf(UniqueConstraint(null, listOf("event_id", "user_id")))
)
val txn = catalog.beginTransaction()
table.insert(txn, registration(1, null))
table.insert(txn, registration(1, null))
assertEquals(2, table.scan(txn).size)
catalog.commit(txn)
}
@Test
fun uniqueConstraintRejectsAnUpdateThatWouldCollideWithAnotherRow() {
val catalog = newCatalog()
val table = catalog.createTable(
"registrations", registrationColumns,
uniqueConstraints = listOf(UniqueConstraint(null, listOf("event_id", "user_id")))
)
val txn = catalog.beginTransaction()
val rid = table.insert(txn, registration(1, 100))
table.insert(txn, registration(1, 200))
assertFailsWith<DiskTableException> { table.update(txn, rid, registration(1, 200)) }
catalog.commit(txn)
}
}