modo-db
Database integration for the modo framework. Provides SeaORM-backed connection pooling, automatic schema synchronisation, versioned migrations, and a compile-time entity/migration registration system built on inventory.
Features
sqlite(default) — enables SQLite viasqlx-sqlite. WAL mode, busy-timeout, and foreign keys are applied automatically.postgres— enables PostgreSQL viasqlx-postgres.
Usage
Configuration
DatabaseConfig is deserialized from your app's YAML config. The backend is auto-detected from the URL scheme.
use DatabaseConfig;
use Deserialize;
Example config.yaml:
database:
url: "sqlite://data.db?mode=rwc"
max_connections: 5
min_connections: 1
Defaults: sqlite://data.db?mode=rwc, max_connections: 5, min_connections: 1.
Connecting and migrating
async
sync_and_migrate runs in two phases:
- Schema sync — creates or adds columns for all registered entities (addition-only).
- Migration runner — executes pending versioned migrations tracked in
_modo_migrations.
Defining entities
Apply #[modo_db::entity(table = "...")] to a plain struct. The macro generates a SeaORM entity module and auto-registers it with inventory.
// adds created_at, updated_at
The macro creates a submodule named after the struct in snake_case (e.g. todo) containing Model, ActiveModel, Entity, Column, and Relation.
Field attributes
| Attribute | Effect |
|---|---|
primary_key |
Marks the primary key column |
auto_increment = false |
Disables auto-increment (required for composite PKs) |
auto = "ulid" / auto = "nanoid" |
Auto-generates the PK before insert (primary key only) |
unique |
Adds a unique constraint |
indexed |
Adds a single-column index |
column_type = "Text" |
Overrides the SeaORM column type |
default_value = <lit> |
Sets a column default |
default_expr = "<sql>" |
Sets a SQL expression default |
belongs_to = "OtherEntity" |
Defines a FK relation; combine with on_delete/on_update |
has_many / has_one |
Declares an inverse relation (no DB column) |
via = "JunctionEntity" |
Many-to-many through a junction table |
renamed_from = "old_name" |
Records rename as a column comment |
nullable |
Accepted (no-op; Option<T> already implies nullable) |
Struct attributes
| Attribute | Effect |
|---|---|
#[entity(timestamps)] |
Appends created_at and updated_at (DateTime<Utc>) |
#[entity(soft_delete)] |
Appends deleted_at (Option<DateTime<Utc>>) and generates find(), find_by_id(), with_deleted(), only_deleted(), soft_delete(), restore(), force_delete() helpers |
#[entity(index(columns = ["a", "b"]))] |
Generates a composite index via CREATE INDEX IF NOT EXISTS |
#[entity(index(columns = ["slug"], unique))] |
Generates a composite unique index |
Versioned migrations
Use #[modo_db::migration] for escape-hatch SQL that schema sync cannot express (e.g. data migrations, renaming columns).
async
Migrations are executed in ascending version order. Each version is recorded in _modo_migrations and runs exactly once.
Extracting the pool in handlers
use Db;
use JsonResult;
async
Pagination
Offset-based
use ;
async
Cursor-based
use ;
async
per_page defaults to 20 and is clamped to [1, 100]. Paginate forward with ?after=<cursor> and backward with ?before=<cursor>.
ID generation
let ulid_id = generate_ulid; // 26-char Crockford Base32
let nano_id = generate_nanoid; // 21-char NanoID
Key Types
| Type | Purpose |
|---|---|
DatabaseConfig |
Connection URL + pool size, deserialised from YAML |
DbPool |
Newtype over sea_orm::DatabaseConnection; implements GracefulShutdown |
Db |
Axum extractor that pulls DbPool from app state |
EntityRegistration |
Compile-time entity registry entry (produced by #[entity] macro) |
MigrationRegistration |
Compile-time migration registry entry (produced by #[migration] macro) |
PageParams / PageResult<T> |
Offset pagination request + response |
CursorParams<V> / CursorResult<T> |
Cursor pagination request + response |