From a76bab51591c69b9d03da47194a6723e46b49fba Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 13 Sep 2026 22:24:30 +0200 Subject: [PATCH] test(sql): cover uniqueSecondaryIndexRejectsAnUpdateThatWouldCollideW... --- .../kotlin/kestrel/store/sql/DiskTableTest.kt | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/src/test/kotlin/kestrel/store/sql/DiskTableTest.kt b/src/test/kotlin/kestrel/store/sql/DiskTableTest.kt index 8cc0cfd..0086e0e 100644 --- a/src/test/kotlin/kestrel/store/sql/DiskTableTest.kt +++ b/src/test/kotlin/kestrel/store/sql/DiskTableTest.kt @@ -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 { 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 { 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 { 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 { 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 { 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 { 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 { table.update(txn, rid, registration(1, 200)) } + catalog.commit(txn) + } +}