Skip to content

test(db): failing-first tests for the RAII fixes - #61

Merged
sebyx07 merged 8 commits into
mainfrom
fix/db-raii-resources
Aug 13, 2026
Merged

test(db): failing-first tests for the RAII fixes#61
sebyx07 merged 8 commits into
mainfrom
fix/db-raii-resources

Conversation

@sebyx07

@sebyx07 sebyx07 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Closes out PR group "RAII — db resource correctness by construction" with regression tests for every leak the prior commits on this branch closed.
  • transaction.test.ts: nesting three deep still takes and releases exactly one pin (a second reserve() per SAVEPOINT level would leak).
  • pglite.test.ts: a failed BEGIN gives the turn back — the next statement runs instead of queuing behind an unreachable reservation.
  • New migrate.live.test.ts against a real Postgres: two concurrent migrate() calls serialize (one applies, the other skips, never a unique-violation race), and the advisory lock releases after a failed migration so the next migrate() finishes instead of hanging.
  • Verified both new live cases fail against the pre-fix migrate.ts (lock taken on a pooled, not pinned, handle) before restoring the fix — genuinely failing-first.

Test plan

  • bun test packages/db (149 pass incl. live, against a local Postgres)
  • bun run typecheck
  • bun run verify (12/17, 5 honest skips — repo baseline unchanged)

🤖 Generated with Claude Code


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

Summary by CodeRabbit

  • New Features

    • Migrations and rollbacks now use session-pinned advisory locks by default, ensuring overlapping deployments wait and run safely in sequence.
    • Added an option to disable migration locking when needed.
    • Database connections and transaction turns now support automatic disposal.
  • Bug Fixes

    • Improved cleanup after failed transactions, savepoints, read-only queries, and connection shutdowns.
    • Prevented rejected shutdowns and repeated releases from leaving connections unusable.
    • Preserved original transaction errors when rollback also fails.
  • Documentation

    • Updated migration, deployment, and error-code guidance to describe serialized concurrent migrations.

sebyx07 and others added 7 commits August 12, 2026 18:35
…once

- `reserve()`'s handle runs direct only while the pin is held; a statement
  issued after `release()` takes its own connection out of the pool instead of
  landing inside whichever unit of work holds that connection now
- `release()` is idempotent on both drivers — `withTransaction`'s `finally` and
  disposal are two owners on one exit path
- `DbConnection extends Disposable`; `[Symbol.dispose]` is `release()` itself,
  so `using connection = await client.reserve()` is the shape

Co-Authored-By: Claude <noreply@anthropic.com>
- `PostgresClient.close()` dropped its driver handle after the await, so a
  teardown that threw left the dead pool cached; the next `connect()` handed
  it back and no second `close()` could clear it
- read-then-clear, matching `pglite.ts`: the rejection still reaches the
  caller, the client is empty either way, and a `connect()` racing the
  teardown opens a fresh pool
- 4 tests, two of which fail without the fix; CHANGELOG + db/CLAUDE.md

Co-Authored-By: Claude <noreply@anthropic.com>
- withTransaction reserved a connection, ran BEGIN above its `try`, and
  released in that block's `finally` — so a rejecting BEGIN leaked the
  reservation forever. On PGlite that is the single session's turn, and
  every later statement in the process waits on it.
- The pin is now held by a `using` declaration and BEGIN sits inside the
  guarded scope. readOnlyQuery, which already had the right shape, is
  converted to the same declaration so both sites read alike.
- ROLLBACK TO SAVEPOINT is best-effort, matching the root's ROLLBACK: a
  dead connection no longer replaces the error that caused the rollback.
  SAVEPOINT and RELEASE stay uncaught, and the reason is written down.
- 8 tests; 2 of them fail without this change.

Co-Authored-By: Claude <noreply@anthropic.com>
- `withAdvisoryLock` reserves a connection (`using`), locks on it, and hands
  that session to its callee: `pg_advisory_lock` is session-scoped, so a lock
  taken on the pool was unlocked on a different connection (answers false, held
  until the backend dies) and its idle holder could be closed by the pool's idle
  timeout mid-migration. `ROLE=migrate` masked it with `max: 1`
