test: cover PreparedStatementTest and run
This commit is contained in:
1 parent
6c074cc16b
commit
061bc535e2
1 file changed
+156
@@ -0,0 +1,156 @@
|
|||||||
|
package kestrel
|
||||||
|
|
||||||
|
import kestrel.catalog.Catalog
|
||||||
|
import kestrel.engine.DeallocateResult
|
||||||
|
import kestrel.engine.DeleteResult
|
||||||
|
import kestrel.engine.Executor
|
||||||
|
import kestrel.engine.InsertResult
|
||||||
|
import kestrel.engine.PrepareResult
|
||||||
|
import kestrel.engine.SelectResult
|
||||||
|
import kestrel.engine.StatementResult
|
||||||
|
import kestrel.engine.UpdateResult
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End-to-end tests for `PREPARE`/`EXECUTE`/`DEALLOCATE` on the in-memory engine: `$1`, `$2`, ...
|
||||||
|
* positional parameters resolved by [kestrel.engine.ParameterSubstitution], driven through the same
|
||||||
|
* [Lexer]/[Parser]/[Executor] pipeline real SQL text goes through, not the AST directly.
|
||||||
|
*/
|
||||||
|
class PreparedStatementTest {
|
||||||
|
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 select(sql: String): SelectResult = run(sql).single() as SelectResult
|
||||||
|
|
||||||
|
private fun seedAccounts() {
|
||||||
|
run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT NOT NULL, balance INTEGER)")
|
||||||
|
run("INSERT INTO accounts VALUES (1, 'ada', 100), (2, 'linus', 200), (3, 'grace', 300)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun prepareReturnsItsNameAndExecuteRunsAParameterizedSelect() {
|
||||||
|
seedAccounts()
|
||||||
|
assertEquals(PrepareResult("byId"), run("PREPARE byId AS SELECT name, balance FROM accounts WHERE id = \$1").single())
|
||||||
|
val result = select("EXECUTE byId(2)")
|
||||||
|
assertEquals(listOf("name", "balance"), result.columns)
|
||||||
|
assertEquals(listOf(listOf(Value.TextValue("linus"), Value.IntegerValue(200))), result.rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun theSamePreparedStatementCanBeExecutedRepeatedlyWithDifferentArguments() {
|
||||||
|
seedAccounts()
|
||||||
|
run("PREPARE byId AS SELECT name FROM accounts WHERE id = \$1")
|
||||||
|
assertEquals(listOf(listOf(Value.TextValue("ada"))), select("EXECUTE byId(1)").rows)
|
||||||
|
assertEquals(listOf(listOf(Value.TextValue("grace"))), select("EXECUTE byId(3)").rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun executeSubstitutesMultipleParametersPositionally() {
|
||||||
|
seedAccounts()
|
||||||
|
run("PREPARE inRange AS SELECT name FROM accounts WHERE balance >= \$1 AND balance <= \$2 ORDER BY balance")
|
||||||
|
assertEquals(
|
||||||
|
listOf(listOf(Value.TextValue("linus")), listOf(Value.TextValue("grace"))),
|
||||||
|
select("EXECUTE inRange(150, 350)").rows
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun prepareCanWrapInsertAndExecuteInsertsARealRow() {
|
||||||
|
run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT NOT NULL, balance INTEGER)")
|
||||||
|
run("PREPARE addAccount AS INSERT INTO accounts VALUES (\$1, \$2, \$3)")
|
||||||
|
assertEquals(InsertResult(1), run("EXECUTE addAccount(1, 'ada', 100)").single())
|
||||||
|
assertEquals(
|
||||||
|
listOf(listOf(Value.IntegerValue(1), Value.TextValue("ada"), Value.IntegerValue(100))),
|
||||||
|
select("SELECT * FROM accounts").rows
|
||||||
|
)
|
||||||
|
assertEquals(InsertResult(1), run("EXECUTE addAccount(2, 'linus', 200)").single())
|
||||||
|
assertEquals(2, select("SELECT * FROM accounts").rows.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun prepareCanWrapUpdate() {
|
||||||
|
seedAccounts()
|
||||||
|
run("PREPARE credit AS UPDATE accounts SET balance = balance + \$1 WHERE id = \$2")
|
||||||
|
assertEquals(UpdateResult(1), run("EXECUTE credit(50, 1)").single())
|
||||||
|
assertEquals(listOf(listOf(Value.IntegerValue(150))), select("SELECT balance FROM accounts WHERE id = 1").rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun prepareCanWrapDelete() {
|
||||||
|
seedAccounts()
|
||||||
|
run("PREPARE removeAccount AS DELETE FROM accounts WHERE id = \$1")
|
||||||
|
assertEquals(DeleteResult(1), run("EXECUTE removeAccount(2)").single())
|
||||||
|
assertEquals(2, select("SELECT * FROM accounts").rows.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun prepareCanWrapMergeAndAParameterReachesTheMatchedUpdateAssignment() {
|
||||||
|
run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT NOT NULL, balance INTEGER)")
|
||||||
|
run("CREATE TABLE staging (id INTEGER PRIMARY KEY, name TEXT NOT NULL, balance INTEGER)")
|
||||||
|
run("INSERT INTO accounts VALUES (1, 'ada', 100)")
|
||||||
|
run("INSERT INTO staging VALUES (1, 'ada', 500)")
|
||||||
|
run(
|
||||||
|
"PREPARE topUp AS MERGE INTO accounts USING staging ON accounts.id = staging.id " +
|
||||||
|
"WHEN MATCHED THEN UPDATE SET balance = staging.balance + \$1"
|
||||||
|
)
|
||||||
|
run("EXECUTE topUp(10)")
|
||||||
|
assertEquals(listOf(listOf(Value.IntegerValue(510))), select("SELECT balance FROM accounts WHERE id = 1").rows)
|
||||||
|
// Executed a second time with a DIFFERENT argument to prove substitution recomputes fresh
|
||||||
|
// every EXECUTE rather than baking in whatever value the first call used.
|
||||||
|
run("EXECUTE topUp(20)")
|
||||||
|
assertEquals(listOf(listOf(Value.IntegerValue(520))), select("SELECT balance FROM accounts WHERE id = 1").rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun prepareCanWrapMergeAndAParameterReachesTheNotMatchedInsertValues() {
|
||||||
|
run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT NOT NULL, balance INTEGER)")
|
||||||
|
run("CREATE TABLE staging (id INTEGER PRIMARY KEY, name TEXT NOT NULL, balance INTEGER)")
|
||||||
|
run("INSERT INTO staging VALUES (1, 'ada', 500)")
|
||||||
|
run(
|
||||||
|
"PREPARE seedFromStaging AS MERGE INTO accounts USING staging ON accounts.id = staging.id " +
|
||||||
|
"WHEN NOT MATCHED THEN INSERT VALUES (staging.id, staging.name, staging.balance + \$1)"
|
||||||
|
)
|
||||||
|
run("EXECUTE seedFromStaging(10)")
|
||||||
|
assertEquals(
|
||||||
|
listOf(listOf(Value.IntegerValue(1), Value.TextValue("ada"), Value.IntegerValue(510))),
|
||||||
|
select("SELECT * FROM accounts").rows
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun prepareCanWrapMergeAndAParameterInTheOnConditionControlsWhetherARowMatches() {
|
||||||
|
run("CREATE TABLE accounts (id INTEGER PRIMARY KEY, name TEXT NOT NULL, balance INTEGER)")
|
||||||
|
run("CREATE TABLE staging (id INTEGER PRIMARY KEY, name TEXT NOT NULL, balance INTEGER)")
|
||||||
|
run("INSERT INTO accounts VALUES (1, 'ada', 100)")
|
||||||
|
run("INSERT INTO staging VALUES (1, 'ada', 500)")
|
||||||
|
// Only a WHEN MATCHED clause - a source row the ON condition rejects has no NOT MATCHED
|
||||||
|
// clause to fall into, so it is a true no-op rather than an insert attempt.
|
||||||
|
run(
|
||||||
|
"PREPARE conditionalTopUp AS MERGE INTO accounts USING staging " +
|
||||||
|
"ON accounts.id = staging.id AND staging.balance > \$1 " +
|
||||||
|
"WHEN MATCHED THEN UPDATE SET balance = staging.balance"
|
||||||
|
)
|
||||||
|
run("EXECUTE conditionalTopUp(1000)")
|
||||||
|
assertEquals(listOf(listOf(Value.IntegerValue(100))), select("SELECT balance FROM accounts WHERE id = 1").rows)
|
||||||
|
run("EXECUTE conditionalTopUp(10)")
|
||||||
|
assertEquals(listOf(listOf(Value.IntegerValue(500))), select("SELECT balance FROM accounts WHERE id = 1").rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun executeWithTheWrongNumberOfArgumentsFailsWithAClearMessage() {
|
||||||
|
seedAccounts()
|
||||||
|
run("PREPARE byId AS SELECT name FROM accounts WHERE id = \$1")
|
||||||
|
val tooMany = assertFailsWith<KestrelException> { run("EXECUTE byId(1, 2)") }
|
||||||
|
assertTrue(tooMany.message!!.contains("expects 1 parameter"), tooMany.message)
|
||||||
|
assertFailsWith<KestrelException> { run("EXECUTE byId()") }
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user