From 65424efff31338aceb893da25d1c54b0aa728bce Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 13:32:34 +0200 Subject: [PATCH] test(sql): cover DiskCatalogTest and tempFile --- .../kestrel/store/sql/DiskCatalogTest.kt | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 src/test/kotlin/kestrel/store/sql/DiskCatalogTest.kt diff --git a/src/test/kotlin/kestrel/store/sql/DiskCatalogTest.kt b/src/test/kotlin/kestrel/store/sql/DiskCatalogTest.kt new file mode 100644 index 0000000..6681064 --- /dev/null +++ b/src/test/kotlin/kestrel/store/sql/DiskCatalogTest.kt @@ -0,0 +1,159 @@ +package kestrel.store.sql + +import kestrel.catalog.Column +import kestrel.catalog.SequenceDefinition +import kestrel.catalog.UniqueConstraint +import kestrel.storage.ColumnType +import kestrel.storage.Row +import kestrel.storage.Value +import java.io.File +import java.nio.file.Files +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class DiskCatalogTest { + private fun tempFile(prefix: String): File = Files.createTempFile(prefix, ".db").toFile().also { it.deleteOnExit() } + + private val usersColumns = listOf( + Column("id", ColumnType.Integer, notNull = true, primaryKey = true), + Column("name", ColumnType.Text, notNull = true, primaryKey = false) + ) + + @Test + fun creatingTheSameTableTwiceFails() { + val catalog = DiskCatalog.open(tempFile("kestrel-cat-data"), tempFile("kestrel-cat-wal")) + catalog.createTable("users", usersColumns) + assertFailsWith { catalog.createTable("users", usersColumns) } + } + + @Test + fun droppedTableIsNoLongerListedOrQueryable() { + val catalog = DiskCatalog.open(tempFile("kestrel-cat-data"), tempFile("kestrel-cat-wal")) + catalog.createTable("users", usersColumns) + catalog.dropTable("users") + + assertTrue(catalog.tableNames.isEmpty()) + assertFailsWith { catalog.table("users") } + } + + @Test + fun reopeningAFreshProcessSeesTablesAndRowsThroughNewObjectsEntirely() { + val dataFile = tempFile("kestrel-cat-data") + val walFile = tempFile("kestrel-cat-wal") + + val catalog1 = DiskCatalog.open(dataFile, walFile) + val table1 = catalog1.createTable("users", usersColumns) + val txn1 = catalog1.beginTransaction() + table1.insert(txn1, Row(listOf(Value.IntegerValue(1), Value.TextValue("alice")))) + table1.insert(txn1, Row(listOf(Value.IntegerValue(2), Value.TextValue("bob")))) + catalog1.commit(txn1) + catalog1.close() + + val catalog2 = DiskCatalog.open(dataFile, walFile) + assertEquals(listOf("users"), catalog2.tableNames) + val table2 = catalog2.table("users") + val reader = catalog2.beginTransaction() + assertEquals(setOf(1L, 2L), table2.scan(reader).map { (it.second.values[0] as Value.IntegerValue).value }.toSet()) + val (_, aliceRow) = table2.findByPrimaryKey(reader, Value.IntegerValue(1))!! + assertEquals("alice", (aliceRow.values[1] as Value.TextValue).value) + catalog2.commit(reader) + } + + @Test + fun aCommittedWriteThatNeverFlushedTheBufferPoolSurvivesAReopen() { + val dataFile = tempFile("kestrel-cat-data") + val walFile = tempFile("kestrel-cat-wal") + + val catalog1 = DiskCatalog.open(dataFile, walFile) + val table1 = catalog1.createTable("users", usersColumns) + val txn = catalog1.beginTransaction() + table1.insert(txn, Row(listOf(Value.IntegerValue(1), Value.TextValue("alice")))) + catalog1.commit(txn) + // No catalog1.close(): the dirty pages may still only live in the buffer pool. + + val catalog2 = DiskCatalog.open(dataFile, walFile) + val table2 = catalog2.table("users") + val reader = catalog2.beginTransaction() + assertEquals("alice", table2.findByPrimaryKey(reader, Value.IntegerValue(1))!!.second.values[1].let { (it as Value.TextValue).value }) + catalog2.commit(reader) + } + + @Test + fun aPrimaryKeyIndexThatSplitsSurvivesAReopenWithTheCorrectRoot() { + val dataFile = tempFile("kestrel-cat-data") + val walFile = tempFile("kestrel-cat-wal") + + val catalog1 = DiskCatalog.open(dataFile, walFile) + val table1 = catalog1.createTable("users", usersColumns) + val txn = catalog1.beginTransaction() + for (id in 1..300L) table1.insert(txn, Row(listOf(Value.IntegerValue(id), Value.TextValue("user$id")))) + catalog1.commit(txn) + catalog1.close() + + val catalog2 = DiskCatalog.open(dataFile, walFile) + val table2 = catalog2.table("users") + val reader = catalog2.beginTransaction() + assertEquals(300, table2.scan(reader).size) + for (id in listOf(1L, 150L, 300L)) { + assertEquals("user$id", table2.findByPrimaryKey(reader, Value.IntegerValue(id))!!.second.values[1].let { (it as Value.TextValue).value }) + } + val range = table2.rangeByPrimaryKey(reader, Value.IntegerValue(100), true, Value.IntegerValue(105), true) + assertEquals((100..105L).toList(), range.map { (it.second.values[0] as Value.IntegerValue).value }) + catalog2.commit(reader) + } + + @Test + fun aSecondaryIndexCreatedBeforeCloseSurvivesAReopen() { + val dataFile = tempFile("kestrel-cat-data") + val walFile = tempFile("kestrel-cat-wal") + val columns = usersColumns + Column("dept", ColumnType.Text, notNull = false, primaryKey = false) + + val catalog1 = DiskCatalog.open(dataFile, walFile) + val table1 = catalog1.createTable("users", columns) + catalog1.createIndex("users", "idx_dept", "dept") + val txn = catalog1.beginTransaction() + table1.insert(txn, Row(listOf(Value.IntegerValue(1), Value.TextValue("alice"), Value.TextValue("eng")))) + table1.insert(txn, Row(listOf(Value.IntegerValue(2), Value.TextValue("bob"), Value.TextValue("eng")))) + catalog1.commit(txn) + catalog1.close() + + val catalog2 = DiskCatalog.open(dataFile, walFile) + val table2 = catalog2.table("users") + val reader = catalog2.beginTransaction() + val engineers = table2.findBySecondary(reader, "idx_dept", Value.TextValue("eng")) + assertEquals(setOf("alice", "bob"), engineers.map { (it.second.values[1] as Value.TextValue).value }.toSet()) + catalog2.commit(reader) + } + + private val orderLineColumns = listOf( + Column("order_id", ColumnType.Integer, notNull = true, primaryKey = false), + Column("line_no", ColumnType.Integer, notNull = true, primaryKey = false), + Column("sku", ColumnType.Text, notNull = false, primaryKey = false) + ) + + @Test + fun aTableCannotDeclareBothAnInlinePrimaryKeyColumnAndACompositePrimaryKey() { + val catalog = DiskCatalog.open(tempFile("kestrel-cat-data"), tempFile("kestrel-cat-wal")) + assertFailsWith { + catalog.createTable("users", usersColumns, compositePrimaryKey = listOf("name")) + } + } + + @Test + fun aCompositePrimaryKeyReferencingAnUnknownColumnIsRejected() { + val catalog = DiskCatalog.open(tempFile("kestrel-cat-data"), tempFile("kestrel-cat-wal")) + assertFailsWith { + catalog.createTable("order_lines", orderLineColumns, compositePrimaryKey = listOf("order_id", "missing")) + } + } + + @Test + fun aCompositePrimaryKeyListingTheSameColumnTwiceIsRejected() { + val catalog = DiskCatalog.open(tempFile("kestrel-cat-data"), tempFile("kestrel-cat-wal")) + assertFailsWith { + catalog.createTable("order_lines", orderLineColumns, compositePrimaryKey = listOf("order_id", "order_id")) + } + } +