Add Cursor.executescript for multi-statement SQL

Synapse's schema setup (`prepare_database.py`) runs `;`-separated SQL
scripts via the engine's `executescript`. `Cursor.execute` can't serve
those: it `prepare`s the query, and Postgres rejects multiple commands in
a prepared statement.

Add `Cursor.executescript`, which runs the whole script on the simple-query
protocol (`batch_execute`), which does allow multiple statements. It takes
no parameters and produces no fetchable rows.

The script runs inside the connection's *current* transaction (opened
lazily like `execute`) and is left open for the caller to commit. It
deliberately does NOT reproduce the commit-any-pending-transaction-first
behaviour of `sqlite3.executescript` (which psycopg2's engine mirrors with a
leading `COMMIT`). That forced commit actually undercuts the atomicity
`prepare_database` sets out to get — it opens a transaction so "upgrades are
either applied completely, or not at all", but the first script's implicit
commit ends it. Running the script within the ongoing transaction instead
lets successive scripts accumulate and be committed once, which is both
simpler and more correct. The only engine-level piece left to layer on top
is the auto-increment placeholder substitution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik Johnston
2026-07-11 09:33:48 +00:00
co-authored by Claude Opus 4.8
parent 978c0296c4
commit f69433cd7b
2 changed files with 91 additions and 0 deletions
+26
View File
@@ -389,6 +389,32 @@ impl Cursor {
Ok(())
}
/// Execute a multi-statement SQL script (statements separated by `;`).
///
/// Unlike [`Cursor::execute`], which `prepare`s a single statement, this
/// runs the whole script on the simple-query protocol (`batch_execute`), so
/// it may contain many `;`-separated statements — as Synapse's schema files
/// do. It takes no parameters and produces no fetchable rows.
///
/// This is a thin primitive: the script runs inside the connection's current
/// transaction, opening one lazily like `execute` and leaving it open for
/// the caller to commit. The higher-level engine `executescript` — which
/// also substitutes the auto-increment placeholder — is layered on top.
/// Note it does *not* commit any prior transaction first: unlike the
/// psycopg2 engine (which still prefixes `COMMIT; BEGIN TRANSACTION;`,
/// committing script-by-script), the Rust engine deliberately keeps a whole
/// sequence of schema/delta scripts in one transaction so it is applied
/// either completely or not at all — see
/// `BaseDatabaseEngine.executescript`'s docstring for the contract.
fn executescript(&self, py: Python<'_>, script: &str) -> PyResult<()> {
// A script yields no fetchable rows, so drop any previous result set.
self.lock_state()?.new_query();
self.connection.with_client(py, |client| {
client.batch_execute(script).block_on_result(py)
})
}
/// Return the next row of the current result set, or `None` if exhausted.
fn fetch_one<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyTuple>>> {
self.lock_state()?.fetch_one(py)