Database
A JSON file worked for the first few weeks, but the box office now sells tickets for dozens of venues, and the staff need the line-up in one place that every instance of the box office shares. Time for PostgreSQL.
You never write database queries by hand in Margaret. Tables are Rust structs with #[model], and Margaret generates a typed query API for each of them while your project builds. A query that would scan a whole table, read without a bound, or load related rows one by one doesn't compile.
Declaring the database
The box office has one database. Declare it once, and name the environment variables that configure it:
use postgres_database;
;Margaret connects a pool when the application starts, and any singleton can ask for it as Arc. A declared database that nothing uses is a build error:
Error: Container { source: UnconsumedDeclaration { path: "margaret::framework::database::database::Database" } }Defining models
Mark the struct with #[model] and every field with #[column]. Option makes a column nullable. ColumnDefault::UuidV7 lets the database pick the primary key, so the box office never generates one itself:
use model;
use ColumnDefault;
use Uuid;
Relationships, indexes, and constraints
A concert takes place at a venue. A field of type Key points at a venue's primary key, and #[foreign_key] decides what happens when that venue goes away. Restrict keeps the staff from deleting a venue that still has concerts:
A few things to notice:
#[column(unique)]and#[column(minimum = 1)]become constraints the database enforces.Jsonstores any serializable value, here the list of support acts.Decimalneeds aprecisionand ascale, because prices must never round.The
concerts_by_startindex lists concerts in the order they start. Theidat its end makes the order unique, which lets Margaret page through it.Every key must lead an index, so finding a venue's concerts never scans the table. Leave out the
#[index]onvenue, and the build stops:
Error: Model { source: UnindexedForeignKey { field: "venue", model: "crate::concerts::concert::Concert" } }Add chrono, uuid, and rust_decimal to your dependencies for these column types.
Loading related rows
A Key holds only the venue's primary key. To read concerts together with their venues, describe the shape you want with #[eager_load]:
use eager_load;
use crateConcert;
use crateVenue;
Margaret loads the whole shape at once. There's no way to reach a venue through a concert row by row: concert.venue.name doesn't compile, because a key has no name:
error[E0609]: no field `name` on type `margaret::framework::margaret_active_record::key::Key<Venue>`Querying
The line-up file goes away. A ConcertRepository takes its place: it holds the database and the clock, and it's the one place that asks about concerts:
Read the query from top to bottom: start at the concerts, walk the concerts_by_start index from now on, in ascending order, at most 50 at a time, and load each concert with its venue. The fields you can walk are exactly the leading fields of your primary key, unique columns, and indexes. Filter by artist, which has no index yet, and it doesn't compile:
error[E0609]: no field `artist` on type `concerts_concert_concert::query::step::Step`
|
= note: available fields are: `id`, `slug`, `starts_at`, `venue`Every read has a bound, and the bound is part of the type. Leave out limit, and there's nothing to fetch:
error[E0599]: no method named `fetch` found for struct `margaret::framework::margaret_active_record::scan::Scan<Modeled, Ordering, Toward>` in the current scopeThe web page shows the next 50 concerts. The staff want the whole season in the terminal, so the repository pages through all of them. Each page tells you whether it Continues, with a cursor to resume from, or is Exhausted:
pub async The lineup command asks for the repository instead of the old Lineup:
pub async Creating the database
Your models describe the whole database, so Margaret generates a schema command. It needs PostgreSQL 18, and prints everything the box office needs: a table for every model, with its primary key, unique constraints, checks, foreign keys, and indexes, created in dependency order. Feed it to a fresh database:
$ box_office schema | psql "$DATABASE_URL"There are no migrations. In development, recreate the database whenever your models change. Then point the box office at it:
$ DATABASE_URL=postgres://localhost/box_office DATABASE_MAX_CONNECTIONS=4 box_office lineupThe tables are empty for now. Console commands fills them.
What the build checks
Unsupported column types, keys to structs that aren't models, keys without an index, cycles between tables, duplicate or redundant indexes, and names PostgreSQL wouldn't accept are all build errors. The Queries cheat sheet lists every query the generated API offers.