test(sql): cover dropIndexStopsThePlannerFromPushingDownAFormerlyInde...
This commit is contained in:
1 parent
71ee4dfabc
commit
743ca28b0c
1 file changed
+156
@@ -957,3 +957,159 @@ class DiskExecutorTest {
|
||||
assertTrue(db.catalog.table("users").secondaryIndexes.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dropIndexStopsThePlannerFromPushingDownAFormerlyIndexedEquality() {
|
||||
val db = newDatabase()
|
||||
db.seedUsers()
|
||||
db.run("CREATE INDEX idx_age ON users(age)")
|
||||
db.run("DROP INDEX idx_age")
|
||||
val table = db.catalog.table("users")
|
||||
val scansBefore = table.scanCount
|
||||
db.select("SELECT * FROM users WHERE age = 54")
|
||||
assertEquals(0, table.secondaryLookupCount)
|
||||
assertEquals(scansBefore + 1, table.scanCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun droppingAUniqueIndexStopsItFromRejectingFurtherDuplicates() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)")
|
||||
db.run("CREATE UNIQUE INDEX idx_email ON users(email)")
|
||||
db.run("INSERT INTO users VALUES (1, 'ada@example.com')")
|
||||
db.run("DROP INDEX idx_email")
|
||||
db.run("INSERT INTO users VALUES (2, 'ada@example.com')")
|
||||
assertEquals(2, db.select("SELECT * FROM users").rows.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dropIndexOnACompositeIndexRemovesTheWholeIndexNotJustOneColumn() {
|
||||
val db = newDatabase()
|
||||
db.seedUsers()
|
||||
db.run("CREATE INDEX idx_both ON users(age, name)")
|
||||
db.run("DROP INDEX idx_both")
|
||||
assertTrue(db.catalog.table("users").secondaryIndexes.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun droppingAnUnknownIndexFails() {
|
||||
val db = newDatabase()
|
||||
db.seedUsers()
|
||||
assertFailsWith<DiskCatalogException> { db.run("DROP INDEX ghost") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun droppingAnAlreadyDroppedIndexFails() {
|
||||
val db = newDatabase()
|
||||
db.seedUsers()
|
||||
db.run("CREATE INDEX idx_age ON users(age)")
|
||||
db.run("DROP INDEX idx_age")
|
||||
assertFailsWith<DiskCatalogException> { db.run("DROP INDEX idx_age") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dropIndexIsPersistedAcrossClosingAndReopeningTheCatalog() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)")
|
||||
db.run("CREATE UNIQUE INDEX idx_email ON users(email)")
|
||||
db.run("INSERT INTO users VALUES (1, 'ada@example.com')")
|
||||
db.run("DROP INDEX idx_email")
|
||||
db.reopen()
|
||||
assertTrue(db.catalog.table("users").secondaryIndexes.isEmpty())
|
||||
db.run("INSERT INTO users VALUES (2, 'ada@example.com')")
|
||||
assertEquals(2, db.select("SELECT * FROM users").rows.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dropIndexCannotRunInsideAnExplicitTransaction() {
|
||||
val db = newDatabase()
|
||||
db.seedUsers()
|
||||
db.run("CREATE INDEX idx_age ON users(age)")
|
||||
db.run("BEGIN")
|
||||
assertFailsWith<DiskExecutorException> { db.run("DROP INDEX idx_age") }
|
||||
db.run("ROLLBACK")
|
||||
}
|
||||
|
||||
// --- declarative UNIQUE constraints (inline `col TYPE UNIQUE` / table-level `UNIQUE (a, b, ...)`),
|
||||
// enforced by a full heap scan per write rather than a backing index, mirroring
|
||||
// kestrel.engine.Executor's own uniqueConstraints handling.
|
||||
|
||||
@Test
|
||||
fun inlineUniqueColumnRejectsADuplicateValueOnInsert() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE)")
|
||||
db.run("INSERT INTO users VALUES (1, 'ada@example.com')")
|
||||
assertFailsWith<DiskTableException> { db.run("INSERT INTO users VALUES (2, 'ada@example.com')") }
|
||||
assertEquals(1, db.select("SELECT * FROM users").rows.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun inlineUniqueColumnAllowsMultipleNulls() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE)")
|
||||
db.run("INSERT INTO users VALUES (1, NULL), (2, NULL)")
|
||||
assertEquals(2, db.select("SELECT * FROM users").rows.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tableLevelCompositeUniqueConstraintRejectsADuplicateColumnCombination() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE registrations (event_id INTEGER, user_id INTEGER, UNIQUE (event_id, user_id))")
|
||||
db.run("INSERT INTO registrations VALUES (1, 100)")
|
||||
assertFailsWith<DiskTableException> { db.run("INSERT INTO registrations VALUES (1, 100)") }
|
||||
// Sharing only one of the two columns is fine: the constraint is on the pair, not either column alone.
|
||||
db.run("INSERT INTO registrations VALUES (1, 200)")
|
||||
assertEquals(2, db.select("SELECT * FROM registrations").rows.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uniqueConstraintRejectsAnUpdateThatWouldDuplicateAnotherRowsValue() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE)")
|
||||
db.run("INSERT INTO users VALUES (1, 'ada@example.com'), (2, 'grace@example.com')")
|
||||
assertFailsWith<DiskTableException> { db.run("UPDATE users SET email = 'ada@example.com' WHERE id = 2") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uniqueConstraintReferencingAnUnknownColumnIsRejectedWithAClearError() {
|
||||
val db = newDatabase()
|
||||
val failure = assertFailsWith<DiskExecutorException> {
|
||||
db.run("CREATE TABLE bad (id INTEGER PRIMARY KEY, UNIQUE (missing_column))")
|
||||
}
|
||||
assertTrue(failure.message!!.contains("missing_column"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uniqueConstraintDataAndEnforcementSurviveClosingAndReopeningTheCatalog() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE)")
|
||||
db.run("INSERT INTO users VALUES (1, 'ada@example.com')")
|
||||
db.reopen()
|
||||
assertFailsWith<DiskTableException> { db.run("INSERT INTO users VALUES (2, 'ada@example.com')") }
|
||||
assertEquals(1, db.select("SELECT * FROM users").rows.size)
|
||||
}
|
||||
|
||||
// --- composite (multi-column) CREATE [UNIQUE] INDEX
|
||||
|
||||
@Test
|
||||
fun createIndexBuildsANonUniqueCompositeSecondaryIndex() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, age INTEGER, name TEXT)")
|
||||
val created = db.run("CREATE INDEX idx_both ON users(age, name)").single()
|
||||
assertEquals(CreateIndexResult("idx_both"), created)
|
||||
val handle = db.catalog.table("users").secondaryIndexes.single()
|
||||
assertEquals(listOf("age", "name"), handle.columnNames)
|
||||
assertEquals(false, handle.unique)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createUniqueCompositeIndexRejectsADuplicateColumnCombinationOnInsert() {
|
||||
val db = newDatabase()
|
||||
db.run("CREATE TABLE registrations (event_id INTEGER, user_id INTEGER)")
|
||||
db.run("CREATE UNIQUE INDEX idx_reg ON registrations(event_id, user_id)")
|
||||
db.run("INSERT INTO registrations VALUES (1, 100)")
|
||||
assertFailsWith<DiskTableException> { db.run("INSERT INTO registrations VALUES (1, 100)") }
|
||||
// Sharing only one of the two columns is fine: the index is on the pair, not either column alone.
|
||||
db.run("INSERT INTO registrations VALUES (1, 200)")
|
||||
assertEquals(2, db.select("SELECT * FROM registrations").rows.size)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user