- ledger, audit and every migration transaction now run on the locked session —
  also the only thing that works on a `max: 1` pool
- `rollback()` took no lock at all; it takes the same one, with the same
  `lock: false` escape hatch for a private branch database
- 6 tests over a pin-observable pool; 4 fail without the fix

Co-Authored-By: Claude <noreply@anthropic.com>
- Turn (pglite-turns.ts) gains release()/[Symbol.dispose], matching
  DbConnection's shape; TurnQueue.run() holds its turn with using
  instead of a hand-rolled try/finally.
- pglite.ts's reserve() calls turn.release() where it used to call
  turn() directly, since its turn outlives the function and can't be
  scoped with using.
- CHANGELOG + packages/db/CLAUDE.md updated in lockstep.

Co-Authored-By: Claude <noreply@anthropic.com>
- New packages/db/src/type-pins.ts asserts DbConnection extends Disposable
  and Turn extends Disposable at compile time
- Regression on either interface is now a typecheck failure, not a
  silently-degraded `using` guard

Co-Authored-By: Claude <noreply@anthropic.com>
- transaction.test.ts: nesting three deep still takes and releases exactly
  one pin — a second reserve() per SAVEPOINT level would leak a connection
  the root's `using` never sees.
- pglite.test.ts: a failed BEGIN gives the turn back, so the next statement
  runs instead of queuing behind a reservation nobody can reach.
- migrate.live.test.ts (new): against a real Postgres — two concurrent
  migrate() calls serialize (one applies, the other skips, never a
  unique-violation race), and the lock releases after a failed migration so
  the next migrate() finishes instead of hanging on a lock left stuck.
  Verified both new live cases fail against the pre-fix migrate.ts
  (advisory lock taken on a pooled, not pinned, handle).

Co-Authored-By: Claude <noreply@anthropic.com>
@sebyx07 sebyx07 added the claudetm Created by Claude Task Master label Aug 13, 2026
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 017cb51f-1c9f-4d0c-9eb4-204485020026

📥 Commits

Reviewing files that changed from the base of the PR and between d848c98 and c64fae0.

📒 Files selected for processing (6)
  • packages/db/CLAUDE.md
  • packages/db/src/fake-reservable.ts
  • packages/db/src/readonly-query.test.ts
  • packages/db/src/transaction.test.ts
  • wiki/Entities-And-Migrations.md
  • wiki/Troubleshooting.md
📝 Walkthrough

Walkthrough

The database package now uses disposable connection and turn lifecycles, guarded transaction cleanup, and session-pinned advisory locks for migrations and rollbacks. Tests and documentation cover cleanup, failure handling, concurrent migrations, and lock configuration.

Changes

Database resource lifecycle

Layer / File(s) Summary
Disposable connections, turns, and teardown
packages/db/src/client.ts, packages/db/src/client.test.ts, packages/db/src/pglite.ts, packages/db/src/pglite-turns.ts, packages/db/src/pglite*.test.ts, packages/db/src/type-pins.ts
DbConnection and Turn implement Disposable. Release is idempotent. Released connections use pooled execution. Pool teardown clears the cached driver before awaiting.
Guarded transaction cleanup
packages/db/src/transaction.ts, packages/db/src/transaction.test.ts, packages/db/src/readonly-query.ts, packages/db/src/readonly-query.test.ts
Transactions and read-only queries use using for reservation cleanup. Failed BEGIN operations release reservations. Nested rollback errors are suppressed while the original transaction error is preserved.

Session-pinned migration locking

Layer / File(s) Summary
Migration and rollback session affinity
packages/db/src/migrate.ts, packages/db/src/migrate.test.ts, packages/db/src/migrate.live.test.ts
Migration and rollback advisory locks, ledger operations, transactions, and unlocks use one reserved session. lock: false disables advisory locking.
Migration behavior documentation
wiki/CLI-Reference.md, wiki/Deployment.md, wiki/Entities-And-Migrations.md, wiki/Error-Codes.md, wiki/Known-Gaps.md, CHANGELOG.md, packages/db/CLAUDE.md
Documentation describes whole-run session-pinned locking, waiting concurrent deployments, disposal behavior, and transaction cleanup rules.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score: 🟡 Moderate · up to d848c

