agentleFS
po4ykapo4yka/rust-skills/skills/rust-database/SKILL.md

rust-database

Skill5 starsChanged 10 days agoBSD-3-ClauseView on GitHub

Use when building or reviewing production Rust database code (sqlx, diesel, tokio-postgres, rusqlite, deadpool, bb8) for database pool exhaustion, transaction rollback, transaction cancellation safety, a serialization failure or deadlock retry, database migration ordering in a rolling deploy, a schema integration test, or a sqlx 0.9 upgrade that hits SqlSafeStr or AssertSqlSafe.

4 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

23 rules. 0 are common, 12 are distinctive, and 11 are very specific to this project.

  • 0

    Common

    Most of the 4 edited versions we read keep these.

  • 12

    Distinctive

    This author's own choices. Worth a look.

  • 11

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Keep the database, driver, and migration tool that the workspace already uses.

    line 9
  2. Distinctive:

    Use their native pool and transaction types before you add wrappers.

    line 10
  3. Very specific:

    Read the section of references/engine-and-driver-notes.md for the engine and crates that Cargo.lock names when you set a pool option, a …

    line 18
  4. Very specific:

    Do not infer a safe pool size or wait from a library default.

    line 51
  5. Distinctive:

    Do not infer transaction semantics from an ORM method name.

    line 54
  6. Very specific:

    Run the database test target while you iterate.

    line 78
  7. Distinctive:

    Treat the test worker count as a connection-budget input, not a constant to copy.

    line 89
  8. Very specific:

    Run a mutating migration command (sqlx migrate run, a down migration, or a DDL script) only against a disposable test database or an …

    line 100
  9. Distinctive:

    Before merge, confirm each item:

    line 106
  10. Distinctive:

    Use an explicit commit on the success path.

    line 156
  11. Distinctive:

    Keep every domain invariant in one transaction or in a database constraint.

    line 163
  12. Distinctive:

    Do not perform an irreversible external effect inside a database transaction.

    line 165
  13. Very specific:

    Treat a lost connection during COMMIT as an unknown outcome.

    line 169
  14. Very specific:

    Keep the transaction lifetime inside the deadline.

    line 192
  15. Distinctive:

    Do not let a connection serve another caller while a cancelled statement or a queued rollback can still produce protocol messages.

    line 195
  16. Distinctive:

    Test cancellation at every phase, from the pool wait to a commit in flight.

    line 214
  17. Distinctive:

    Read references/schema-tests.md when you write these tests.

    line 215
  18. Distinctive:

    Never retry these cases automatically, because the outcome is known to be permanent, the caller has given up, or a repeat can apply the …

    line 241
  19. Very specific:

    Keep classification and retry policy pure and test them separately from the driver.

    line 256
  20. Very specific:

    Read references/migrations.md when you write, order, or run a schema migration.

    line 265
  21. Very specific:

    Read references/pool-operations.md when you size a pool or set its timeouts, lifetime, health checks, startup behavior, or metrics.

    line 270
  22. Very specific:

    Read references/isolation-and-retry.md when you choose an isolation level, a constraint, or a lock order for an invariant.

    line 275
  23. Very specific:

    Read references/schema-tests.md when you write a database integration test, set up test databases, or build the CI matrix.

    line 280

“Common” is measured: kept by at least 6 in 10 of the 4 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Rust Database

Keep the database, driver, and migration tool that the workspace already uses. Use their native pool and transaction types before you add wrappers. Adapt error codes and DDL behavior to the selected engine.

Other skills own neighboring work, when installed: rust-async-internals (future cancellation and task ownership), rust-networking (HTTP deadlines and retries), rust-observability (telemetry), and rust-security (the dependency advisory gate).

Read the section of references/engine-and-driver-notes.md for the engine and crates that Cargo.lock names when you set a pool option, a timeout, a retry code, or a migration command.

Inspect the existing contract

Find the database boundary before you change it:

