Queries
Every #[model] gets a typed query API while your project builds. You never write queries by hand: you walk your model's keys and indexes, field by field, and the types only offer what that path supports. Import margaret::framework::active_record::model::Model to use it, and pass a &Database or a &Transaction wherever a query runs. Database introduces it with the box office's concerts.
Walking keys and indexes
Model::query() starts a query. Its fields are exactly the leading fields of the model's primary key, unique constraints, and indexes. After .eq(value) on a field, the next fields of the same index become available.
| You write | You get |
.field.eq(value) reaching a primary key or a unique constraint | one row |
.field.eq(value) along an index | the next fields of that index |
.field.above(v), .at_least(v), .at_most(v), .below(v) | a range |
.ascending() or .descending() | a scan, in the order of the rest of the index |
A scan needs an order that's unique, so end your index with the primary key, like [starts_at, id]. Ranges work on numbers, strings, bytes, UUIDs, decimals, and times.
One row
| Method | Answers with |
find(executor) | Lookup::Found(model) or Lookup::Missing |
load:: | Lookup of a shape, related rows included |
when(|row| condition) | the same row, only if the condition holds |
update(executor, |columns| columns.a.to(x).and(columns.b.to(y))) | Change::Changed(model) or Change::Unmatched |
delete(executor) | Removal::Removed(model) or Removal::Missing |
exists:: | a condition for create(...).when(...) and insert().when(...) |
find_key_shared(&transaction) | Lookup, and the row can't be deleted until the transaction ends |
Conditions compare any column with eq, above, at_least, at_most, below, check an Option column with is_null(), and combine with and and negated.
Many rows
| Method | Answers with |
a range or an index prefix, then delete(executor) | the number of deleted rows |
a range or an index prefix, then update(executor, |columns| ...) | the number of updated rows |
a scan, then limit::, then fetch(executor) | a Page of models |
a scan, then limit::, then load:: | a Page of shapes |
a Page's next | NextPage::Continues(cursor) or NextPage::Exhausted |
resume(cursor) | the next page of the same scan |
a scan, then stream:: | a stream of models, read BATCH at a time |
The limit is part of the type, so every read has a bound.
Writing
| Method | Answers with |
Model::create(Draft { ... }).run(executor) | the created model, with its database defaults filled in |
Model::create(...).when(condition).run(executor) | Creation::Created(model) or Creation::Refused |
model.insert().run(executor) | nothing, or an error |
model.insert().or_ignore(executor) | Insertion::Inserted or Insertion::AlreadyPresent |
model.insert().when(condition).run(executor) | GuardedInsertion::Inserted or GuardedInsertion::Refused |
model.insert().or_update(|columns| columns.a.excluded()).run(executor) | Upsertion::Written or Upsertion::Kept |
model.save(executor) | Saving::Saved or Saving::Missing |
model.delete(executor) | Deletion::Deleted or Deletion::Missing |
A Draft is the model without the fields the database fills in. or_ignore and or_update look at the primary key. In or_update, excluded() takes the value you tried to insert, and greatest() keeps the larger of the two. or_update(...).when(...) updates only rows that pass a condition.
Relations and shapes
A field of type
Keypoints at another model's primary key. Build one withKey::of(&model)orKey::new(primary_key).#[has_many(name, model, key)]and#[has_one(name, model, key)]declare the other direction.An
#[eager_load(model = ...)]shape loads a model together with its relations: a related row for a key, anOptionforhas_one, andChildrenforhas_many, which always takes alimitand tells you whether it'sCompleteorTruncated. Shapes can nest.Models and shapes can be bound from a route parameter by their primary key.
Transactions
let mut connection = database.connection.await?;
let transaction = connection.transaction.await?;Pass &transaction instead of the database, then commit() or rollback() it. A transaction you drop without committing is rolled back. Isolation also has RepeatableRead and Serializable.
Errors
Queries fail with an ActiveRecordError. The ones worth matching on:
UniqueViolationwhen a unique constraint refuses a row,ForeignKeyViolationwhen a key points nowhere, or a row is still referenced,CheckViolationwhen aminimumorbyte_lengthrefuses a value,NumericValueOutOfRangewhen a decimal doesn't fit its precision,SerializationFailureandDeadlockwhen concurrent transactions collide,UnknownVariantwhen an enum column holds a value your enum doesn't have.
What doesn't compile
Filtering by a field that doesn't lead an index: there's no such field on the query.
Skipping a field of an index, or narrowing further after a range.
findalong a path that isn't unique.Fetching a scan without
limit, or a limit of zero.Deleting or updating a whole table.
Ranges on booleans, enums, keys, and JSON.
Resuming a cursor of a different scan or direction.
Updating a primary key, assigning a column twice, or an update that assigns nothing.
Reaching a related row through a key, like
concert.venue.name. Load a shape instead.