test: cover CompositeKeyTest and run
This commit is contained in:
1 parent
f0b4061393
commit
9f5328b55e
1 file changed
+158
@@ -0,0 +1,158 @@
|
||||
package kestrel
|
||||
|
||||
import kestrel.catalog.Catalog
|
||||
import kestrel.engine.Executor
|
||||
import kestrel.engine.InsertResult
|
||||
import kestrel.engine.SelectResult
|
||||
import kestrel.engine.StatementResult
|
||||
import kestrel.persistence.SqlWriter
|
||||
import kestrel.sql.ast.CreateTableStatement
|
||||
import kestrel.sql.ast.Statement
|
||||
import kestrel.sql.lexer.Lexer
|
||||
import kestrel.sql.parser.Parser
|
||||
import kestrel.storage.Value
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Multi-column PRIMARY KEY (`PRIMARY KEY (a, b, ...)`) and multi-column FOREIGN KEY
|
||||
* (`FOREIGN KEY (a, b) REFERENCES parent(x, y)`), the last documented "not yet supported" gap
|
||||
* left in TableSchema/ForeignKeyEnforcer after single-column UNIQUE landed.
|
||||
*/
|
||||
class CompositeKeyTest {
|
||||
private val catalog = Catalog()
|
||||
private val executor = Executor(catalog)
|
||||
|
||||
private fun run(sql: String): List<StatementResult> =
|
||||
Parser(Lexer(sql).tokenize()).parseProgram().map { executor.execute(it) }
|
||||
|
||||
private fun exec(sql: String): StatementResult = run(sql).single()
|
||||
|
||||
private fun parseOne(sql: String): Statement = Parser(Lexer(sql).tokenize()).parseProgram().single()
|
||||
|
||||
private fun select(sql: String): SelectResult = exec(sql) as SelectResult
|
||||
|
||||
private fun int(value: Long) = Value.IntegerValue(value)
|
||||
|
||||
// ---- parsing ----
|
||||
|
||||
@Test
|
||||
fun parsesTableLevelCompositePrimaryKey() {
|
||||
val statement = parseOne(
|
||||
"CREATE TABLE line_items (order_id INTEGER, sku TEXT, qty INTEGER, PRIMARY KEY (order_id, sku))"
|
||||
) as CreateTableStatement
|
||||
assertEquals(listOf("order_id", "sku"), statement.compositePrimaryKey)
|
||||
assertTrue(statement.columns.none { it.primaryKey })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parsesTableLevelCompositeForeignKey() {
|
||||
val statement = parseOne(
|
||||
"CREATE TABLE t (a INTEGER, b INTEGER, " +
|
||||
"FOREIGN KEY (a, b) REFERENCES parent (x, y) ON DELETE CASCADE)"
|
||||
) as CreateTableStatement
|
||||
val fk = statement.foreignKeys.single()
|
||||
assertEquals(listOf("a", "b"), fk.columns)
|
||||
assertEquals("parent", fk.referencedTable)
|
||||
assertEquals(listOf("x", "y"), fk.referencedColumns)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mismatchedForeignKeyColumnCountIsRejectedAtParseTime() {
|
||||
assertFailsWith<Exception> {
|
||||
parseOne("CREATE TABLE t (a INTEGER, b INTEGER, FOREIGN KEY (a, b) REFERENCES parent (x))")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- CREATE TABLE validation ----
|
||||
|
||||
@Test
|
||||
fun declaringBothInlineAndTableLevelPrimaryKeyIsRejected() {
|
||||
assertFailsWith<KestrelException> {
|
||||
run("CREATE TABLE t (a INTEGER PRIMARY KEY, b INTEGER, PRIMARY KEY (a, b))")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun duplicateColumnWithinCompositePrimaryKeyIsRejected() {
|
||||
assertFailsWith<KestrelException> {
|
||||
run("CREATE TABLE t (a INTEGER, b INTEGER, PRIMARY KEY (a, a))")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compositePrimaryKeyReferringToUnknownColumnIsRejected() {
|
||||
assertFailsWith<KestrelException> {
|
||||
run("CREATE TABLE t (a INTEGER, b INTEGER, PRIMARY KEY (a, missing))")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compositePrimaryKeyImpliesNotNullOnEveryListedColumn() {
|
||||
run("CREATE TABLE t (a INTEGER, b INTEGER, PRIMARY KEY (a, b))")
|
||||
assertFailsWith<KestrelException> { run("INSERT INTO t VALUES (1, NULL)") }
|
||||
assertFailsWith<KestrelException> { run("INSERT INTO t VALUES (NULL, 1)") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun foreignKeyMustReferenceAnExactEnforcedUniqueGroupNotAnArbitraryPair() {
|
||||
run("CREATE TABLE parent (a INTEGER, b INTEGER, c INTEGER, PRIMARY KEY (a, b))")
|
||||
assertFailsWith<KestrelException> {
|
||||
run("CREATE TABLE child (x INTEGER, y INTEGER, FOREIGN KEY (x, y) REFERENCES parent (b, c))")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun foreignKeyMayReferenceACompositePrimaryKeyInAnyColumnOrder() {
|
||||
run("CREATE TABLE parent (a INTEGER, b INTEGER, PRIMARY KEY (a, b))")
|
||||
run("CREATE TABLE child (x INTEGER, y INTEGER, FOREIGN KEY (x, y) REFERENCES parent (b, a))")
|
||||
}
|
||||
|
||||
// ---- composite PRIMARY KEY uniqueness ----
|
||||
|
||||
@Test
|
||||
fun compositePrimaryKeyRejectsAnExactDuplicateCombination() {
|
||||
run("CREATE TABLE line_items (order_id INTEGER, sku TEXT, qty INTEGER, PRIMARY KEY (order_id, sku))")
|
||||
run("INSERT INTO line_items VALUES (1, 'A', 3)")
|
||||
assertFailsWith<KestrelException> { run("INSERT INTO line_items VALUES (1, 'A', 5)") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compositePrimaryKeyAllowsDifferentCombinationsSharingOneColumnValue() {
|
||||
run("CREATE TABLE line_items (order_id INTEGER, sku TEXT, qty INTEGER, PRIMARY KEY (order_id, sku))")
|
||||
run("INSERT INTO line_items VALUES (1, 'A', 3)")
|
||||
val result = exec("INSERT INTO line_items VALUES (1, 'B', 3)")
|
||||
assertEquals(InsertResult(1), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateThatWouldCollideWithAnotherRowsCompositePrimaryKeyIsRejected() {
|
||||
run("CREATE TABLE line_items (order_id INTEGER, sku TEXT, qty INTEGER, PRIMARY KEY (order_id, sku))")
|
||||
run("INSERT INTO line_items VALUES (1, 'A', 3), (1, 'B', 5)")
|
||||
assertFailsWith<KestrelException> { run("UPDATE line_items SET sku = 'A' WHERE sku = 'B'") }
|
||||
}
|
||||
|
||||
// ---- composite FOREIGN KEY: existence checks ----
|
||||
|
||||
private fun seedOrdersAndLineItems() {
|
||||
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, qty INTEGER, " +
|
||||
"FOREIGN KEY (region, order_no) REFERENCES orders (region, order_no) " +
|
||||
"ON DELETE CASCADE ON UPDATE CASCADE)"
|
||||
)
|
||||
run("INSERT INTO orders VALUES ('eu', 1), ('us', 1)")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compositeForeignKeyRejectsAPartialMatch() {
|
||||
seedOrdersAndLineItems()
|
||||
// ('eu', 1) and ('us', 1) both exist individually but not as the pair ('us', 2).
|
||||
assertFailsWith<KestrelException> {
|
||||
run("INSERT INTO line_items VALUES ('us', 2, 'SKU1', 3)")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user