Skip to content

feat(database): put transaction settings in BEGIN through SqlClient.BeginTransaction - #101

Merged
Makisuo merged 3 commits into
MapleTechLabs:mainfrom
artile:feat/begin-transaction
Nov 2, 2026
Merged

Makisuo merged 3 commits into
MapleTechLabs:mainfrom
artile:feat/begin-transaction

Conversation

@artile

@artile artile commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Db.transaction sends its isolation level and access mode as a separate SET TRANSACTION after BEGIN. effect 4.0.6 adds SqlClient.BeginTransaction (Effect-TS/effect#9576), which lets one BEGIN carry the settings and saves that round trip.

Closes #72.
Part of #52.

Changes

  • New optional hook DialectTransactions.beginTransaction(settings). It writes the BEGIN that carries the settings. postgresDialect writes BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY, DEFERRABLE (same mode list as its SET TRANSACTION).
  • transaction uses the hook when the installed effect has SqlClient.BeginTransaction. Then it sends no SET TRANSACTION. Otherwise it sends SET TRANSACTION as before.
  • The installed effect is still 4.0.0, which has no BeginTransaction. I did not bump effect. A small internal seam, src/database/begin-transaction.ts, reads the reference by name (Reflect.get, so a bundler does not flag a missing export). On effect 4.0.0 to 4.0.5 it is undefined, and behaviour does not change.
  • Inside the body, the reference is cleared. Effect applies it to every top-level transaction started while it is provided, so without this a transaction the body starts on a second client would take our BEGIN.
  • observe sees the BEGIN statement, so a statement collector still sees the settings.
  • A failed BEGIN reports the real statement in DatabaseError.sql.
  • Second commit: effect 4.0.6 reports a failed COMMIT as a SqlError failure, where 4.0.0 died. A SqlError failure after the body succeeded is now TransactionCommitFailed too, so the typed error survives the bump. ROLLBACK still dies on 4.0.6 and is handled as before. Contention retry is unchanged.
  • Docs: docs/database.md (Settings, observe, Errors), docs/reference.md, design/transactions.md (sections 1, 4.8, 9). One minor changeset.

What changes when effect is bumped to 4.0.6

  • Postgres transactions with settings start with one BEGIN ... and send no SET TRANSACTION. No code change is needed; the seam finds the reference.
  • When the peer range moves to ^4.0.6, the seam can become a plain SqlClient.BeginTransaction import. SET TRANSACTION then stays only for dialects without beginTransaction.
  • Not part of this PR: on 4.0.6 one tracing test fails because Effect renamed its sql.execute span (src/database/tracing.test.ts). It does not depend on this change.

Left out

  • SQLite BEGIN IMMEDIATE and MySQL START TRANSACTION READ ONLY. Upstream main has no SQLite or MySQL dialect. The hook is the place for them. MySQL isolation still needs SET TRANSACTION before BEGIN, which neither hook can do on a pooled client (design/transactions.md 4.8).
  • The effect bump itself, for the reason above.
  • Tracing the BEGIN as an effect_orm statement span. Effect's own sql.transaction span covers it.

Validation

  • Both paths are tested on effect 4.0.0. A test client reads a stand-in reference where effect 4.0.6 reads BeginTransaction. A second test runs with no reference and checks SET TRANSACTION. A third test gives a COMMIT failure in the error channel, as 4.0.6 does.
  • In a throwaway copy with effect@4.0.6 and @effect/sql-pglite@4.0.6 (not committed), src/database/database.test.ts and src/pg: 66 passed, 5 files. This includes the real PGlite check that BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY, DEFERRABLE sets transaction_isolation and transaction_read_only.
  • bun run build: ok.
  • bun run typecheck: ok.
  • bunx vitest run --testTimeout=60000 --hookTimeout=60000 with live ClickHouse 26.8.2.7: 60 files passed, 1037 tests passed, 0 skipped.
  • node scripts/check-doc-citations.mjs, node scripts/check-doc-examples.mjs, node scripts/check-exports-documented.mjs: ok.
  • bun scripts/check-package.ts: ok.
  • Upstream CI for a fork PR 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.

artile and others added 2 commits October 25, 2026 17:07
…eginTransaction

effect 4.0.6 adds SqlClient.BeginTransaction (Effect-TS/effect#9576), a
reference that replaces the client's BEGIN for one top-level transaction.
When the installed effect has it and the dialect gives the new optional
DialectTransactions.beginTransaction hook, transaction() starts with
BEGIN ISOLATION LEVEL ..., READ ONLY, DEFERRABLE and sends no
SET TRANSACTION. On effect 4.0.0 to 4.0.5 nothing changes.

The reference is read by name, so the peer range stays ^4.0.0, and it is
cleared inside the body so a transaction there on another client keeps
its own BEGIN.

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

effect 4.0.6 fails with the SqlError on a failed COMMIT, where 4.0.0 died.
A SqlError failure after the body succeeded can only be COMMIT's, so it
is TransactionCommitFailed on both versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@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 43 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: 317f6cea-43c8-4c34-a6c0-ced42c58c799
Commits

Reviewing files that changed from the base of the PR and between b57faf1 and ccdc8a7.

Files selected for processing: 10
  • .changeset/begin-transaction-settings.md
  • design/transactions.md
  • docs/database.md
  • docs/reference.md
  • src/ch/dialect.ts
  • src/database/after-commit.test.ts
  • src/database/database.test.ts
  • src/database/database.ts
  • src/pg/dialect.ts
  • src/sqlite/sqlite.test.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

…nsaction directly

main requires effect ^4.0.6 and already turns a COMMIT SqlError failure into
TransactionCommitFailed, so the by-name seam (begin-transaction.ts), the
makeDatabase test entry point and this branch's duplicate commit-failure
path are dropped in favour of main's restoreCause. transaction provides
SqlClient.BeginTransaction itself; a dialect without beginTransaction keeps
SET TRANSACTION, and SQLite keeps its client's BEGIN IMMEDIATE.

Tests: an afterCommit hook after a transaction with settings starts its own
transaction with the default BEGIN; SQLite sends no BEGIN override.
Moves the postgresDialect JSDoc back onto postgresDialect.

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

Makisuo commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Merge-readiness pass (head ccdc8a7):

Changed

  • Merged main. Main requires effect ^4.0.6, so I removed the version seam (src/database/begin-transaction.ts, Reflect.get) and the makeDatabase test entry point. transaction now provides SqlClient.BeginTransaction directly.
  • This branch's second commit (a COMMIT SqlError failure becomes TransactionCommitFailed) duplicated what main already does in restoreCause. I kept main's version and only carried over the change that a failed BEGIN reports the real BEGIN statement as DatabaseError.sql.
  • A dialect without beginTransaction still sends SET TRANSACTION. SQLite defines neither, so @effect/sql-sqlite-node keeps its own BEGIN IMMEDIATE, and D1 still refuses transactions. Added a SQLite test: a serializable transaction sends no BEGIN override and an access mode is rejected.
  • afterCommit interaction (feat(database): afterCommit runs work after the outermost COMMIT #100): added a PGlite test. A hook that runs after a repeatable read transaction sees BeginTransaction unset, and a Db.transaction it starts runs at read committed. So the settings do not leak into it.
  • Tests use the real SqlClient.BeginTransaction. A recording client checks what each transaction starts with, and a dialect without the hook checks the SET TRANSACTION fallback. I dropped the simulated "4.0.6 COMMIT failure" test, which main's real COMMIT test already covers. Moved the postgresDialect JSDoc back onto postgresDialect; it had ended up above transactionModes. Updated docs, design/transactions.md and the changeset for the 4.0.6 baseline.

Verified

  • bun run typecheck, then bun run test with a live ClickHouse 26.8: 73 files, 1434 tests, plus doc checks, all green. The PGlite test confirms BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY, DEFERRABLE is applied (transaction_isolation, transaction_read_only).
  • FUZZ_SCALE=0.3 vitest run tests/fuzz: 269 passed.
  • CI run 41473746753 failed only on the known ClickHouse "Division by zero" flake in "generated fixtures: union results agree (clickhouse)", which test(fuzz): fix three flaky generated-data properties #131 fixes.

@Makisuo
Makisuo merged commit 164ddb1 into MapleTechLabs:main Nov 2, 2026
2 of 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.

Use SqlClient.BeginTransaction for transaction settings

2 participants