From eb56dc4b876743bd831f7ab9cbc1891583166624 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 8 Sep 2026 20:00:04 +0200 Subject: [PATCH] test: cover compositeForeignKeyAcceptsAFullMatch --- src/test/kotlin/kestrel/CompositeKeyTest.kt | 158 ++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/src/test/kotlin/kestrel/CompositeKeyTest.kt b/src/test/kotlin/kestrel/CompositeKeyTest.kt index f9f7eb0..56285e0 100644 --- a/src/test/kotlin/kestrel/CompositeKeyTest.kt +++ b/src/test/kotlin/kestrel/CompositeKeyTest.kt @@ -156,3 +156,161 @@ class CompositeKeyTest { } } + @Test + fun compositeForeignKeyAcceptsAFullMatch() { + seedOrdersAndLineItems() + val result = exec("INSERT INTO line_items VALUES ('eu', 1, 'SKU1', 3)") + assertEquals(InsertResult(1), result) + } + + @Test + fun compositeForeignKeyMatchSimpleSkipsCheckWhenAnyLocalColumnIsNull() { + seedOrdersAndLineItems() + val result = exec("INSERT INTO line_items VALUES ('eu', NULL, 'SKU1', 3)") + assertEquals(InsertResult(1), result) + } + + @Test + fun compositeForeignKeyOnDeleteCascadeRemovesMatchingChildren() { + seedOrdersAndLineItems() + run("INSERT INTO line_items VALUES ('eu', 1, 'SKU1', 3), ('us', 1, 'SKU2', 4)") + run("DELETE FROM orders WHERE region = 'eu'") + val remaining = select("SELECT sku FROM line_items") + assertEquals(listOf(listOf(Value.TextValue("SKU2"))), remaining.rows) + } + + @Test + fun compositeForeignKeyOnUpdateCascadePropagatesBothColumnsTogether() { + seedOrdersAndLineItems() + run("INSERT INTO line_items VALUES ('eu', 1, 'SKU1', 3)") + run("UPDATE orders SET order_no = 7 WHERE region = 'eu'") + val updated = select("SELECT region, order_no FROM line_items WHERE sku = 'SKU1'") + assertEquals(listOf(listOf(Value.TextValue("eu"), int(7))), updated.rows) + } + + @Test + fun compositeForeignKeyOnDeleteRestrictBlocksByDefaultWithoutCascade() { + run("CREATE TABLE orders (region TEXT, order_no INTEGER, PRIMARY KEY (region, order_no))") + run( + "CREATE TABLE line_items (region TEXT, order_no INTEGER, sku TEXT, " + + "FOREIGN KEY (region, order_no) REFERENCES orders (region, order_no))" + ) + run("INSERT INTO orders VALUES ('eu', 1)") + run("INSERT INTO line_items VALUES ('eu', 1, 'SKU1')") + assertFailsWith { run("DELETE FROM orders WHERE region = 'eu'") } + } + + @Test + fun compositeForeignKeyOnDeleteSetNullNullsEveryLocalColumn() { + run("CREATE TABLE orders (region TEXT, order_no INTEGER, PRIMARY KEY (region, order_no))") + run( + "CREATE TABLE line_items (region TEXT, order_no INTEGER, sku TEXT, " + + "FOREIGN KEY (region, order_no) REFERENCES orders (region, order_no) ON DELETE SET NULL)" + ) + run("INSERT INTO orders VALUES ('eu', 1)") + run("INSERT INTO line_items VALUES ('eu', 1, 'SKU1')") + run("DELETE FROM orders WHERE region = 'eu'") + val row = select("SELECT region, order_no FROM line_items WHERE sku = 'SKU1'").rows.single() + assertEquals(listOf(Value.NullValue, Value.NullValue), row) + } + + @Test + fun rollbackAfterCompositeCascadeDeleteRestoresBothTables() { + seedOrdersAndLineItems() + run("INSERT INTO line_items VALUES ('eu', 1, 'SKU1', 3)") + run("BEGIN") + run("DELETE FROM orders WHERE region = 'eu'") + run("ROLLBACK") + assertEquals(2, select("SELECT * FROM orders").rows.size) + assertEquals(1, select("SELECT * FROM line_items").rows.size) + } + + // ---- composite FOREIGN KEY referencing a composite UNIQUE (not the PRIMARY KEY) ---- + + @Test + fun compositeForeignKeyMayReferenceACompositeUniqueConstraintInsteadOfThePrimaryKey() { + run( + "CREATE TABLE products (id INTEGER PRIMARY KEY, brand TEXT, model TEXT, UNIQUE (brand, model))" + ) + run( + "CREATE TABLE reviews (id INTEGER PRIMARY KEY, brand TEXT, model TEXT, stars INTEGER, " + + "FOREIGN KEY (brand, model) REFERENCES products (brand, model))" + ) + run("INSERT INTO products VALUES (1, 'acme', 'x100')") + val result = exec("INSERT INTO reviews VALUES (1, 'acme', 'x100', 5)") + assertEquals(InsertResult(1), result) + assertFailsWith { + run("INSERT INTO reviews VALUES (2, 'acme', 'missing', 4)") + } + } + + // ---- self-referencing composite FOREIGN KEY ---- + + @Test + fun selfReferencingCompositeForeignKeySeedableInASingleMultiRowInsert() { + run( + "CREATE TABLE edges (" + + "tenant TEXT, node INTEGER, parent_tenant TEXT, parent_node INTEGER, " + + "PRIMARY KEY (tenant, node), " + + "FOREIGN KEY (parent_tenant, parent_node) REFERENCES edges (tenant, node))" + ) + // The root's parent columns are NULL (no parent); child references the root, all seeded + // together in one INSERT so the child must see its not-yet-committed sibling. + run( + "INSERT INTO edges VALUES " + + "('t1', 1, NULL, NULL), ('t1', 2, 't1', 1), ('t1', 3, 't1', 2)" + ) + assertEquals(3, select("SELECT * FROM edges").rows.size) + } + + // ---- SqlWriter round-trip ---- + + @Test + fun createTableWithCompositePrimaryKeyRoundTripsThroughSqlWriter() { + val statement = parseOne( + "CREATE TABLE line_items (order_id INTEGER, sku TEXT, qty INTEGER, PRIMARY KEY (order_id, sku))" + ) + run(SqlWriter.write(statement)) + run("INSERT INTO line_items VALUES (1, 'A', 3)") + assertFailsWith { run("INSERT INTO line_items VALUES (1, 'A', 9)") } + } + + @Test + fun createTableWithCompositeForeignKeyRoundTripsThroughSqlWriter() { + run(SqlWriter.write(parseOne("CREATE TABLE orders (region TEXT, order_no INTEGER, PRIMARY KEY (region, order_no))"))) + run( + SqlWriter.write( + parseOne( + "CREATE TABLE line_items (region TEXT, order_no INTEGER, sku TEXT, " + + "FOREIGN KEY (region, order_no) REFERENCES orders (region, order_no) ON DELETE CASCADE)" + ) + ) + ) + run("INSERT INTO orders VALUES ('eu', 1)") + run("INSERT INTO line_items VALUES ('eu', 1, 'SKU1')") + run("DELETE FROM orders WHERE region = 'eu'") + assertTrue(select("SELECT * FROM line_items").rows.isEmpty()) + } + + @Test + fun dumpAndReplayPreservesCompositePrimaryKeyAndForeignKey() { + seedOrdersAndLineItems() + run("INSERT INTO line_items VALUES ('eu', 1, 'SKU1', 3)") + val dump = SqlWriter.dump(catalog) + + val freshCatalog = Catalog() + val freshExecutor = Executor(freshCatalog) + Parser(Lexer(dump).tokenize()).parseProgram().forEach { freshExecutor.execute(it) } + + assertFailsWith { + Parser(Lexer("INSERT INTO orders VALUES ('eu', 1)").tokenize()).parseProgram() + .forEach { freshExecutor.execute(it) } + } + Parser(Lexer("DELETE FROM orders WHERE region = 'eu'").tokenize()).parseProgram() + .forEach { freshExecutor.execute(it) } + val remaining = freshExecutor.execute( + Parser(Lexer("SELECT * FROM line_items").tokenize()).parseProgram().single() + ) as SelectResult + assertTrue(remaining.rows.isEmpty()) + } +}