diff --git a/src/test/kotlin/kestrel/PreparedStatementTest.kt b/src/test/kotlin/kestrel/PreparedStatementTest.kt index eb044e0..9885fdc 100644 --- a/src/test/kotlin/kestrel/PreparedStatementTest.kt +++ b/src/test/kotlin/kestrel/PreparedStatementTest.kt @@ -154,3 +154,84 @@ class PreparedStatementTest { assertFailsWith { run("EXECUTE byId()") } } + @Test + fun executeOfAnUnknownNameFails() { + assertFailsWith { run("EXECUTE nope(1)") } + } + + @Test + fun preparingTheSameNameTwiceFails() { + seedAccounts() + run("PREPARE byId AS SELECT name FROM accounts WHERE id = \$1") + assertFailsWith { run("PREPARE byId AS SELECT balance FROM accounts WHERE id = \$1") } + } + + @Test + fun deallocateRemovesAPreparedStatementSoExecuteNoLongerFindsIt() { + seedAccounts() + run("PREPARE byId AS SELECT name FROM accounts WHERE id = \$1") + assertEquals(DeallocateResult("byId", all = false), run("DEALLOCATE byId").single()) + assertFailsWith { run("EXECUTE byId(1)") } + } + + @Test + fun deallocatingAnUnknownNameFails() { + assertFailsWith { run("DEALLOCATE nope") } + } + + @Test + fun deallocateAllRemovesEveryPreparedStatement() { + seedAccounts() + run("PREPARE p1 AS SELECT name FROM accounts WHERE id = \$1") + run("PREPARE p2 AS SELECT balance FROM accounts WHERE id = \$1") + assertEquals(DeallocateResult(null, all = true), run("DEALLOCATE ALL").single()) + assertFailsWith { run("EXECUTE p1(1)") } + assertFailsWith { run("EXECUTE p2(1)") } + } + + @Test + fun aNameCanBeReparedAfterBeingDeallocated() { + seedAccounts() + run("PREPARE byId AS SELECT name FROM accounts WHERE id = \$1") + run("DEALLOCATE byId") + run("PREPARE byId AS SELECT balance FROM accounts WHERE id = \$1") + assertEquals(listOf(listOf(Value.IntegerValue(100))), select("EXECUTE byId(1)").rows) + } + + @Test + fun preparedStatementsAreNotUndoneByRollback() { + seedAccounts() + run("BEGIN") + run("PREPARE byId AS SELECT name FROM accounts WHERE id = \$1") + run("ROLLBACK") + // Session-scoped, deliberately outside the transactional catalog snapshot - see + // PrepareStatement's own doc for why. + assertEquals(listOf(listOf(Value.TextValue("ada"))), select("EXECUTE byId(1)").rows) + } + + @Test + fun executeArgumentsMustBeConstantNotColumnReferences() { + seedAccounts() + run("PREPARE byId AS SELECT name FROM accounts WHERE id = \$1") + assertFailsWith { run("EXECUTE byId(id)") } + } + + @Test + fun aParameterInTheSelectListDoesNotNeedToAppearInGroupBy() { + seedAccounts() + run("PREPARE report AS SELECT \$1, COUNT(*) FROM accounts GROUP BY name") + val result = select("EXECUTE report('label')") + assertEquals(3, result.rows.size) + result.rows.forEach { row -> assertEquals(Value.TextValue("label"), row[0]) } + } + + @Test + fun aParameterTypedDirectlyOutsidePrepareParsesButOnlyFailsAtEvaluationTime() { + seedAccounts() + // $1 is structurally legal wherever any expression is (no PREPARE context exists at parse + // time to reject it) - GROUP BY validation lets it through exactly like a literal would, and + // it only fails once evaluation actually needs a real value for it. + val exception = assertFailsWith { run("SELECT \$1, COUNT(*) FROM accounts GROUP BY name") } + assertTrue(exception.message!!.contains("parameter reference"), exception.message) + } +}