rg -g 'Cargo.toml' -g 'Cargo.lock' \
  'sqlx|diesel|tokio-postgres|rusqlite|deadpool|bb8|r2d2|sea-orm' .
rg 'begin\(|begin_with\(|transaction\(|commit\(|rollback\(|acquire\(' --glob '*.rs' .
rg 'CREATE TABLE|ALTER TABLE|CREATE INDEX|migrate' --glob '*.sql' --glob '*.rs' .
rg -U -i 'AssertSqlSafe|sql_query\(|format!\(\s*r?#*"\s*(select|insert|update|delete|with)\b' --glob '*.rs' .

Record these facts before you change pool, transaction, retry, or migration policy:

FactRequired answer
DatabaseEngine and production major version
DriverCrate, version, runtime, and TLS backend
Pool ownerProcess object that creates and closes the pool
Connection budgetServer limit, reserved capacity, instance count, and pool maximum
Acquisition policyQueue bound, acquisition timeout, and overload result
Query policyOperation deadline and server-side statement limit
Transaction policyIsolation, retryable errors, and maximum attempts
Migration ownerOne job or process that serializes migrations
Deployment policyExpand, migrate data, switch code, and contract order
Test engineExact engine major version used in production

Do not infer a safe pool size or wait from a library default. A deadpool default pool grows with the host CPU count, and it has no wait timeout, so Pool::get() can wait forever: set timeouts.wait below the caller deadline. Do not infer transaction semantics from an ORM method name. Confirm pool and transaction behavior in the driver and database documentation for the locked versions.

Triage failures

SymptomLikely causeFirst proofFix
Pool acquisition timeout risesConnections are held too long or total budget is too smallCompare checked-out count, wait duration, and query durationShorten hold scope, bound admission, then recalculate pool budget
Database rejects new sessions during deployRolling instances exceed the deployment budgetCount pools across old and new instancesLower per-instance maximum or server instance overlap
Pool stays smaller after cancellationCancelled query or rollback still owns a connectionCancel at each phase and observe pool capacityAwait cleanup or discard the dirty connection
Writes succeed but later vanish, with no errorAn open transaction returned to the pool (sqlx before 0.9.0, drop during BEGIN)cargo tree --locked -i sqlx-core --depth 0 shows a version below 0.9.0; pg_stat_activity shows idle in transaction sessionsUpgrade to sqlx 0.9.0 or later, or stop cancelling begin()
Rows changed although caller saw an errorCommit outcome is unknownQuery by stable operation IDReconcile; do not repeat the transaction blindly
Deadlocks repeat on every attemptLock order differs or retry repeats immediatelyCapture bounded operation names and lock orderUse stable lock order and jittered bounded retry
Serialization failures surgeContention or transaction scope grewCompare conflict rate, duration, and touched rowsShorten scope, reduce hot rows, keep bounded retry
SQLite returns SQLITE_BUSY at once despite busy_timeoutA deferred transaction tried to upgrade from read to writeCheck how the transaction beginsBegin write transactions with BEGIN IMMEDIATE
Migration blocks application trafficDDL takes a strong lock or rewrites dataMeasure on production-shaped dataSplit the change and use online or batched operations
CREATE INDEX CONCURRENTLY cannot run inside a transaction blockThe migration tool wraps the file in a transactionRead the migration tool's transaction ruleMove the statement to its own non-transactional migration
New binary fails before migration completesDeploy order is incompatibleReproduce old and new binaries on expand schemaRestore expand-switch-contract compatibility
Migration passes locally but fails in CITest engine or extension differsCompare engine version and enabled extensionsMatch production engine and provision extensions
Tests fail only in parallelWorkers share schema or exceed connection budgetRun with one worker, then inspect names and poolsIsolate schemas and bound parallelism

Verify and finish

Run the database test target while you iterate. Select it explicitly when the full suite is too slow. Run the other commands once before merge or at review. Skip the two sqlx commands when Cargo.lock has no sqlx.

