Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
axumstart_db
Proc-macro crate (axumstart_db_macros, re-exported here) that generates a repository
struct from a trait definition, plus the runtime types it depends on (Page,
Repository, Db/DbPool/DbConnection aliases).
Picking a backend
Exactly one of the postgres / mysql / sqlite Cargo features must be enabled —
the target backend is a compile-time choice, since SQL placeholder syntax and RETURNING
availability are baked into generated queries at macro-expansion time. Enabling zero or
more than one is a compile error.
= { = "...", = ["postgres"] }
#[repository(table = "...")]
Annotate a trait to generate:
- The trait itself, wrapped with
#[async_trait] - A
Db{TraitName}struct with apool: DbPoolfield andnew(pool: DbPool) -> Self impl {TraitName} for Db{TraitName}with bodies generated from method names (see below)
// Generated: DbUserStatsRepository { pool } + impl UserStatsRepository for DbUserStatsRepository
join(table_name) — cross-table field resolution
Declare related tables so that find_by_* method names can reference their columns:
Convention: {join}_id FK must exist on the main table. Multiple joins: join(challenge, user).
Field resolution: {join_name}_{col} → "{join_name}".{col} with JOIN added. All other fields stay unqualified.
Supported method name conventions
| Method name pattern | Generated SQL |
|---|---|
find_by_{field} |
SELECT * FROM "{table}" WHERE {field} = $1 |
find_by_{f1}_and_{f2} |
SELECT * FROM "{table}" WHERE f1 = $1 AND f2 = $2 |
find_all_by_{field} |
SELECT * FROM "{table}" WHERE {field} = $1 (always fetch_all) |
find_random_by_{field} |
SELECT * FROM "{table}" WHERE {field} = $1 ORDER BY RANDOM() LIMIT 1 |
exists_by_{field} |
SELECT EXISTS(SELECT 1 FROM "{table}" WHERE {field} = $1) |
delete_by_{field} |
DELETE FROM "{table}" WHERE {field} = $1 |
insert |
Delegates to SqlxInsert::__sqlx_insert_into (requires #[derive(SqlxInsert)] on the values struct) |
insert_all |
Single multi-row INSERT for the whole batch |
insert and insert_all only fetch the written row(s) back when the method actually
returns them. Declare -> sqlx::Result<()> instead of -> sqlx::Result<Row> /
-> sqlx::Result<Vec<Row>> and codegen skips RETURNING entirely (Postgres/SQLite) or the
follow-up SELECT entirely (MySQL) — a plain INSERT, nothing fetched back:
async ;
async ;
Bonus on MySQL: since the void form is a single statement, it doesn't need E: Copy the
way the Row-returning form does — #[transactional] insert/insert_all does work on
MySQL when the return type is () (see the #[transactional] limitation below, which
only applies to the Row-returning form).
| db or pool | Returns &self.pool |
Multi-field _and_ chaining works for any number of fields:
find_by_user_id_and_test_type → WHERE user_id = $1 AND test_type = $2.
Fetch mode for find_by_* is inferred from the return type:
Option<T>→fetch_optionalVec<T>→fetch_all- anything else →
fetch_one
Methods with a default body are emitted as-is; no codegen is applied.
Unknown names compile to todo!("no codegen rule for name").
_in — works the same on every backend
find_by_id_in, delete_by_id_in, find_all_by_id_in, etc. take a Vec<T> and render
an IN (...) list. Unlike the original Postgres-only implementation (which used
= ANY($n)), this is now built at runtime via sqlx::QueryBuilder — the same code path
on all three backends, since neither MySQL nor SQLite (nor, for consistency, Postgres
here) let a single placeholder bind a Vec<T>.
#[transactional]
Marking a method #[transactional] generates a {method}_tx variant that takes an extra
conn: &mut DbConnection instead of using self.pool. The original method is also
generated using the pool.
async ;
// generates both:
// update(&self, stats) — uses pool
// update_tx(&self, conn: &mut DbConnection, stats) — uses conn
MySQL limitation: upsert, update, and the Row-returning form of insert/
insert_all emulate RETURNING with a follow-up SELECT on the same executor, which
requires the executor to be Copy (true for &DbPool, never true for
&mut DbConnection). This means #[transactional] combined with any of those does not
compile under the mysql feature — only the pool-based (non-_tx) call works. The void
form of insert/insert_all (-> sqlx::Result<()>) is exempt — see above. Postgres and
SQLite have no such restriction (RETURNING is a single statement either way).
Combining with mockall
Add mock to the macro to generate #[cfg_attr(test, ::mockall::automock)] on the output trait:
Type alias pattern
pub type OnboardingTestRepositoryImpl = DbOnboardingTestRepository;
#[derive(SqlxInsert)]
Generates __sqlx_insert_into(table, pool), __sqlx_insert_ignore_into,
__sqlx_insert_all_into, and __sqlx_upsert_into on a named struct, from its fields in
declaration order.
Postgres/SQLite: all four build INSERT ... RETURNING * (or ON CONFLICT ... RETURNING * for upsert) — one round-trip.
MySQL has no RETURNING. Every write is followed by a second SELECT on the same
connection to hand back the row:
insert:INSERT→last_insert_id()→SELECT * WHERE {key} = ?insert_all: multi-rowINSERT→SELECT * WHERE {key} BETWEEN first_id AND first_id + rows.len(), relying on MySQL's contiguous auto-increment allocation for a single multi-row insert (true under the defaultinnodb_autoinc_lock_mode) — if the table's key isn't an auto-increment column, this won't return the right rows.upsert:INSERT ... ON DUPLICATE KEY UPDATE ...→SELECT * WHERE {conflict_col} = ?
{key} for insert/insert_all comes from an optional struct-level attribute, defaulting
to id:
All field types must implement sqlx::Encode + sqlx::Type + Send; on MySQL every field
must also implement Clone (the upsert path needs to keep a copy of whichever field ends
up being the runtime-selected conflict column, since the INSERT's bind chain already
consumes self by the time the follow-up SELECT needs it).
#[derive(SqlxUpdate)]
One field marked #[key] is the WHERE target; every other field is a SET column.
Postgres/SQLite generate UPDATE ... SET ... WHERE key = $n RETURNING *. MySQL generates
UPDATE ... SET ... WHERE key = ? followed by SELECT * WHERE key = ? (same Copy
executor requirement as above, so no #[transactional] on MySQL here either).
_this_week — cross-dialect caveat
find_all_by_*_this_week (and #[created_at(col)] to pick a column other than
created_at) filters rows from the current ISO week (Monday start). The boundary
expression is backend-specific (axumstart_db::WEEK_START_SQL) and assumes a
particular storage convention per backend:
- Postgres/MySQL: a native
TIMESTAMP/DATETIMEcolumn — always comparable. - SQLite: has no native timestamp type.
WEEK_START_SQLproduces an ISO8601 date string (date(CURRENT_TIMESTAMP, 'weekday 1', '-7 days')), which only compares correctly against a column storing ISO8601 TEXT (e.g.created_at TEXT DEFAULT CURRENT_TIMESTAMP). If your SQLite schema storescreated_atas an INTEGER unix timestamp instead, this comparison silently returns no rows (SQLite compares INTEGER-affinity columns and TEXT literals by type, not value) — use a method body instead in that case.
Exact edge-boundary behavior (timezone handling, week-start inclusivity) is not guaranteed to be bit-identical across the three backends.