---
title: Querying
description: Finding records with where, order, limit and scopes, joining associated tables, counting, plucking, sums and groups, bulk updates and deletes, and how queries stay type-checked.
category: Models
order: 3
---
Every model can query its table without SQL: `Book.where(status:
:published).order(:title)` builds the query, and `!` runs it. Each query
is checked by the compiler, so a misspelled column is a build error
rather than a runtime one.
After reading this guide, you will know:
- How to find records by id, by fields and by conditions.
- How to find a record or create it in one call.
- How to order, limit and offset results, and take the first or last.
- How to count records, check that they exist, and read single columns.
- How to sum, average and group values in the database.
- How to filter records by their associated records with `join` and
`joins`.
- How to update and delete many records at once.
- How to write scopes, and how a default scope works.
- How queries are typed, and how to write conditions in Rust.
The examples query the library app from the [Models](models.html) guide:
four books by two authors, in two sections.
## Running a Query
A query is built up with calls such as `where` and `order`, and nothing
runs until you add `!`. A query with `!` returns an `Array` of records;
`first()`, `count()`, `pluck()` and the other finishing calls return their
own types. Queries run on the app's database during a request, and in
tests on the test database; you never pass a connection.
## Retrieving a Single Record
`find(id)` loads a record by its primary key. When there's no such row it
fails with a `404 Not Found` error, so a controller action that loads a
record by its URL needs nothing else to answer "not found" (see
[Models](models.html#reading-records)).
`find_by` takes fields as keywords and returns an `Option`: the first
match, or `None`:
```glacier
book = Book.find_by(isbn: "9780441013593")!.unwrap()
assert book.title == "Dune"
```
### Finding or Creating
`find_or_create_by` also takes fields as keywords. It returns the first
record whose columns equal them, as `first()` finds it, or, when there's
none, a new record with those fields set and saved with `save()`, so its
validations and callbacks run:
```glacier
fantasy = Tag.find_or_create_by(name: "fantasy")!
assert fantasy.is_persisted()
again = Tag.find_or_create_by(name: "fantasy")!
assert again.id == fantasy.id
assert Tag.count()! == 1
```
A new record that isn't valid comes back unsaved, with its errors, rather
than as an error:
```glacier
blank = Tag.find_or_create_by(name: "")!
assert blank.is_new()
assert blank.errors_on("name") == ["can't be blank"]
```
`find_or_initialize_by` finds the record in the same way, but doesn't
save a new one, so you can set its other fields first. `is_new()` tells
the two cases apart:
```glacier
member = Member.find_or_initialize_by(email: "ada@example.com")!
assert member.is_new()
member.name = "Ada Lovelace".to_owned()
assert member.save()!
found = Member.find_or_initialize_by(email: "ada@example.com")!
assert found.id == member.id
```
Both run a query and then, for `find_or_create_by`, an insert, so two
requests at the same moment can both find nothing and both try to insert.
A unique index on the columns keeps out the second row, but the request
that loses doesn't get the existing record back: it gets an unsaved record
with the error "has already been taken" when the field is declared
`unique`, or a database error when it isn't. To get the existing record
then, call `find_by` again.
## Conditions
`where` takes fields as keywords, and returns every record whose columns
equal the values. Several keywords must all match (SQL `AND`). An atom
like `:published` is the text of an enum value:
```glacier
published = Book.where(status: :published)!
assert published.len() == 3
by_herbert = Book.where(status: :published, author_id: herbert_id)!
assert by_herbert.len() == 2
```
`nil` matches NULL, and `where_not` negates its conditions:
```glacier
priced = Book.where_not(price: nil)!
assert priced.len() == 1
unpriced = Book.where(price: nil).count()!
assert unpriced == 3
```
The keywords become checks on the model's typed columns, so a typo is
caught when the app is built. Misspelling `status` in a scope gives:
```shell
$ glacier build
Compiling library v0.1.0 (/Users/georgebuckley/framework/guides/examples/library)
error[E0609]: no field `stauts` on type `&BookColumns`
--> src/models/book.glacier:17:26
|
|
help: a field with a similar name exists
|
15 | scope published, where(status: :published)
For more information about this error, try `rustc --explain E0609`.
error: could not compile `library` (lib) due to 1 previous error
error: build failed
```
A value of the wrong type is a build error in the same way, with one
exception: an atom or a string for an enum column is only checked when the
query is built at run time, and a name that isn't one of the enum's values panics
there ("no BookStatus is called `lost`; expected one of draft, published,
out_of_print").
### Comparisons, `or` and `not`
For anything other than equality, `filter` takes a closure that receives
the model's columns. Each column has `eq`, `ne`, `lt`, `le`, `gt`, `ge`
and `is_in`; text columns have `like`; nullable ones have `is_null` and
`is_not_null`. Conditions combine with `and`, `or` and `not`:
```glacier
long = Book.filter(fn b -> b.pages.gt(400) end)!
assert long.len() == 2
dune = Book.filter(fn b -> b.title.like("%Dune%") end).count()!
assert dune == 2
either = Book.filter(fn b -> b.pages.lt(200).or(b.pages.gt(440)) end).order(:pages).pluck(:title)!
assert either == ["A Wizard of Earthsea", "Children of Dune"]
not_dune = Book.filter(fn b -> b.title.like("%Dune%").not() end).count()!
assert not_dune == 2
chosen = Book.filter(fn b -> b.isbn.is_in(["9780441013593", "9780061054884"]) end).count()!
assert chosen == 2
```
`where` and `filter` can be chained, and each adds a condition with
`AND`.
### Raw SQL Conditions
For a condition the typed columns can't express, `Db.Expr.raw`
takes SQL with `?` placeholders and their values. The values are bound,
never pasted into the SQL:
```glacier
raw = Book.filter(fn _ -> Db.Expr.raw("lower(title) = ?", [glacier::db::Value::Text("dune".to_owned())]) end).count()!
assert raw == 1
```
## Ordering (updated)
A query without `order` has no order: `where`, `filter` and the rest add
none, and the database returns rows in whatever order it finds them,
which can change from one run to the next (`first`, `last` and `find`
are the exception: they order by primary key). Give an `order`, or a
model a `default_scope` with one, wherever the order matters.
`order` takes columns as atoms, sorted ascending, or as keywords with
`:asc` or `:desc`. Several columns sort by the first, then the next:
```glacier
by_title = Book.order(:title)!
assert by_title[0].title == "A Wizard of Earthsea"
longest_first = Book.order(pages: :desc)!
assert longest_first[0].title == "Children of Dune"
by_status_then_title = Book.order(status: :asc, title: :asc).pluck(:title)!
assert by_status_then_title == ["The Dispossessed", "A Wizard of Earthsea", "Children of Dune", "Dune"]
```
`order_raw` takes an SQL `ORDER BY` clause, such as `"pages DESC"`, and
applies after any typed `order`:
```glacier
titles = Book.all().order_raw("pages DESC").pluck(:title)!
assert titles[0] == "Children of Dune"
```
WARNING: `order_raw` puts its text straight into the SQL. Build it from
fixed choices, never from request params; to sort by what a request asks
for, `sort(¶ms, ["title", "pages"])` takes only the columns listed.
`glacier audit` points out an `order_raw` (or `where_sql`, or
`Expr.raw`) built from params or `#{…}`.
## Limit and Offset
`limit(n)` returns at most `n` records, and `offset(n)` skips the first
`n`:
```glacier
first_two = Book.order(:title).limit(2).pluck(:title)!
assert first_two == ["A Wizard of Earthsea", "Children of Dune"]
next_two = Book.order(:title).limit(2).offset(2).pluck(:title)!
assert next_two == ["Dune", "The Dispossessed"]
```
## First, Last and Take
`first()` returns the first record (`Option`), by primary key unless the
query is ordered. `last()` reverses the order, or takes the highest
primary key. `take(n)` returns the first `n` records:
```glacier
first = Book.first()!.unwrap()
assert first.title == "Dune"
last = Book.last()!.unwrap()
assert last.title == "The Dispossessed"
shortest = Book.order(:pages).first()!.unwrap()
assert shortest.title == "A Wizard of Earthsea"
longest = Book.order(:pages).last()!.unwrap()
assert longest.title == "Children of Dune"
some = Book.take(2)!
assert some.len() == 2
```
## Counting, Existence and Plucking
`count()` counts the matching rows, `exists(…)` checks whether any row
matches some fields, `pluck(:column)` reads one column without loading
records, and `ids()` reads the primary keys:
```glacier
assert Book.exists(isbn: "9780441013593")!
refute Book.exists(title: "Neuromancer")!
assert Book.count()! == 4
assert Book.where(status: :draft).count()! == 1
pages = Book.order(:pages).pluck(:pages)!
assert pages == [183, 387, 412, 444]
ids = Book.where(status: :published).ids()!
assert ids.len() == 3
```
`exists()` with no arguments ends a query: `Book.where(…).exists()!`. On
the model itself it needs conditions, as `Book.exists(title: "…")!`.
### Records in Hand (new)
Records already loaded have `ids()` too, and `find_id(id)` picks one out
by its key (a key given as text, such as `params[:id]`, is parsed as
`find` parses it), without another query. `to_query()` turns them back
into a query, for a bulk update or a further condition:
```glacier
books = Book.order(:pages)!
assert books.ids().len() == 4
dune = Book.find_by(title: "Dune")!.unwrap()
assert books.find_id(dune.id).unwrap().title == "Dune"
assert books.find_id("#{dune.id}").is_some()
assert books.find_id(0).is_none()
long = books.filter(fn b -> b.pages > 400 end)
long.to_query().update_all(status: :draft)!
assert Book.where(status: :draft).count()! == 3
```
## Calculations (updated)
### Sum, Average, Minimum and Maximum
`sum(:column)` adds up a numeric column over the matching rows, in the
database. Integer columns (`i16`, `i32`, `i64`) sum to an `i64`, `f64`
columns to an `f64`, and `Decimal` columns to a `Decimal`. With no
matching rows the sum is 0:
```glacier
total = Book.sum(:pages)!
assert total == 1426
published = Book.where(status: :published).sum(:pages)!
assert published == 1039
nothing = Book.where(status: :out_of_print).sum(:pages)!
assert nothing == 0
```
`average(:column)` takes the same numeric columns and always returns an
`Option<f64>`, even for a `Decimal` column: `None` when no rows match.
NULLs are left out of a sum or an average:
```glacier
assert Book.average(:pages)! == Some(356.5)
# Only one book has a price: the NULLs are left out.
assert Book.average(:price)! == Some(8.99)
assert Book.where(status: :out_of_print).average(:pages)! == None
```
`minimum(:column)` and `maximum(:column)` return the smallest and largest
value as an `Option` of the column's own type, `None` when no rows
match. They work on numbers, text, booleans, `Decimal`, `Date` and the
timestamp types:
```glacier
assert Book.minimum(:pages)! == Some(183)
assert Book.maximum(:title)!.as_deref() == Some("The Dispossessed")
assert Book.where(status: :out_of_print).maximum(:pages)! == None
```
Called on the model, as `Book.sum(:pages)`, they cover every row (within
the model's `default_scope`, if it has one); after
`where`, `filter` or a scope, only the matching ones.
A `Decimal` column sums exactly, `12.50` and `8.99` to `21.49`, alone
or per group. Postgres adds `NUMERIC`s exactly; SQLite keeps decimals as
REAL and its `SUM` would add them as floats (`21.490000000000002`), so on
SQLite Glacier reads the values and adds them itself:
```glacier
prices = Book.sum(:price)!
assert prices.to_s() == "21.49"
per_section = Book.group(:section_id).sum(:price)!
assert per_section[0].1.to_s() == "21.49"
```
Like `count()`, the calculations use a query's conditions but ignore its
`limit`, `offset` and `order`:
```glacier
assert Book.order(:pages).limit(2).sum(:pages)! == 1426
assert Book.order(:pages).limit(2).count()! == 4
```
### Grouping
`group(:column)` splits the matching rows by a column's value, and
`count()`, `sum`, `average`, `minimum` or `maximum` then works out one
value per group. The result is an `Array` of `(key, value)` pairs, ordered by
the key as the database sorts it (an enum's text sorts alphabetically),
with each value of the same type as the ungrouped calculation returns:
```glacier
per_status = Book.group(:status).count()!
assert per_status == [(BookStatus::Draft, 1), (BookStatus::Published, 3)]
pages_per_section = Book.group(:section_id).sum(:pages)!
assert pages_per_section.len() == 2
longest = Book.where(status: :published).group(:author_id).maximum(:pages)!
assert longest[0].1 == Some(444)
assert longest[1].1 == Some(183)
```
`group` can follow `where`, `filter` and scopes, so the conditions apply
before the rows are grouped.
### Distinct Values
`distinct()` makes a query select only distinct rows. It matters for
`pluck`, which then reads each value once:
```glacier
authors = Book.all().distinct().order(:author_id).pluck(:author_id)!
assert authors.len() == 2
assert Book.all().distinct().count()! == 4
```
`count()` still counts the rows, since a table's rows are already
distinct from each other. On Postgres, a `distinct` query can only be
ordered by the columns it selects, so order it by the plucked column (as
above), and take care with a model whose `default_scope` orders by
another.
NOTE: Glacier has no `having` or `select` yet. `pluck` reads one column
at a time; for anything more, use [raw SQL](#raw-sql).
### Raw SQL
`Db.sql("…")` runs SQL as written, with `?` placeholders whose values
`bind` adds (never `#{…}` in the SQL). `values()` reads the first column
of every row and `value()` the first row's, `nil` when there's none, as
Rails' `select_values` and `select_value`; `fetch_all()` gives the rows,
read by column name:
```glacier
titles: Array<String> = Db.sql("SELECT title FROM books WHERE pages > ? ORDER BY title").bind(400).values()!
assert titles == ["Children of Dune", "Dune"]
rows = Db.sql("SELECT title, pages FROM books ORDER BY pages LIMIT 1").fetch_all()!
title: String = rows[0].get("title")?
assert title == "A Wizard of Earthsea"
```
The binding's type (`titles: Array<String>`) says what each value is read
as. In Rust, it's `glacier::db::sql`.
## Joining Associated Tables
`join` and `joins` keep the records that have matching associated
records, by joining the association's table to the query with an SQL
`INNER JOIN`. Each takes the name of a `belongs_to` or `has_many`
association (see [Associations](associations.html)).
### Conditions on an Associated Model
`join(:author, name: "…")` keeps the books whose author matches. The
keywords after the association are conditions on the associated model's
columns, written as `where` writes them, and they combine with the
query's own conditions with `AND`:
```glacier
by_le_guin = Book.join(:author, name: "Ursula K. Le Guin").order(:title).pluck(:title)!
assert by_le_guin == ["A Wizard of Earthsea", "The Dispossessed"]
published_classics = Book.where(status: :published).join(:section, name: "Classics").count()!
assert published_classics == 2
```
Once tables are joined, every column Glacier writes into the SQL has its
table's name in front, so a column that both tables have isn't ambiguous. Authors
and sections both have a `name`, and each condition here reads its own:
```glacier
titles = Book.join(:author, name: "Frank Herbert").join(:section, name: "Fiction").pluck(:title)!
assert titles == ["Children of Dune"]
```
`nil` matches NULL, as in `where`. Joined from the `Author` side, these
are the authors with a book that has no price, and the authors with a
book at 8.99:
```glacier
unpriced = Author.join(:books, price: nil).distinct().order(:name).pluck(:name)!
assert unpriced == ["Frank Herbert", "Ursula K. Le Guin"]
priced = Author.join(:books, price: "8.99".parse::<Decimal>().unwrap()).pluck(:name)!
assert priced == ["Ursula K. Le Guin"]
```
`join` needs at least one condition; `Book.join(:author)` on its own is
a build error.
### Records With Associated Records
`joins(:reviews)` joins without conditions, keeping the records that have
at least one associated record. Several associations can be listed,
`joins(:author, :section)`, or `joins` called again.
A `has_many` join gives one row for each associated record, so a book
with two reviews comes back twice, and `count()` counts the rows.
`distinct()` gives each record once, and `count()` then counts the
records. For these examples Dune has two reviews, A Wizard of Earthsea
has one, and a third author has no books yet:
```glacier
rows = Book.joins(:reviews).count()!
assert rows == 3
reviewed = Book.joins(:reviews).distinct()
assert reviewed.clone().count()! == 2
assert reviewed.order(:title).pluck(:title)! == ["A Wizard of Earthsea", "Dune"]
with_books = Author.joins(:books).distinct().count()!
assert with_books == 2
```
With conditions, a `has_many` join keeps the records that have at least
one associated record matching them:
```glacier
five_stars = Book.join(:reviews, rating: 5).distinct().order(:title).pluck(:title)!
assert five_stars == ["A Wizard of Earthsea", "Dune"]
four_stars = Book.join(:reviews, rating: 4).pluck(:title)!
assert four_stars == ["Dune"]
```
### What a Joined Query Returns
A joined query returns records of its own model, read from its own table:
the joined table's columns aren't loaded, and its records aren't attached
to the results. `preload` loads them, as on any query:
```glacier
books = Book.join(:section, name: "Classics").preload(:author).order(:title)!
assert books[0].author.name == "Ursula K. Le Guin"
assert books[1].author.name == "Frank Herbert"
```
`order`, `pluck`, `ids`, `group` and the calculations work on the
query's own model's columns. The calculations see every joined row, so
after a `has_many` join, `group(:author_id).count()` counts each
author's reviews:
```glacier
classic_pages = Book.join(:section, name: "Classics").sum(:pages)!
assert classic_pages == 595
reviews_per_author = Book.joins(:reviews).group(:author_id).count()!
assert reviews_per_author == [(dune.author_id, 2), (earthsea.author_id, 1)]
```
WARNING: `distinct()` applies to the records, `pluck`, `ids` and
`count()`, but not to `sum`, `average`, `minimum`, `maximum` or `group`.
After a `has_many` join, these still see each record once per associated
row: Dune's 412 pages are counted twice here.
```glacier
assert Book.joins(:reviews).distinct().count()! == 2
assert Book.joins(:reviews).distinct().sum(:pages)! == 1007
```
`order_raw` and `Expr::raw` go into the SQL as written, so qualify a
column with its table when both tables have it:
```glacier
longest_first = Book.join(:section, name: "Classics").order_raw("books.pages DESC").pluck(:title)!
assert longest_first == ["Dune", "A Wizard of Earthsea"]
```
`update_all` and `delete_all` refuse a query with joins, returning an
error rather than running ("delete_all can't run on a query with joins;
filter by ids instead"). Read the `ids()` of the joined query, then update
or delete by id. `destroy_all` loads the records and destroys each one,
so after a `has_many` join, add `distinct()` so that each record is
destroyed once.
A `has_many … through:` a join table joins through it: `Book.joins(:tags)`
joins `books_tags`, then `tags` (see [Associations](associations.html#has-many-through-a-join-table)).
NOTE: A query can't yet join the same table twice, so `Book.join(:author, …).join(:translated_by, …)` fails
with a database error, because the second join of `authors` isn't given a
name of its own. Joins are inner joins; there
is no outer join.
## Updating and Deleting in Bulk (updated)
`update_all` sets columns on every matching row in one statement and
returns how many rows it changed. It skips validations and callbacks,
and doesn't touch `updated_at`:
```glacier
changed = Book.where(status: :draft).update_all(status: :published, featured: true)!
assert changed == 1
assert Book.where(status: :published).count()! == 4
```
`delete_all` deletes every matching row in one statement, without
callbacks, and returns how many there were:
```glacier
deleted = Book.where(title: "Dune").delete_all()!
assert deleted == 1
```
`destroy_all` loads the matching records and destroys each one in turn,
running its destroy callbacks and its `dependent:` associations, and
returns how many it destroyed. Here the `Author` model's `before_destroy`
deletes the author's books:
```glacier
destroyed = Author.where(name: "Frank Herbert").destroy_all()!
assert destroyed == 1
# Author's before_destroy deleted his books.
assert Book.count()! == 2
```
With a `limit` or `offset`, `update_all` and `delete_all` change only
the rows the query would load, in its order, as Rails does: Glacier
writes `WHERE id IN (SELECT id … ORDER BY … LIMIT 1)`.
```glacier
# Only the rows the query would load: the shortest book.
assert Book.all().order(pages: :asc).limit(1).delete_all()! == 1
assert Book.count()! == 2
```
## Scopes
A scope is a named, reusable piece of a query, declared in the model with
`scope`. A scope can take arguments, and its body is any query: `where`,
`order`, `filter` and the rest. These are `Book`'s:
```glacier
module Book do
implements Model
field title: String, required, length(max: 200), unique(scope: :author_id)
field isbn: String, required, unique, length(is: 13)
field slug: String
field pages: i32, numericality(greater_than: 0)
field price: Option<Decimal>
field status: enum(draft, published, out_of_print), default: :draft
field featured: bool, protected
belongs_to :author
belongs_to :section, default: Section.order(:position).first()!
belongs_to :translated_by, Author, foreign_key: :translator_id, optional
has_many :tags, through: :books_tags
has_many :reviews, nested, allow_destroy
has_many :comments, as: :commentable
scope published, where(status: :published)
scope by(author_id: i64), where(author_id: author_id)
scope longer_than(pages: i32), filter(fn b -> b.pages.gt(pages) end)
def summary(self) -> String do
"#{self.title} (#{self.pages} pages)"
end
def publish(self) do
self.update(status: :published)!
end
def average_price(books: &[Book]) -> Option<Decimal> do
prices = books.filter_map(:price)
if prices.is_empty() do
return None
end
count = Decimal.from(prices.len() as i64)
Some(prices.sum(fn p -> p.clone() end) / count)
end
before_save do
self.slug = self.title.to_lowercase().replace(" ", "-")
end
end
```
Scopes are called on the model and chain with each other and with every
other query call, in any order:
```glacier
assert Book.published().count()! == 3
assert Book.published().by(herbert_id).count()! == 2
assert Book.longer_than(400).published().count()! == 2
assert Book.where(title: "Dune").published().count()! == 1
```
### Default Scopes
A `default_scope` applies to every query of the model. The `Section`
model orders sections by their position:
```glacier
module Section do
implements Model
field name: String, required
field position: i32
has_many :books, order: "title desc"
default_scope order(:position)
end
```
`unscoped()` starts a query without it:
```glacier
names = Section.all().pluck(:name)!
assert names == ["Fiction", "Classics"]
by_name = Section.unscoped().order(:name).pluck(:name)!
assert by_name == ["Classics", "Fiction"]
```
A default scope can also preload associations
(`default_scope order(:position).preload(:books)`), so every section
would come with its books.
## Pagination
`glacier::pagination::Page` holds a page number and size. Given a `page`,
`page.of(query)` limits a query to that page, and `total_pages` works out how many pages a
count fills:
```glacier
page = glacier::pagination::Page { number: 2, per_page: 3 }
books = page.of(Book.order(:title))!
assert books.len() == 1
assert page.total_pages(Book.count()! as u64) == 2
```
A query can also page itself, with `query.page(page)`. Queries can be
cloned, so one query can be counted for the total and then paged, with the
same conditions in both:
```glacier
# Queries can be cloned, so one can be counted and then paged.
by_title = Book.order(:title)
total = by_title.clone().count()!
books = by_title.page(page)!
assert total == 4
assert books.len() == 1
```
In a controller, `Page.from_params(¶ms, 25)` reads `page` and
`per_page` from the request. The [JSON APIs](json_apis.html) guide shows
how `json …, paginate:` adds the pagination headers.
## Read Replicas
A busy app can send its reads to a replica, a copy of the database kept
up to date by the primary, and keep the primary for writes. Give the
replica's URL alongside the primary's, through the environment in
production:
```sh
GLACIER__DATABASE__URL=postgres://user:password@primary/api
GLACIER__DATABASE__REPLICA_URL=postgres://user:password@replica/api
```
`Repo::from_config` connects to both (`database.replica_pool_size` sizes
the replica's pool). Glacier then chooses per request, as Rails does:
- A `GET` or `HEAD` request reads from the replica.
- Every other request, and every write, uses the primary.
- Once a request writes, its reads go to the primary too, so it sees what
it wrote.
- For two seconds after a browser's request writes, that browser's
requests read from the primary as well. A cookie records when it last
wrote, so a redirect after a form shows the change even if the replica
hasn't received it yet.
Transactions and plain `Db.sql` always run on the primary. Outside a
request, in a job or a script, queries use the primary unless wrapped in
`db::reading(…)`. Inside a request, `db::writing(…)` keeps a read that must
be current on the primary:
```rust
let repo = primary.clone().with_replica(replica);
let client = TestClient::new(api::app(config, repo.clone()).unwrap());
// A book written to the primary, which the replica hasn't received.
db::with(&repo, async {
let author = insert::<Author>(Form::new().with("name", "Frank Herbert"))
.await
.unwrap();
let fields = Form::new()
.with("title", "Dune")
.with("isbn", "9780441013593");
insert::<Book>(fields.with("author_id", author.id))
.await
.unwrap();
})
.await;
// A GET reads the replica, so it doesn't see the book yet.
assert_eq!(client.get("/books").await.body_text(), "{\"books\":[]}");
// Outside a request, queries use the primary unless they ask.
db::with(&repo, async {
assert_eq!(Book::count().await.unwrap(), 1);
assert_eq!(db::reading(Book::count()).await.unwrap(), 0);
})
.await;
```
A replica is usually a moment behind the primary, as this test's never
catches up. Read from it only what can be slightly old.
## Queries in Rust
In Rust, `where` is `filter` with a closure, `order` takes a closure
returning `column.asc()` or `column.desc()`, and a query is run with
`.await?`. The closure's argument is the model's generated columns struct
(`BookColumns`), so the compiler checks each column name and the type of
each value, and the editor can complete them:
```rust
let long_books = Book::filter(|b| b.pages.gt(420).and(b.status.eq(BookStatus::Published)))
.order(|b| b.title.asc())
.limit(10)
.await?;
assert_eq!(long_books[0].title, "Children of Dune");
let titles: Vec<String> = Book::published()
.order(|b| b.pages.desc())
.pluck(|b| b.title)
.await?;
assert_eq!(titles, ["Children of Dune", "Dune"]);
```
`join` takes two closures in Rust: one picks the association from the
model's columns, and the other writes the condition on the associated
model's columns. `joins` takes the first:
```rust
.distinct()
.pluck(|b| b.title)
.await?;
assert_eq!(reviewed, ["Dune"]);
refused.to_string(),
"500 Internal Server Error: delete_all can't run on a query with joins; filter by ids instead"
);
```
`find_or_create_by` and `find_or_initialize_by` also take two closures:
the condition, and one that sets up the new record. In `.glacier`, the
keywords become both; in Rust, the set-up can set fields the condition
doesn't mention:
```rust
let ada = Member::find_or_create_by(
|m| m.email.eq("ada@example.com"),
|m| {
m.email = "ada@example.com".into();
m.name = "Ada Lovelace".into();
},
)
.await?;
assert!(ada.is_persisted());
assert_eq!(ada.name, "Ada Lovelace");
```
Scopes defined in `.glacier` are ordinary methods in Rust, as
`Book::published()` shows. In `.glacier`, a keyword query compiles to
the same kind of code: `Book.where(status: :published)` becomes a `filter`
closure that compares the `status` column.