cargo test --locked --test database -- --test-threads=4
cargo sqlx prepare --workspace --check   # sqlx only
cargo tree --locked -i sqlx-core --depth 0   # sqlx only
cargo deny --config deny.toml --locked check advisories
  • Treat the test worker count as a connection-budget input, not a constant to copy. A green run does not prove lock duration or rewrite cost on production data volumes.
  • prepare --check exits 1 when the .sqlx query metadata is stale.
  • cargo tree prints the locked sqlx version for the 0.9.0 floor in "Make cancellation safe".
  • cargo deny with unsound = "all" (or cargo audit --deny warnings) gates the driver crates against known advisories (the rust-security skill owns that policy).

Run a mutating migration command (sqlx migrate run, a down migration, or a DDL script) only against a disposable test database or an environment the user has authorized, because a migration can destroy data. Do not print DATABASE_URL or pass it as a command-line argument, where process listings expose it.

Before merge, confirm each item:

  • The budget includes rolling overlap. Admission, acquisition, query, lock, and shutdown waits are finite.
  • One async scope owns each transaction. External effects use an idempotent outbox or follow commit.
  • Cancellation restores a clean protocol and pool capacity. Unknown commit outcomes reconcile by operation ID.
  • Isolation follows a written invariant. Retry matches exact codes, reruns the whole transaction, and is bounded.
  • Every value is bound. Every AssertSqlSafe or raw SQL string names its trusted source.
  • Migrations are serialized and rolling-compatible. Destructive or non-transactional DDL has a recovery plan.
  • CI on the production engine covers empty and previous-release schemas, conflicts, cancellation, and pool recovery.
  • The advisory gate passes.

Report the database and driver versions, pool calculation, transaction and retry policy, migration phases, integration test topology, commands run, and any production-only behavior that remains unverified.

Keep SQL text static

Bind every value. Do not build SQL text from untrusted input with format!.

sqlx 0.9 query*() functions take impl SqlSafeStr. A &'static str implements it; a runtime String does not, so query(&sql) stops compiling after the upgrade. Do not wrap the string in AssertSqlSafe only to make it compile, because the wrapper asserts that the text is safe without proof. Build dynamic SQL with QueryBuilder. QueryBuilder::new and push accept any string and do not escape. Start from a static string, use push_bind for every value, and use push only for a fragment from a fixed allowlist, such as a column name. Give every remaining AssertSqlSafe( and diesel sql_query( a comment that names where its text comes from.

Keep one transaction in one async scope

The function that starts a transaction owns it until commit or rollback. Pass &mut Transaction or the driver equivalent to helpers. Do not put a live transaction in shared state. Do not move it into a detached task. Do not return it across an API boundary unless that boundary exists only to compose database operations in the same task.

Use this shape:

begin
  read and lock the rows required by the invariant
  validate the invariant
  write all related rows
  write an outbox record when an external effect must follow commit
commit
publish or perform the external effect from the committed outbox

Use an explicit commit on the success path. Return the commit error. Do not report success before commit succeeds. Roll back explicitly when the driver provides useful rollback errors and the error path still has time. Do not assume that Drop performs a synchronous rollback: many async drivers queue it. sqlx runs the queued rollback on the next use of the connection, including its return to the pool. Test the drop behavior of the selected driver.

Keep every domain invariant in one transaction or in a database constraint.

Do not perform an irreversible external effect inside a database transaction. The database can roll back after the effect succeeds. Use a transactional outbox for messages and callbacks. Make the outbox consumer idempotent.

Treat a lost connection during COMMIT as an unknown outcome. The server might have committed. Do not retry the transaction blindly. Reconcile by a stable operation ID, an idempotency row, or a domain read that can prove the outcome.

Make cancellation safe

A dropped Rust query future does not prove that the database stopped the statement. The driver can cancel the server statement, close the session, wait for the result, or leave cleanup to a connection worker. Confirm the actual behavior for the locked driver and database versions.

Require sqlx 0.9.0 or later when a timeout, select!, or an aborted request can cancel begin(). Before 0.9.0, a transaction dropped during BEGIN could return to the pool still open, and later writes on that connection could stay uncommitted and be lost without an error.

