Skip to content

perf(pg-delta): opt in to batched transactional apply - #468

Open
avallete wants to merge 1 commit into
avallete/230e62b4from
avallete/cli-2296-batched-apply
Open

perf(pg-delta): opt in to batched transactional apply#468
avallete wants to merge 1 commit into
avallete/230e62b4from
avallete/cli-2296-batched-apply

Conversation

@avallete

@avallete avallete commented Sep 8, 2026

Copy link
Copy Markdown
Member

Summary

  • Opt-in batchTransactional / --batch-transactional sends each transactional apply segment as bounded simple-protocol batches (BEGIN + preamble + actions) so high-latency links stop paying one RTT per action. Default remains one query per statement so we can fall back if batching mis-attributes a failure. COMMIT stays its own round trip (inDoubt = lost COMMIT only).
  • Failure attribution prefers Postgres POSITION through join offsets (parse-time and multi-statement actions), otherwise CommandComplete count. The joiner puts ; on its own line so a trailing -- cannot swallow the next statement.
  • Stacked on feat(pg-delta): split plans to fit the lock-table budget #462 because both rewrite apply.ts (lock-table probe + this executor).

Linked issue

Linear CLI-2296
Refs #462

  • The linked issue is open and carries the open-for-contribution label (or I'm a Supabase maintainer).

Checklist

  • Tests added or updated for the change
  • Changeset added if this is a user-facing fix/feature (bunx changeset)
  • bun run format-and-lint and bun run check-types pass

Test plan

  • cd packages/pg-delta && bun test src/apply/apply.test.ts src/apply/batch-query.test.ts
  • cd packages/pg-delta && bun test tests/apply-roundtrips.test.ts tests/apply-batch-failure.test.ts tests/apply-observer.test.ts
  • Confirm default apply still emits one query per action; --batch-transactional collapses a 200-action segment to O(1) RTTs plus COMMIT

High-latency apply was one RTT per action. Callers can now batch a
transactional segment into one simple-protocol query; the default stays
one statement per query so we can fall back if attribution mis-fires.
@changeset-bot

changeset-bot Bot commented Sep 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 38f0524

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@supabase/pg-delta Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 38f05249f9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

function stripTrailingSemicolons(sql: string): string {
return sql.replace(/;+\s*$/, "");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve action SQL instead of regex-transforming it

Remove this regex rewrite from the executor: the batch path now strips terminal semicolons and whitespace from every planner-rendered action before sending it, so apply no longer replays the action SQL verbatim. The repository explicitly prohibits regex-transforming SQL in engine replay paths; append batching delimiters without altering the original statement text instead.

AGENTS.md reference: AGENTS.md:L74-L78

Useful? React with 👍 / 👎.

Comment on lines +197 to +200
const completed = completedBeforeError(error);
if (completed === 0) return 0;
if (completed < batchLength) return completed;
return batchLength;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Map completion counts to actions rather than batch slots

When a batch action contains multiple top-level statements and a later error has no PostgreSQL position, completedBeforeError counts CommandComplete messages rather than entries in ranges. Returning that count directly advances past the actual action slot; for example, a two-statement action followed by a failing action and another queued action reports the latter action's index and SQL. Track how many completions belong to each action, isolate such actions, or otherwise avoid treating completion count as a batch index.

Useful? React with 👍 / 👎.

Comment on lines +110 to +112
function utf8Bytes(text: string): number {
return Buffer.byteLength(text, "utf8");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use a runtime-neutral UTF-8 byte counter

Replace the unimported Buffer global with TextEncoder or another runtime-neutral byte counter. partitionByBatchBounds() reaches this code whenever batchTransactional is enabled, but Buffer is not a standard Deno global, so the newly advertised public option can throw ReferenceError: Buffer is not defined before submitting its first batch under Deno. The package explicitly promises that this library is importable in Deno as well as Bun and Node.

AGENTS.md reference: AGENTS.md:L53-L55

Useful? React with 👍 / 👎.

Comment on lines +500 to +502
for (let i = segment.start; i < segment.end; i++) {
const action = actions[i]!;
assertActionSqlBatchable(action.sql);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Roll back when action validation fails

When the default path encounters an empty or transaction-control action, this assertion runs only after BEGIN has succeeded and is outside every rollback handler. It therefore rejects apply() but reaches the outer finally with destroyClient === false, releasing a client that is still inside the transaction back into the shared pool; the next borrower can inherit that transaction and its session state. Validate every action before opening a transaction, or catch this failure and issue ROLLBACK before releasing the client.

Useful? React with 👍 / 👎.

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.

1 participant