Skip to content

Add execute_script for running multi-statement SQL - #25

Draft
PingoLee wants to merge 1 commit into
JuliaDatabases:mainfrom
PingoLee:execute-script
Draft

PingoLee wants to merge 1 commit into
JuliaDatabases:mainfrom
PingoLee:execute-script

Conversation

@PingoLee

@PingoLee PingoLee commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

I went ahead and built this so #23 has something concrete to look at; think of it as a long comment on the issue that happens to compile. PormG's migrations kept running into the limitation, which was motivation enough.

I'm happy to change the name, the return value or the COPY behavior, or to close this if you prefer a different approach or would rather write it yourself.

DBInterface.execute always uses the extended protocol, so a string holding several statements fails, even without parameters:

DBInterface.execute(conn, "CREATE TEMP TABLE t (a int); COMMENT ON TABLE t IS 'one; two'")
# Postgres.Error 42601: cannot insert multiple commands into a prepared statement

libpq runs the same string with PQexec, which is how migration and schema scripts are usually sent.

Postgres.execute_script(conn, sql) sends sql as one simple-query message and returns the command tag of each statement, e.g. ["CREATE TABLE", "COMMENT"]. Result rows are discarded; DBInterface.execute remains the way to run queries. It reuses the existing simple-query building blocks but has its own read loop, as copy_in and copy_out do, so execute_simple and the transaction helpers are unchanged.

  • The first server error is thrown after draining to ReadyForQuery, so the connection stays usable, and server_in_transaction is refreshed from ReadyForQuery either way.
  • COPY ... FROM STDIN in a script is aborted with CopyFail and rejected with a PostgresInterfaceError pointing at copy_from. COPY ... TO STDOUT is drained and rejected the same way as in execute; because the simple protocol cannot abort a copy-out, the docstring says the rest of the script has already run by then.
  • The query logger reports it as :execute_script.
  • Docs: a docstring, a manual section and a README section.

Choices I made that you may want differently: the name; returning only the tags; rejecting COPY rather than returning its tag.

Adds one public name, execute_script. No runtime dependency changes. Closes #23.

Validation:

  • New fake-server testset (no Docker): tags with rows, notices, ParameterStatus and notifications interleaved; first error kept and the connection reused; COPY in both directions; CopyBoth closing the connection; an empty string. Reverting the CopyFail, copy-out or ParameterStatus handling makes it fail.
  • New "Execute Script" integration testset: a ; inside a literal and a dollar-quoted body; implicit-transaction rollback when a later statement fails; VACUUM refused inside a multi-statement string (25001), as with PQexec; BEGIN/COMMIT inside a script tracked; COPY rejected without desync.
  • Pkg.test() with local Docker integration: 8846 checks pass on Julia 1.12.7 against PostgreSQL 14 (MD5), 16 and 18 (SCRAM), and 8819 on Julia 1.10.12; it also passes with -t 2 (Linux x64).
  • Fork CI on 425f713: tests pass on Linux, Windows and macOS (Julia min, 1 and pre), with Docker integration against PostgreSQL 14, 15, 17 and 18 and SCRAM and MD5 auth. Only the Codecov upload failed, because the fork has no token.
Additional checks against a real server
  • 500 seeded random scripts built from lexical traps (E'…' escapes, a trailing backslash in a standard string, quoted identifiers with "", nested $tag$ bodies, nested block comments, U&'…', comments containing ;) each returned exactly one tag per statement on PostgreSQL 16.
  • cancel_query! from another task during SELECT pg_sleep(5); SELECT 1 raises 57014, and the connection stays usable.
  • An ORM's migration suite (PormG, 1113 checks) passes with its multi-statement migration entries sent whole through execute_script, with the same result as sending them through LibPQ's PQexec.

This PR was written with AI assistance (Claude Code) and reviewed by me before opening. The checks above were run as described.

🤖 Generated with Claude Code

DBInterface.execute always uses the extended protocol, so a string holding
several statements fails with 42601 "cannot insert multiple commands into a
prepared statement". libpq runs the same string with PQexec over the
simple-query protocol, which is how migration and schema scripts are usually
sent (JuliaDatabases#23).

Postgres.execute_script(conn, sql) sends the string as one Query message and
returns the command tag of each statement, discarding rows. The first server
error is thrown after draining to ReadyForQuery, so the connection stays
usable, and the transaction status is refreshed either way. COPY in a script
is rejected: copy-in is aborted with CopyFail, copy-out is drained. The query
logger reports it as :execute_script. DBInterface.execute and the internal
execute_simple are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provide an explicit way to run SQL containing several statements

1 participant