Skip to content

feat(database): afterCommit runs work after the outermost COMMIT - #100

Merged
Makisuo merged 5 commits into
MapleTechLabs:mainfrom
artile:feat/after-commit
Oct 25, 2026
Merged

Makisuo merged 5 commits into
MapleTechLabs:mainfrom
artile:feat/after-commit

Conversation

@artile

@artile artile commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

A vendor call (SMS, HTTP, job enqueue) must not run inside a transaction, and today each call site must wait for COMMIT by hand. This PR adds Db.afterCommit(effect): the effect runs only after the outermost transaction commits, and never after a rollback.

Closes #73.
Part of #52.

Changes

  • Db.afterCommit(effect) in /database. Like requireTransaction, it adds Transaction to R. The requirements of the queued effect stay in R.
  • Each transaction level keeps a queue on its private open-state service. The outermost level runs its queue after COMMIT, in queue order, one hook at a time, outside the transaction's span (in an effect_orm.after_commit span).
  • A savepoint that is kept hands its queue to its parent. A savepoint that rolls back drops its queue. With retry, only the attempt that commits runs its hooks.
  • A hook runs with the context of the place where it was queued. The transaction services (sql.transactionService, the open state, Transaction) are replaced with the ones outside the transaction, so a statement in a hook uses its own connection.
  • Failure policy (for maintainers to review): a hook that fails or dies does NOT fail the transaction. The data is committed, and a typed failure would invite a retry that writes twice. The cause goes to ErrorReporter.report (the reporters in context) and to Effect.logError, with the hook index as a log annotation. The next hook still runs. A caller that must know handles the error in the hook.
  • New defect AfterCommitUnavailable, when nothing would run the work: a Transaction provided by hand, a transaction that has ended (an unjoined fork), or an outermost transaction opened by a plain sql.withTransaction. The work is never dropped without a sign.
  • Tests: src/database/after-commit.test.ts (PGlite, 6 tests) and type tests in src/database/database.test-d.ts.
  • Docs: "After commit" section in docs/database.md, docs/reference.md, section 10 in design/transactions.md. One minor changeset.

Left out

  • Delivery guarantee. Hooks are at most once: a crash or interruption after COMMIT skips the hooks not yet run. Guaranteed delivery is an outbox table written in the transaction; it needs no library support. The docs say this.
  • Dialects other than Postgres. Upstream main has no SQLite, MySQL or D1 dialect yet. The queue lives in the dialect-free part of transaction, so a later dialect gets it without change. D1 batch() (D1 batch() as a typed atomic unit #55) is not covered.
  • Savepoints made by a plain sql.withTransaction inside a Db.transaction do not drop hooks when they roll back. The docs say to nest with Db.transaction.
  • A typed error for a failed hook. I chose report + log; I can change it to a typed error in the transaction's error channel if you prefer.

Validation

  • bun run build: pass.
  • bun run typecheck: pass.
  • bunx vitest run --testTimeout=60000 --hookTimeout=60000 with a live ClickHouse 26.8.2.7 (EFFECT_ORM_CLICKHOUSE_* set): 61 files passed, 1040 tests passed, 0 skipped.
  • node scripts/check-doc-citations.mjs, node scripts/check-exports-documented.mjs, node scripts/check-doc-examples.mjs: pass.
  • bun scripts/check-package.ts: pass.
  • Mutation check: running hooks in their queued context unchanged, or running a savepoint's hooks at once, each makes one test fail.
  • Upstream CI for fork PRs waits for a maintainer to approve the run.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 51 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8c103c98-c9b6-4a89-aa4c-e90856e2e349
Commits

Reviewing files that changed from the base of the PR and between 2ca58aa and 72e6204.

Files selected for processing: 8
  • .changeset/after-commit.md
  • design/transactions.md
  • docs/database.md
  • docs/reference.md
  • src/database.ts
  • src/database/after-commit.test.ts
  • src/database/database.test-d.ts
  • src/database/database.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Makisuo and others added 2 commits October 26, 2026 00:30
Effect 4.0.6 ships SqlClient.afterCommit with the same queueing rules:
after the outermost COMMIT, dropped on rollback, a kept savepoint hands
its callbacks to its parent, and a defect after the transaction ended.
Db.afterCommit now delegates to it and keeps only what Effect lacks:
Transaction in R, and a failing hook reported to ErrorReporter and the
log instead of failing the committed transaction.

The hook queue on OpenState, runHooks and AfterCommitUnavailable go.
A plain sql.withTransaction as the outer transaction now runs the hooks
instead of dying, since Effect queues them there too.

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

Makisuo commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Thanks for this. Since #125 bumped us to Effect 4.0.6, which ships SqlClient.afterCommit with the same queueing rules (outermost COMMIT, rollback and savepoint handling, a defect after the transaction ended), I've pushed a commit that builds Db.afterCommit on top of it:

  • It keeps your API: Transaction stays in R, and a failing hook is reported to ErrorReporter and the log without failing the committed transaction.
  • The hook queue on OpenState, runHooks and AfterCommitUnavailable are gone. The source change in database.ts is down from about 120 lines to about 40.
  • Behaviour change: when the outer transaction is a plain sql.withTransaction, hooks now run instead of dying, because Effect queues them there too.
  • Your tests are kept, with the "unavailable" test adjusted for that change. 1024 tests pass.

@Makisuo
Makisuo merged commit cca0525 into MapleTechLabs:main Oct 25, 2026
3 checks passed
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.

afterCommit: post-commit work bound to the transaction

2 participants