The PR adds regression coverage and serializes concurrent migrations, but a held migration lock can still leave a deployment waiting indefinitely without a bounded failure or recovery path. Merge should wait for a lock timeout/error solution or explicit owner acceptance; the remaining findings are limited to test, documentation, and maintainability cleanup.

Sequence Diagram(s)

sequenceDiagram
  participant Deployment
  participant migrate
  participant DbConnection
  participant PostgreSQL
  Deployment->>migrate: start migration
  migrate->>DbConnection: reserve pinned session
  DbConnection->>PostgreSQL: acquire advisory lock
  migrate->>PostgreSQL: run migration transaction
  migrate->>PostgreSQL: release advisory lock
  migrate->>DbConnection: dispose reservation
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the database RAII regression tests, which are the stated focus of the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/db-raii-resources

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/db/src/migrate.ts (2)

137-178: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

migrate.ts now needs "and" to describe it.

The file applies migrations, keeps the ledger honest, and owns session-pinned advisory locking. packages/db/CLAUDE.md states the rule: "Files | < 200 LOC, one responsibility, kebab-case.ts, test beside source". This file is roughly 265 lines after the change.

Move withAdvisoryLock, MIGRATION_LOCK_KEY, and the lock's doc comment into packages/db/src/migration-lock.ts with a test beside it. migrate.ts then imports one named function, and the lock gets its own failing-first test file rather than sharing migrate.test.ts.

