test(sql): cover uniqueSecondaryIndexRejectsAnUpdateThatWouldCollideW...
This commit is contained in:
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user