Skip to content

feat(sqlite): add SqliteConnection::execute_batch for running bound queries in one worker command - #4511

Open
LeenHawk wants to merge 1 commit into
transact-rs:mainfrom
LeenHawk:sqlite-execute-batch
Open

LeenHawk wants to merge 1 commit into
transact-rs:mainfrom
LeenHawk:sqlite-execute-batch

Conversation

@LeenHawk

@LeenHawk LeenHawk commented Nov 7, 2026

Copy link
Copy Markdown

Does your PR solve an issue?

fixes #4510

Adds SqliteConnection::execute_batch(), which runs a sequence of bound queries in one command to the worker thread instead of one command per query:

let mut tx = conn.begin().await?;
let results = tx
    .execute_batch(rows.iter().map(|row| {
        sqlx::query("INSERT INTO item (name, value) VALUES (?, ?)")
            .bind(&row.name)
            .bind(row.value)
    }))
    .await?;
tx.commit().await?;
  • It takes any Execute<'_, Sqlite>. Arguments are encoded and persistent() is honoured exactly as in fetch_many(). On the worker, each query goes through the same execute::iter as Command::Execute, so repeating one SQL string with different arguments uses one cached statement.
  • Queries run in order and stop at the first error. SqliteBatchError reports the index and converts into sqlx::Error. Earlier queries are not rolled back.
  • SqliteBatchResult exposes rows_affected(), last_insert_rowid(), rows(), into_rows() and into_parts(). Its fields are private, so it can be extended.
  • If the caller drops the future, the worker finishes the current query and checks Sender::is_canceled() before the next one.
  • It is an inherent method rather than part of Executor, following the discussion in Discussion: deprecation of execute_many() and fetch_many() on prepared statement interfaces and potential replacement #3108.

Inserting 100,000 three-column rows on a Ryzen 7 8745H, committing every N rows (:memory:, median of 5 runs):

rows per transaction execute per query execute_batch speedup
1 ~40,000 rows/s ~40,000 rows/s 1.0x
10 ~104,000 ~307,000 ~2.9x
100 ~119,000 ~575,000–640,000 ~5x
1000 ~70,000 ~715,000–775,000 ~10x

A WAL file database with synchronous = NORMAL shows the same shape (2.5x–11x).

Tests in tests/sqlite/sqlite.rs cover:

  • ordered execution with bind parameters, RETURNING rows, and statement cache reuse;
  • mixing UPDATE, SELECT and a multi-statement string in one batch;
  • an empty batch;
  • stopping at the first error, with the right index, earlier writes kept, later ones skipped, and the connection still usable;
  • rolling back a failed batch inside a transaction.

Locally, with DATABASE_URL=sqlite:tests/sqlite/sqlite.db and the CI feature set, sqlite, sqlite-types and sqlite-any tests pass. The CI cargo clippy ... -- -D warnings command and cargo fmt --check pass too.

Is this a breaking change?

No. It only adds a method and two types (SqliteBatchResult, SqliteBatchError) to sqlx-sqlite, plus a variant to the private worker Command enum.

Copilot AI balanced review requested due to automatic review settings November 7, 2026 17:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

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.

SQLite: execute a sequence of bound queries in one worker command

2 participants