As per coding guidelines: "Files | < 200 LOC, one responsibility, kebab-case.ts, test beside source".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/src/migrate.ts` around lines 137 - 178, Extract
MIGRATION_LOCK_KEY, withAdvisoryLock, and its documentation into a new
migration-lock.ts module, preserving the existing session-pinning and unlock
behavior. Update migrate.ts to import and call the named lock function rather
than defining it locally. Add a colocated migration-lock.test.ts covering the
lock behavior, including failure cleanup, instead of extending migrate.test.ts.

Source: Coding guidelines


154-178: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

pg_advisory_lock blocks forever, and a hang is the one failure an agent cannot read.

Line 166 takes the lock with no bound. If another session holds it — a wedged migrator, a paused deploy, a psql window someone left open — this call waits with no output, no code, and no fix. The live test at packages/db/src/migrate.live.test.ts lines 80-83 documents that exact shape: the caller "blocks until that connection's idle timeout fires". Axiom 4 wants a stable X_* code and a runnable fix. An indefinite wait produces neither.

Set lock_timeout on the pinned session before taking the lock, then convert the timeout into a coded DbError with a fix command.

🔒️ Proposed bound on the lock wait
   const session: DbClient = pinned ?? client;
-  await session.execute(sql`select pg_advisory_lock(${MIGRATION_LOCK_KEY})`);
+  // Bounded on purpose: an unbounded `pg_advisory_lock` turns "another migrator is running" into a
+  // deploy that hangs with no code and no fix — unreadable to the agent driving `x db migrate`.
+  await session.execute(raw(`SET lock_timeout = ${MIGRATION_LOCK_TIMEOUT_MS}`));
+  try {
+    await session.execute(sql`select pg_advisory_lock(${MIGRATION_LOCK_KEY})`);
+  } catch (error) {
+    throw migrationLockBusy(
+      `another session has held the migration lock for more than ${MIGRATION_LOCK_TIMEOUT_MS}ms`,
+      'x db status --json   # find the running migrator, then retry once it finishes',
+      error,
+    );
+  }
   try {

migrationLockBusy belongs in packages/db/src/errors.ts beside migrationConflict, with its own X_* code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/src/migrate.ts` around lines 154 - 178, Update withAdvisoryLock
to set a bounded lock_timeout on the selected session before pg_advisory_lock,
using the existing migration error patterns. Catch a lock-timeout failure and
throw the new migrationLockBusy DbError defined beside migrationConflict in
errors.ts, including its unique X_* code and runnable fix command; preserve
normal lock acquisition and cleanup behavior for other outcomes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/db/src/pglite-turns.test.ts`:
- Line 88: Replace the bare errors thrown in
packages/db/src/pglite-turns.test.ts:88,
packages/db/src/transaction.test.ts:277, and
packages/db/src/transaction.test.ts:311 with dbUnavailable() or another
UltimateError subclass carrying a stable X_* code, cause, and exact fix command.
Preserve the existing assertions that verify cleanup retains the original typed
failure.

In `@packages/db/src/transaction.test.ts`:
- Around line 15-52: Extract the shared PinCounts and reservableOver reservation
fixture into one reusable db test fixture module, preserving the existing
reservation counting and idempotent release behavior. In
packages/db/src/transaction.test.ts lines 15-52 and
packages/db/src/readonly-query.test.ts lines 12-48, remove the local fixture
definitions and import the shared reservableOver fixture instead.

In `@wiki/Entities-And-Migrations.md`:
- Line 167: Update the Errors table in wiki/Entities-And-Migrations.md to remove
the active migration-failure entry for X_MIGRATE_CONCURRENT or explicitly mark
it as reserved, matching the status described in the pre-deploy row and
wiki/Error-Codes.md. Keep the error documentation consistent and avoid
presenting this code as thrown.

---

Outside diff comments:
In `@packages/db/src/migrate.ts`:
- Around line 137-178: Extract MIGRATION_LOCK_KEY, withAdvisoryLock, and its
documentation into a new migration-lock.ts module, preserving the existing
session-pinning and unlock behavior. Update migrate.ts to import and call the
named lock function rather than defining it locally. Add a colocated
migration-lock.test.ts covering the lock behavior, including failure cleanup,
instead of extending migrate.test.ts.
- Around line 154-178: Update withAdvisoryLock to set a bounded lock_timeout on
the selected session before pg_advisory_lock, using the existing migration error
patterns. Catch a lock-timeout failure and throw the new migrationLockBusy
DbError defined beside migrationConflict in errors.ts, including its unique X_*
code and runnable fix command; preserve normal lock acquisition and cleanup
behavior for other outcomes.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro

Run ID: be057d59-464c-4aaa-9e65-65b102c836ec

📥 Commits

Reviewing files that changed from the base of the PR and between 467cb45 and d848c98.

📒 Files selected for processing (21)
  • CHANGELOG.md
  • packages/db/CLAUDE.md
  • packages/db/src/client.test.ts
  • packages/db/src/client.ts
  • packages/db/src/migrate.live.test.ts
  • packages/db/src/migrate.test.ts
  • packages/db/src/migrate.ts
  • packages/db/src/pglite-turns.test.ts
  • packages/db/src/pglite-turns.ts
  • packages/db/src/pglite.test.ts
  • packages/db/src/pglite.ts
  • packages/db/src/readonly-query.test.ts
  • packages/db/src/readonly-query.ts
  • packages/db/src/transaction.test.ts
  • packages/db/src/transaction.ts
  • packages/db/src/type-pins.ts
  • wiki/CLI-Reference.md
  • wiki/Deployment.md
  • wiki/Entities-And-Migrations.md
  • wiki/Error-Codes.md
  • wiki/Known-Gaps.md

Comment thread packages/db/src/pglite-turns.test.ts
Comment thread packages/db/src/transaction.test.ts Outdated
Comment thread wiki/Entities-And-Migrations.md
- extract PinCounts/reservableOver into fake-reservable.ts; transaction
  and readonly-query tests import it instead of each keeping a copy
- drop the X_MIGRATE_CONCURRENT row from Entities-And-Migrations' Errors
  table and rewrite Troubleshooting's: the code is reserved and never
  thrown, so a table of thrown codes must not list it and the symptom a
  reader actually sees is a migrate that waits
- packages/db/CLAUDE.md: name which throws are typed and which are the
  caller's arbitrary failure, and point tests at the shared fixture

Co-Authored-By: Claude <noreply@anthropic.com>
@developerz-ai

developerz-ai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Looks ready — CI is green, CodeRabbit approved, and this is a routine patch with regression tests. Ready to merge when you're ready.

🤖 Posted by developerz.ai — the maintainer agent, not a human.

@sebyx07
sebyx07 merged commit dc05e11 into main Aug 13, 2026
5 checks passed
@sebyx07
sebyx07 deleted the fix/db-raii-resources branch August 13, 2026 00:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claudetm Created by Claude Task Master

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant