diff --git a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt index 6fb03cb..eb59d84 100644 --- a/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt +++ b/src/test/kotlin/kestrel/store/sql/DiskExecutorTest.kt @@ -1113,3 +1113,160 @@ class DiskExecutorTest { assertEquals(2, db.select("SELECT * FROM registrations").rows.size) } + @Test + fun createUniqueCompositeIndexAllowsMultipleRowsWithANullInEitherColumn() { + 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, NULL), (1, NULL), (NULL, 100), (NULL, 100)") + assertEquals(4, db.select("SELECT * FROM registrations").rows.size) + } + + @Test + fun createUniqueCompositeIndexOverExistingDuplicateCombinationsFailsAndLeavesNoTraceOfTheIndex() { + val db = newDatabase() + db.run("CREATE TABLE registrations (event_id INTEGER, user_id INTEGER)") + db.run("INSERT INTO registrations VALUES (1, 100), (1, 100)") + assertFailsWith { db.run("CREATE UNIQUE INDEX idx_reg ON registrations(event_id, user_id)") } + assertTrue(db.catalog.table("registrations").secondaryIndexes.isEmpty()) + } + + @Test + fun createUniqueCompositeIndexRejectsAnUpdateThatWouldDuplicateAnotherRowsCombination() { + val db = newDatabase() + db.run("CREATE TABLE registrations (id INTEGER PRIMARY KEY, 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, 1, 100), (2, 1, 200)") + assertFailsWith { db.run("UPDATE registrations SET user_id = 100 WHERE id = 2") } + assertEquals(int(200), db.select("SELECT user_id FROM registrations WHERE id = 2").rows.single().single()) + } + + @Test + fun createUniqueCompositeIndexDataAndConstraintSurviveClosingAndReopeningTheCatalog() { + 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)") + db.reopen() + assertFailsWith { db.run("INSERT INTO registrations VALUES (1, 100)") } + val handle = db.catalog.table("registrations").secondaryIndexes.single() + assertEquals(listOf("event_id", "user_id"), handle.columnNames) + assertTrue(handle.unique) + } + + // --- index pushdown: proven by DiskTable's access counters, not just by correct results, + // since a full scan and an index lookup return identical rows for these queries. + + @Test + fun whereEqualityOnThePrimaryKeyUsesAPointLookupNotAFullScan() { + val db = newDatabase() + db.seedUsers() + val table = db.catalog.table("users") + val result = db.select("SELECT name FROM users WHERE id = 2") + assertEquals(listOf(listOf(text("linus"))), result.rows) + assertEquals(1, table.pointLookupCount) + assertEquals(0, table.scanCount) + } + + @Test + fun aMissingPrimaryKeyPointLookupReturnsNoRowsWithoutScanning() { + val db = newDatabase() + db.seedUsers() + val table = db.catalog.table("users") + val result = db.select("SELECT name FROM users WHERE id = 999") + assertEquals(emptyList>(), result.rows) + assertEquals(1, table.pointLookupCount) + assertEquals(0, table.scanCount) + } + + @Test + fun aRangeOnThePrimaryKeyUsesARangeScanNotAFullScan() { + val db = newDatabase() + db.seedUsers() + val table = db.catalog.table("users") + val result = db.select("SELECT name FROM users WHERE id > 1 AND id <= 3 ORDER BY id") + assertEquals(listOf(listOf(text("linus")), listOf(text("grace"))), result.rows) + assertEquals(1, table.rangeScanCount) + assertEquals(0, table.scanCount) + } + + @Test + fun equalityOnAnUnindexedColumnStillFallsBackToAFullScan() { + val db = newDatabase() + db.seedUsers() + val table = db.catalog.table("users") + val result = db.select("SELECT id FROM users WHERE name = 'linus'") + assertEquals(listOf(listOf(int(2))), result.rows) + assertEquals(1, table.scanCount) + assertEquals(0, table.pointLookupCount) + } + + @Test + fun equalityOnASecondaryIndexedColumnUsesTheSecondaryIndexNotAFullScan() { + val db = newDatabase() + db.seedUsers() + db.run("CREATE INDEX idx_age ON users(age)") + val table = db.catalog.table("users") + val scansBeforeSelect = table.scanCount // backfilling the new index already scanned once + val result = db.select("SELECT name FROM users WHERE age = 54") + assertEquals(listOf(listOf(text("linus"))), result.rows) + assertEquals(1, table.secondaryLookupCount) + assertEquals(scansBeforeSelect, table.scanCount) + } + + @Test + fun equalityOnAllColumnsOfACompositeSecondaryIndexUsesTheSecondaryIndexNotAFullScan() { + val db = newDatabase() + db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, age INTEGER, name TEXT)") + db.run("INSERT INTO users VALUES (1, 36, 'ada'), (2, 36, 'grace')") + db.run("CREATE INDEX idx_both ON users(age, name)") + val table = db.catalog.table("users") + val scansBeforeSelect = table.scanCount // backfilling the new index already scanned once + val result = db.select("SELECT id FROM users WHERE age = 36 AND name = 'grace'") + assertEquals(listOf(listOf(int(2))), result.rows) + assertEquals(1, table.secondaryLookupCount) + assertEquals(scansBeforeSelect, table.scanCount) + } + + @Test + fun equalityOnOnlyOneColumnOfACompositeSecondaryIndexFallsBackToAFullScan() { + val db = newDatabase() + db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, age INTEGER, name TEXT)") + db.run("INSERT INTO users VALUES (1, 36, 'ada'), (2, 36, 'grace')") + db.run("CREATE INDEX idx_both ON users(age, name)") + val table = db.catalog.table("users") + val scansBeforeSelect = table.scanCount + val result = db.select("SELECT id FROM users WHERE age = 36") + assertEquals(2, result.rows.size) + assertEquals(0, table.secondaryLookupCount) + assertEquals(scansBeforeSelect + 1, table.scanCount) + } + + @Test + fun aRangeOnASingleColumnSecondaryIndexUsesARangeScanNotAFullScan() { + val db = newDatabase() + db.seedUsers() + db.run("CREATE INDEX idx_age ON users(age)") + val table = db.catalog.table("users") + val scansBeforeSelect = table.scanCount // backfilling the new index already scanned once + val result = db.select("SELECT name FROM users WHERE age > 40 AND age <= 85 ORDER BY age") + assertEquals(listOf(listOf(text("linus")), listOf(text("grace"))), result.rows) + assertEquals(1, table.rangeScanCount) + assertEquals(0, table.secondaryLookupCount) + assertEquals(scansBeforeSelect, table.scanCount) + } + + @Test + fun aRangeOnTheFirstColumnOfACompositeSecondaryIndexUsesARangeScanNotAFullScan() { + val db = newDatabase() + db.run("CREATE TABLE order_lines (order_id INTEGER, line_no INTEGER, sku TEXT)") + db.run("INSERT INTO order_lines VALUES (1, 1, 'apple'), (2, 1, 'kiwi'), (3, 1, 'mango')") + db.run("CREATE INDEX idx_order_sku ON order_lines(order_id, sku)") + val table = db.catalog.table("order_lines") + val scansBeforeSelect = table.scanCount // backfilling the new index already scanned once + val result = db.select("SELECT sku FROM order_lines WHERE order_id > 1 ORDER BY order_id") + assertEquals(listOf(listOf(text("kiwi")), listOf(text("mango"))), result.rows) + assertEquals(1, table.rangeScanCount) + assertEquals(scansBeforeSelect, table.scanCount) + } +