Skip to content

fix(rivetkit): wrap onMigrate in savepoints#4960

Merged
NathanFlurry merged 1 commit intomainfrom
rivetkit-db/onmigrate-savepoint
May 5, 2026
Merged

fix(rivetkit): wrap onMigrate in savepoints#4960
NathanFlurry merged 1 commit intomainfrom
rivetkit-db/onmigrate-savepoint

Conversation

@NathanFlurry
Copy link
Copy Markdown
Member

@NathanFlurry NathanFlurry commented May 4, 2026

Stack Context

This PR updates RivetKit SQLite migration handling.

What?

  • Wrap raw SQL and Drizzle onMigrate work in a SQLite savepoint.
  • Roll back the savepoint when onMigrate throws.
  • Remove the dead writeMode API from active SQLite surfaces and examples.
  • Document that onMigrate is atomic because it runs inside a savepoint.

Why?

writeMode no longer routes anything specially, so onMigrate was not atomic by default. A savepoint gives migrations transaction semantics without using a top-level BEGIN, which keeps the wrapper nestable with SQLite's transaction model.

Verification

  • pnpm run check-types
  • pnpm vitest run src/common/database/mod.test.ts src/common/database/native-database.test.ts
  • git diff --check

@NathanFlurry NathanFlurry changed the title fix(rivetkit): wrap onMigrate in savepoint fix(rivetkit): wrap onMigrate in savepoints May 4, 2026
This was referenced May 4, 2026
Copy link
Copy Markdown
Member Author

NathanFlurry commented May 4, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 4, 2026

Review: fix(rivetkit): wrap onMigrate in savepoints

Overview

This PR replaces the dead writeMode API with a proper SQLite savepoint around onMigrate in both the raw SQL (mod.ts) and Drizzle (drizzle.ts) database providers. The change is correct and well-motivated: writeMode was a no-op wrapper that gave false atomicity guarantees, and a real SAVEPOINT is the right SQLite primitive for nestable transaction semantics.


Issues

Duplicate withMigrationSavepoint implementation

The function is copy-pasted verbatim into both mod.ts and drizzle.ts. Any future bugfix must be applied to both files. Extract it into a shared internal utility (e.g. src/common/database/savepoint.ts) and import it from both.

Original error lost when ROLLBACK itself fails

In the catch block in both files, if ROLLBACK TO SAVEPOINT throws, the new error propagates through finally and the original error from the user migration callback is silently swallowed. A bare catch on the rollback call preserves the original:

} catch (error) {
    try {
        await client.execute("ROLLBACK TO SAVEPOINT __rivet_on_migrate");
    } catch {
        // rollback failed; original error is still re-thrown below
    } finally {
        await client.execute("RELEASE SAVEPOINT __rivet_on_migrate");
    }
    throw error;
}

Missing Drizzle test coverage

mod.test.ts gets both a success test and a rollback test for raw SQL. The Drizzle withMigrationSavepoint is identical code but has no analogous coverage, especially for the rollback path and the new early-return guard (if (!migrations && !onMigrate) return).


Observations (no action required)

  • The hardcoded savepoint name __rivet_on_migrate is fine. Worth noting that user migration code using the exact same name would conflict, but that is an acceptable risk.
  • The early-return guard added to the Drizzle path (if (!migrations && !onMigrate) return) is a nice optimization. The raw SQL path already has an equivalent if (onMigrate) guard, so no SAVEPOINT is opened unnecessarily in either case.
  • Removing writeMode from SqliteDatabase is a breaking API change for callers of ctx.sql.writeMode(...). If writeMode was ever documented as public API, a changelog entry may be warranted.
  • The ROLLBACK TO SAVEPOINT followed by RELEASE SAVEPOINT sequence is correct SQLite semantics: rollback rewinds without popping the savepoint stack; release then pops it.

Summary

The core logic is correct and the removal of the defunct writeMode API is welcome. Two items worth addressing before merge: extract the duplicated withMigrationSavepoint helper into a shared file, and ensure the original migration error is preserved when the ROLLBACK statement itself fails.

@NathanFlurry NathanFlurry mentioned this pull request May 4, 2026
11 tasks
@NathanFlurry NathanFlurry changed the base branch from runner-config/drain-grace-threshold to graphite-base/4960 May 5, 2026 12:00
@NathanFlurry NathanFlurry force-pushed the rivetkit-db/onmigrate-savepoint branch from c4c18dc to 627fd20 Compare May 5, 2026 12:09
@NathanFlurry NathanFlurry force-pushed the graphite-base/4960 branch from f8261ca to 4bec251 Compare May 5, 2026 12:09
@NathanFlurry NathanFlurry changed the base branch from graphite-base/4960 to runner-config/drain-grace-threshold May 5, 2026 12:09
Base automatically changed from runner-config/drain-grace-threshold to main May 5, 2026 14:58
@NathanFlurry NathanFlurry merged commit 627fd20 into main May 5, 2026
8 of 22 checks passed
@NathanFlurry NathanFlurry deleted the rivetkit-db/onmigrate-savepoint branch May 5, 2026 14:58
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