Apply all of these controls:

  1. Carry one caller deadline into pool acquisition and database work.
  2. Set a server-side statement timeout below the caller deadline when the database supports it.
  3. Set a lock-wait timeout below the statement timeout when lock waits must fail separately.
  4. Keep the transaction lifetime inside the deadline. PostgreSQL 17 adds transaction_timeout; it terminates the session, so expect a dead pooled connection after it fires.
  5. Do not let a connection serve another caller while a cancelled statement or a queued rollback can still produce protocol messages. Confirm that the driver drains, rolls back, or closes it. Discard the connection yourself when the driver does not.

Send a server-side cancel explicitly (tokio-postgres Client::cancel_token()) when a long statement must stop, unless the driver documents that a drop sends one. Keep the statement timeout as the real bound: the server does not report whether a cancel worked.

Set transaction-local timeouts where possible. A session-wide timeout can leak into the next pool user when reset fails. If the driver requires a session setting, set it on checkout and reset or discard the session before check-in.

Cancellation before commit must leave no partial database state. Cancellation during commit has the same unknown-outcome rule as a lost connection. Keep the stable operation ID available after the future is dropped so a caller or reconciler can query the outcome.

Test cancellation at every phase, from the pool wait to a commit in flight. Read references/schema-tests.md when you write these tests; it lists the cancel points and what to prove after each one.

Retry the complete transaction

Retry only an error that proves the current transaction did not commit and that the database defines as transient. The common classes are serialization failure and deadlock victim. Match structured database error codes. Do not match error message text.

Retryable examples: PostgreSQL SQLSTATE 40001 and 40P01, MySQL error 1213 (SQLSTATE 40001), and SQLite SQLITE_BUSY. Match exact codes, not a class prefix: PostgreSQL 40003 means statement completion unknown.

Report a lock-wait timeout (PostgreSQL 55P03, MySQL 1205) by default. Retry it only after a full rollback, inside the remaining caller deadline and attempt limit. MySQL 1205 rolls back only the statement by default (innodb_rollback_on_timeout=OFF), so roll back the transaction before you act on it. PostgreSQL documents that 23505 or 23P01 can be a serialization failure when the transaction derived the key from its own reads. Let only that call site opt into a retry.

Retry the complete transaction closure with a new transaction. Do not retry only the failed statement. Preserve the same logical operation ID and stable input across attempts. Re-read database state on every attempt.

Never retry these cases automatically, because the outcome is known to be permanent, the caller has given up, or a repeat can apply the effect twice:

  • a constraint violation that reports a domain conflict;
  • invalid SQL, invalid data, permission failure, or missing schema;
  • cancellation, a statement timeout (PostgreSQL 57014), or an exhausted caller deadline;
  • a lost connection during commit or any other unknown commit outcome;
  • a transaction that performed a non-idempotent external side effect.

Count the first execution as attempt one. Use a small finite attempt maximum, an absolute operation deadline, exponential backoff, and jitter. Release the failed transaction and its connection before the delay. Bound retries across the process so an outage does not create a retry storm.

Keep classification and retry policy pure and test them separately from the driver. Get the code with err.as_database_error().and_then(|e| e.code()) in sqlx, or err.code() in tokio-postgres. A lost connection has no SQLSTATE; classify it at the call site, where you know whether COMMIT was in flight. Read references/isolation-and-retry.md when you write the classifier; it has a runnable PostgreSQL example.

Deploy migrations in compatible phases

Read references/migrations.md when you write, order, or run a schema migration.

Budget database connections

Read references/pool-operations.md when you size a pool or set its timeouts, lifetime, health checks, startup behavior, or metrics.

Choose isolation from the invariant

Read references/isolation-and-retry.md when you choose an isolation level, a constraint, or a lock order for an invariant.

Test the real schema

Read references/schema-tests.md when you write a database integration test, set up test databases, or build the CI matrix.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.