Skip to main content

Persistence

Version stamp

Applies to: renvor 0.0.0 · framework source 7d0816a · MSRV 1.94.0 · documentation set pre-release

This stamp is a single shared partial (docs/_stamp.mdx) imported by every prose page and the API reference. It binds this documentation snapshot to the immutable framework commit it describes. The framework remains unpublished and no release compatibility promise applies.

Nothing is published

None of the crates below are on crates.io. This page documents contracts that are implemented and tested, not software you can install.

Two models, one set of ports

renvor-database declares the ports — Database, UnitOfWork, Executor, Keyset, MigrationPolicy, DatabaseError — and names no driver. Two adapters implement them:

CrateProgramming modelDatabases
renvor-sqlxQueries written as SQL, every value boundPostgreSQL, MySQL
renvor-seaormEntities and a query builderPostgreSQL, MySQL

Both are measured against the same contract suite. The assertions live in renvor_testkit::persistence and are compiled once; each adapter supplies only the driver-specific operations. "Both models pass the same contracts" is therefore a fact about the build rather than two test files that happen to agree.

Selecting SeaORM changes the programming model, not the driver family

SeaORM is built on SQLx. A project that selects SeaORM resolves SQLx transitively, and renvor-seaorm depends on SQLx directly. This is stated plainly rather than left to be discovered in a lockfile.

What selecting SeaORM does change is what you write:

// renvor-seaorm
let uow = database.begin().await?;
let items = item::Entity::find().all(&uow).await?;
uow.commit().await?;
// renvor-sqlx
let mut uow = database.begin().await?;
let items = sqlx::query_as::<_, Item>(ITEMS).fetch_all(&mut **uow.inner()).await?;
uow.commit().await?;

Direct-SQLx application APIs are not part of the SeaORM surface. The two adapters are siblings; neither depends on the other, and the framework facade depends on neither, so there is nowhere for one to leak through the other. cargo xtask verify asserts both directions with a positive control.

Transactions and cancellation

A transaction is begun explicitly, in application code. There is no commit-on-drop path and no implicit begin: a unit of work that goes out of scope for any reason — an early return, a ?, a panic, a cancelled future — writes nothing.

Renvor owns the pooled connection in both adapters rather than wrapping the driver's own transaction type. For SeaORM that is a deliberate departure from the obvious implementation, and it is what lets Renvor promise that cancellation never costs the pool its configured capacity for longer than a bound you set. The reasoning, and the measurements behind it, are in ADR-0021.

What is not promised: a cancelled client cannot make the server abandon a statement already running. A connection cancelled mid-statement stays pinned server-side until that statement finishes. Renvor's guarantee is about its own pool accounting.

sea_orm::TransactionTrait, savepoints, and isolation-level configuration are not exposed.

Migrations

Migrations are SQL files, ordered by version, checksummed, and run by SQLx's engine — for both persistence models. A project therefore has exactly one migration history whichever ORM it selected, and switching between them is not a re-migration.

sea-orm-migration is deliberately not used. Its bookkeeping table has two columns, version and applied_at, and no checksum, so a migration edited after it was applied would be undetectable. See ADR-0022.

The cost is stated rather than absorbed: you do not get SeaORM's Rust-authored MigrationTrait migrations.

PropertyBehaviour
OrderingBy version, independent of directory enumeration order
TamperA changed already-applied migration is refused before any schema modification
Default policyNever. Schema change on boot requires two separate acts
OnBootApplies before readiness is reported; a failure publishes no database
Concurrent startupExactly once, under a lock Renvor owns with a deadline you set
RollbackDeclared per migration; an unsupported rollback fails before changing data

Atomicity differs between the engines, and that is not hidden

PostgreSQL runs DDL inside a transaction, so a migration that fails part-way leaves nothing behind. MySQL does not: most DDL statements commit implicitly, so a migration that fails after its third statement leaves the first two applied. SQLx records that version in _sqlx_migrations with success = FALSE; every later migration command refuses it as dirty before sending another statement. Recovery is manual: inspect and back up the schema, bring it to a fully applied or fully undone state, then update or remove the ledger row to match. The exact recovery sequence is in Data types and migration portability. Prefer one schema change per MySQL migration so that partial state is smaller and easier to repair.

The escape hatch, in order

  1. Entity and SeaQuery APIs. Everything the generated repository uses.
  2. Statement::from_sql_and_values, when a query cannot be expressed above — and every value is still bound.
  3. A database-specific adapter module, when the two engines genuinely differ.

execute_unprepared is not a rung: it binds nothing. Renvor's implementation routes it through SQLx's AssertSqlSafe, which SQLx 0.9 requires for any SQL that is not &'static str — so caller-supplied SQL has to be marked as such in your own source.

Sorting is an allowlist, never interpolation. A column name cannot be a bound parameter, so the only safe construction is one where every possible value is written in the source. An unknown sort field is refused rather than silently replaced with a default.

Generated projects

renvor new --orm seaorm --database postgres generates src/entity.rs and src/repository.rs in full, in SeaORM 2.0 dense style, plus the same migrations the direct-SQLx path gets.

The generated Cargo.toml declares no dependencies, and the two modules are not declared in src/main.rs. sea-orm is published and could be declared — but generation runs the staged project's own cargo fmt, clippy, build, test and run before placing it, so a real dependency would make renvor new need the registry. Offline generation is a guarantee Renvor keeps. Cargo.toml names the lines to add and the declarations to make.

Both databases, one schema

All four rows are first-class, which puts the burden of agreement on your schema rather than on Renvor. Two pages cover what that takes:

  • Data types and migration portability — column types, identifier rules, upserts, pagination order, and why a migration should make one change. Backed by contract C-16, and by assertions that run against both engines on every verification.
  • Testing backup and restore — which tools are actually in the pinned images, how to invoke them without leaking a password, and what a restore test has to assert.

The differences that cannot be removed are named rather than hidden. MySQL's TIMESTAMP ends in 2038; no adapter can supply the missing years, so the guide tells you to use DATETIME(6) instead.

Not yet

  • No generic resource generator. renvor generate resource does not exist. Generation of arbitrary CRUD resources into an existing project is Phase 011.
  • No cache, jobs, mail, or storage capability. A generated container profile may include a Valkey service; that is local development infrastructure. Renvor's cache port and adapter arrive in Phase 010.