test: cover compositeForeignKeyAcceptsAFullMatch
This commit is contained in:
1 parent
9f5328b55e
commit
eb56dc4b87
1 file changed
+158
@@ -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<KestrelException> { 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<KestrelException> {
|
||||||
|
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<KestrelException> { 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<KestrelException> {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user