glacier-web-cli 0.5.0

Command-line tool for creating and running Glacier applications.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
---
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
   |
17 |   scope published, where(stauts: :published)
   |                          ^^^^^^ unknown field
   |
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(&params, ["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"]
longest: i64 = Db.sql("SELECT MAX(pages) FROM books").value()! || 0
assert longest == 444
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(&params, 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
let reviewed: Vec<String> = Book::join(|b| b.author, |a| a.name.eq("Frank Herbert"))
    .joins(|b| b.reviews)
    .distinct()
    .pluck(|b| b.title)
    .await?;
assert_eq!(reviewed, ["Dune"]);

let refused = Book::joins(|b| b.reviews).delete_all().await.unwrap_err();
assert_eq!(
    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.