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
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
---
title: Models
description: Defining models in .glacier, their fields and types, and creating, reading, updating and deleting records with validations and callbacks.
category: Models
order: 1
---

Models are the M in MVC: a model maps a database table to a Rust struct,
and each record is one row. A record can be built, changed and saved in
place, and keeps track of its own errors and changes. You write models in `.glacier`, and
the app's build compiles each one to an ordinary Rust struct.

After reading this guide, you will know:

- How to generate and define a model, and how it maps to a table.
- Which field types and field options a model can have.
- How to create, read, update and delete records.
- How validations and callbacks work, and in which order they run.
- How timestamps are read from params, and how to work with times on a
  user's clock.
- How to write a model in plain Rust instead.

The examples come from a small library app with authors, books, sections,
tags, members, loans and reviews. Its tests run every example in this guide.

## Generating a Model

`glacier generate model` writes a model, a migration to create its table,
a factory and a test:

```shell
$ glacier generate model Book title:string isbn:string pages:integer author:references section:references
      create  db/migrations/m20261004100639_create_books.glacier
      create  src/models/book.glacier
      create  tests/factories/book.glacier
      create  tests/models/book.test.glacier
      update  tests/glacier.rs

Next: glacier db migrate
```

The field types it understands are `string`, `text`, `integer`, `bigint`,
`float`, `boolean` and `references`. A string or text field can be made
nullable with `:optional` (`bio:text:optional`), and any but a boolean or
a reference made unique with `:unique` (`email:string:unique`): the model
validates it and the migration adds a unique index. In a `.glacier` model, a
`references` field becomes a `belongs_to` association and an `<name>_id`
column; with `--dialect rust`, the generator writes only the `<name>_id`
field.

There's no list of models to keep: every file in `src/models/` is found
when the app builds.

Run `glacier db migrate` to create the table, as the
[Migrations](migrations.html) guide describes.

## Defining Models

A model is a module that `implements Model`, in
`src/models/<name>.glacier` (`model Name do … end` is the short form).
The library's models started out as the generator wrote them, then grew
the features this guide describes. Here is `Author`:

```glacier
module Author do
  implements Model

  field name: String, required, normalize: fn n -> n.trim().to_owned() end
  field bio: Option<String>
  has_many :books, order: :title
  has_many :translations, Book, foreign_key: :translator_id
  has_one :latest_book, Book, latest: :id
  has_many :comments, as: :commentable
  # Each book's reviews, by way of the author's books.
  has_many :reviews, through: :books

  def display_name(self) -> String do
    "#{self.name} (#{self.books.len()} books)"
  end

  before_destroy do
    Book.where(author_id: self.id).delete_all()!
  end
end
```

Fields and associations come first. Scopes, methods and callbacks follow,
with all of a model's scopes together and all of its callbacks together;
the compiler reports an error otherwise.

### Naming Conventions

The table is the model's name in snake_case, pluralised with English's
common rules and irregular words: `Author` reads and writes `authors`, a `BlogPost` model would
use `blog_posts`, and `Person` and `Shelf` would use `people` and
`shelves`. When the convention doesn't fit, give the table's name:
`implements Model, table: "staff"`.

### Ids and Timestamps

Every model gets an `id: i64` primary key, and two timestamps,
`inserted_at` and `updated_at`, which Glacier sets when a record is saved.
You don't declare them:

```glacier
assert author.inserted_at == author.updated_at
assert author.inserted_at <= Timestamp.now()
```

A model without timestamps says so, and its table then needs no
timestamp columns:

```glacier
module Tag do
  implements Model, timestamps: false

  field name: String, required, unique
end
```

`timestamps: :rails` maps the `created_at` and `updated_at` columns of a
schema a Rails app created, as `NaiveTimestamp` (a time without a zone,
as Rails stores them). A field you declare yourself wins over the one Glacier would add,
so `field id: i32` maps an `integer` key, and `field created_at:
Option<NaiveTimestamp>` a nullable one. The [Migrations](migrations.html)
guide has more on mapping an existing schema.

### Field Types

A field's type is a Rust type. These are the ones a column can be read
into:

| Type | Column | Notes |
| --- | --- | --- |
| `String` | text | `Option<String>` for a nullable column |
| `bool` | boolean | |
| `i32`, `i64` | integer, bigint | `i16` works too |
| `f64` | double precision / real | |
| `Decimal` | numeric | exact on Postgres; stored as a REAL on SQLite |
| `Timestamp` | timestamptz (Postgres), text (SQLite) | a UTC time |
| `NaiveTimestamp` | timestamp without time zone | |
| `Date` | date | |
| `JsonValue` | json / jsonb (Postgres), text (SQLite) | |
| `Uuid` | uuid | |
| `Vec<u8>` | bytea / blob | |
| `enum(…)` | text | see [Enums]#enums |

Wrap any of them in `Option<…>` when the column is nullable. A `Decimal`
holds an amount exactly, and prints without trailing zeros but with at
least one decimal place (`50.0`):

```glacier
book = insert(Book, price: "12.50")
assert book.price.unwrap().to_s() == "12.5"
```

A `Decimal` has no fixed precision. Adding, subtracting and multiplying
are exact. Dividing is exact when the quotient ends within the digits it keeps, as
`100 / 8` does; otherwise the quotient keeps a number of digits worked out from
the sizes and precision of the two numbers (36 decimal places for `1 /
3`) and its last digit is rounded. Dividing by zero panics:

```glacier
tenth = "0.1".parse::<Decimal>().unwrap()
fifth = "0.2".parse::<Decimal>().unwrap()
sum = tenth + fifth
assert sum.to_s() == "0.3"

share = Decimal.from(100) / Decimal.from(8)
assert share.to_s() == "12.5"

third = Decimal.from(1) / Decimal.from(3)
assert third.to_s() == "0.333333333333333333333333333333333333"
two_thirds = Decimal.from(2) / Decimal.from(3)
assert two_thirds.to_s() == "0.666666666666666666666666666666666667"
```

Decimals can be summed, so the `Book` model's `average_price` function,
shown in the next section, adds up the books' prices and divides by how
many there are. (The database can average a column too, but
`Book.average(:price)` returns an `f64`, as the
[Querying](querying.html#calculations) guide describes, and
`average_price` keeps the result exact.) Its first line,
`books.filter_map(:price)`, is the prices that are set:

```glacier
books = Book.all()!
assert Book.average_price(&books).unwrap().to_s() == "10.496666666666666666666666666666666667"
```

### Field Options (updated)

Options follow a field's type, separated by commas. Here is the `Book`
model, which uses most of them:

```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
```

| Option | Meaning |
| --- | --- |
| `required` | must be present: not `None`, and not empty or only whitespace |
| `required(message: "…")` | the same, with your own message |
| `unique` | no other row may have the same value |
| `unique(scope: :author_id)` | unique among rows with the same `author_id`; `scope: [:a, :b]` takes several |
| `length(min: 3, max: 200)` | text length in characters; `min:`, `max:` or `is:` |
| `format: "^[a-z-]+$"` | text must match a regular expression |
| `inclusion: [:draft, :published]` | must be one of the values, compared as text; atoms or strings |
| `numericality(greater_than: 0)` | must be a number; `numericality` alone, or with `greater_than:`, `greater_than_or_equal_to:`, `less_than:`, `less_than_or_equal_to:` and `only_integer: true` |
| `with: check` | runs your own function on the field's value: `fn check(value: Option<&Value>) -> Option<String>`, returning an error message or `None` |
| `protected` | never set from request params |
| `encrypted` | stored encrypted; see [Encrypted Fields]#encrypted-fields |
| `encrypted(deterministic)` | stored encrypted, and still searchable |
| `hidden` | never written to JSON; see [Hidden Fields]#hidden-fields |
| `virtual` | set from params like a field, but never stored |
| `default: :draft` | for `enum(…)` fields: the value a new record starts with |
| `normalize: fn n -> n.trim().to_owned() end` | the value's normal form, set before validation on every save (an `Option`'s value when there is one) |
| `confirmation` | must match `<field>_confirmation` when that's given; see [Confirmation and Acceptance]#confirmation-and-acceptance |
| `acceptance` | a checkbox that must be ticked when it's sent |

`normalize:` keeps a field in one form however it's typed, as Rails'
`normalizes` does. `Author` trims its name, so a name of only spaces is
blank:

```glacier
field name: String, required, normalize: fn n -> n.trim().to_owned() end
```

```glacier
test "an author's name is kept trimmed" do
  author = Author.create(params_for(Author, name: "  Ursula K. Le Guin "))!.unwrap()
  assert author.name == "Ursula K. Le Guin"
  # Only spaces is blank once trimmed.
  invalid = Author.create(params_for(Author, name: "   "))!.unwrap_invalid()
  assert invalid.errors_on("name") == ["can't be blank"]
end
```

`default:` is only for enums; other defaults belong in the migration, so
that the database fills them in. The [Validations](#validations) section
shows these options at work.

### Enums

`field status: enum(draft, published, out_of_print)` stores the value as
text (`"out_of_print"`) and gives the model a Rust enum named after the
model and field, `BookStatus`, with a variant per value. A new record
starts with the `default:` value, or the first one listed. In `.glacier`
you can assign an atom, and each value has a predicate:

```glacier
book = insert(Book, title: "Dune")
assert book.status == BookStatus::Draft
assert book.status.is_draft()

book.status = :out_of_print
book.save()!
assert Book.find(book.id)!.status.as_str() == "out_of_print"
```

Text that isn't one of the values fails validation with "is not one of
draft, published, out_of_print". The values don't become scopes on the
model: for `Book.published()`, write a [scope](querying.html#scopes), and
on a record use the enum's own `is_published()`.

### Stores

A `store` block reads typed settings out of a JSON column. In the `Member` model, `preferences` is the column
and each `field` in the block becomes a pair of methods:

```glacier
module Member do
  implements Model

  field name: String, required(message: "Please give your name")
  field email: String, required, unique, format: "^[^@ ]+@[^@ ]+$"
  field admin: bool, protected
  field password: Option<String>, virtual, confirmation
  field password_confirmation: Option<String>, virtual
  field preferences: Option<JsonValue>
  has_many :loans
  # The books on loan, by way of each loan's `book_id`.
  has_many :borrowed_books, Book, through: :loans
  # A member who leaves stays on file, for their loans.
  soft_deletes

  store :preferences do
    field language: String, default: "en"
    field newsletter: bool
    field nickname: Option<String>
  end

  # The sign-up form: a name and an email, nothing else.
  changeset signup do
    cast :name, :email
    validate :name, length(max: 50)
  end

  # Staff can change a member's name and make them an admin, which
  # `protected` keeps out of every other form.
  changeset staff_edit do
    cast :name, :admin
  end

  before_validation do
    self.email = self.email.trim().to_lowercase()
  end

  validate do
    if self.password.as_ref().is_some_and(fn p -> p.len() < 8 end) do
      errors.add("password", "is too short (minimum is 8 characters)")
    end
  end
end
```

```glacier
member = insert(Member)
assert member.language() == "en"
refute member.newsletter()

member.set_language("cy".to_owned())
member.set_newsletter(true)
member.save()!

member = Member.find(member.id)!
assert member.language() == "cy"
assert member.newsletter()
assert member.nickname() == None
```

A setting without a `default:` reads as its type's default (`false`, `0`,
an empty string), or `None` for an `Option`.

### What a Model Compiles To

`build.rs` compiles each `.glacier` file into the hidden `.glacier/`
folder before the Rust compiler sees it, keeping every line on the same
line number so that errors point at your source. The `Author` model above
compiles to:

```rust title=.glacier/models/author.rs
#![allow(unused_mut)] #[allow(unused_imports)] use glacier::prelude::*; #[allow(unused_imports)] use super::*; #[glacier::model(callbacks)] pub struct Author { // @generated by glacier from author.glacier; edit that file instead
  #[validate(required)] pub name: String,
  pub bio: Option<String>,
  #[glacier(has_many, order = "title")] pub books: ::glacier::db::HasMany<Book>,
  #[glacier(has_many, foreign_key = "translator_id")] pub translations: ::glacier::db::HasMany<Book>,

  } impl Author { pub fn display_name(&self) -> String {
    format!("{} ({} books)", self.name, self.books.len())
  }

  } impl ::glacier::Callbacks for Author { async fn before_destroy(&mut self) -> ::glacier::Result<()> { Ok({
    Book::filter(|__r| __r.author_id.eq_field(self.id)).delete_all().await?;
  }) }
}
```

You never edit this file, but it explains what you can do with a model:
it is a plain Rust struct with public fields, so `author.name` is a
`String` and the compiler checks every use of it. The
[Glacier Language](glacier_language.html) guide covers `.glacier` itself.

## Creating Records (updated)

`Model.new(…)` builds a record in memory, taking fields as keywords.
`save()` validates it and inserts it, returning `true`, or `false` if it
isn't valid:

```glacier
author = Author.new(name: "Ursula K. Le Guin")
assert author.is_new()
assert author.save()!
assert author.is_persisted()
assert author.id > 0
```

The `!` waits for the database, and fails the action (or test) on a
database error. Fields can also be assigned one at a time:

```glacier
author = Author.new()
author.name = "Octavia E. Butler"
author.bio = "American science fiction author"
author.save()!
```

`Model.build(params)` builds a record from request parameters instead,
casting text to each field's type, which is how a controller usually
starts. The [Controllers](controllers.html) guide shows it in a `create`
action.

`Model.create(params)` validates and inserts in one step, through a
changeset. It returns either `Saved(record)` or `Invalid(changeset)`,
and the changeset carries the errors and the submitted values:

```glacier
author = Author.create(params_for(Author, name: "N. K. Jemisin"))!.unwrap()
assert author.name == "N. K. Jemisin"

changeset = Author.create(params_for(Author, name: ""))!.unwrap_invalid()
assert changeset.errors_on("name") == ["can't be blank"]
```

Given fields as keywords, `create` sets them on a new record and saves
it, returning `Saved` or `Invalid` the same way:

```glacier
author = Author.create(name: "Ursula K. Le Guin")!.unwrap()
assert author.id > 0
changeset = Author.create(name: "")!.unwrap_invalid()
assert changeset.errors_on("name") == ["can't be blank"]
```

`create` runs the model's callbacks and its `validate do` checks, as
`save()` does and as Rails' `create` does: `Book.create(…)` runs `Book`'s
`before_save`, which fills in its `slug`. A virtual field in the params,
such as a `password`, reaches the callbacks too. `changeset.insert()` is
the write without them, for the rare case that wants it.

```glacier
# before_save fills in the slug, as save() does.
book = Book.create(params_for(Book, title: "Dune", author_id: author.id, section_id: section.id))!.unwrap()
assert book.slug == "dune"
```

## Reading Records

`find` loads a record by its id, and `find_by` loads the first record
matching some fields, or `None`:

```glacier
found = Author.find(author.id)!
assert found.name == "Frank Herbert"

missing = Author.find_by(name: "Nobody")!
assert missing.is_none()
```

A missing id is an error with a `404 Not Found` status, so in a controller
action `Book.find(id)!` turns into a "not found" response on its own:

```glacier
error = Author.find(999).await.unwrap_err()
assert error.status() == StatusCode::NOT_FOUND
```

`find_or_create_by` returns the first record matching some fields, or
creates one with them, and `find_or_initialize_by` builds one without
saving it. The [Querying](querying.html) guide covers these, `where`,
`order`, scopes and the rest.

## Updating Records

Change fields and call `save()` again, or pass the changes to
`update(…)`, which assigns them and saves. Both return whether the record
was valid. Only the columns that changed are written:

```glacier
assert book.update(pages: 896, price: Decimal.from(10))!
assert Book.find(book.id)!.pages == 896
```

`update` also takes request params (`book.update(params)!`), as
`build` does. For a model with a `has_many … nested` association, as
`Book`'s `reviews` is, `update` also creates, updates and destroys the
associated records from the params, saving them together with the record
(see [Associations](associations.html#nested-attributes)). A saved record
also has `reload()`, which reads its row again.

### Dirty Tracking

A record remembers the values it was loaded with. `is_changed()` says
whether anything has changed since, `changed("column")` whether one
column has, and `was("column")` gives the value as loaded:

```glacier
book = Book.find_by(title: "Dune")!.unwrap()
refute book.is_changed()

book.title = "Dune Messiah"
assert book.changed("title")
refute book.changed("pages")
assert book.was("title").unwrap().to_text() == "Dune"

book.save()!
refute book.is_changed()
```

`changes()` lists every changed column with its new value.

## Deleting Records

`destroy()` deletes a record's row, running its `before_destroy` and
`after_destroy` callbacks. Here the `Author` model's `before_destroy`
deletes the author's books first:

```glacier
author.destroy()!
assert Author.count()! == 0
assert Book.count()! == 0
```

`destroy()` and `delete()` take the record by value, so it can't be used
afterwards. `delete()` deletes the row without running callbacks. To
delete many rows
at once, use `delete_all` on a query, as the [Querying](querying.html)
guide shows.

A `has_many` association can remove its records itself when the owner is
destroyed: `dependent: :destroy` destroys each one with its own
callbacks, and `dependent: :delete_all` deletes them in one statement,
as `Author`'s callback does by hand. The
[Associations](associations.html) guide has the details. A foreign key
with `on_delete_cascade()` in a migration has the database delete them
instead.

### Soft Deletes (new)

A model with `soft_deletes` keeps a deleted record's row, setting its
`deleted_at` instead, as Laravel's `SoftDeletes` does. A library member
who leaves stays on file, for their loans:

```glacier
# A member who leaves stays on file, for their loans.
soft_deletes
```

`destroy()`, `delete()` and `delete_all()` then set `deleted_at`, and
every query leaves such rows out: `all`, `where`, `find` (`404`), counts,
and the associations that load them. `Member.with_trashed()` reads
them too and `Member.only_trashed()` only them; `record.is_trashed()`
says which a record is, `record.restore()` brings it back, and
`record.force_delete()` (or a query's `force_delete_all()`) deletes it
for good. Destroying a soft-deleted record leaves its `dependent:`
children, so that restoring it brings them back with it:

```glacier
test "a member who leaves stays on file" do
  member = insert(Member, name: "Ada", email: "ada@example.com")
  member.clone().destroy()!
  assert Member.count()! == 0
  assert Member.find_by(id: member.id)!.is_none()
  gone = Member.only_trashed().first()!.unwrap()
  assert gone.is_trashed()
  gone.restore()!
  assert Member.count()! == 1
end
```

The column comes from a migration's `soft_deletes`, in `create_table` or
`change_table`:

```glacier
migration do
  change_table :members do
    soft_deletes
  end
end
```

A `unique` field still counts soft-deleted rows, so a member who left
keeps their email address until they're force-deleted.

## Validations

Validations run every time a record is saved, inserted or updated. When
any fail, `save()` returns `false`, nothing is written, and the record
keeps its errors. `errors_on("column")` lists one column's messages, and
`full_messages()` gives every error with its column name in front,
humanised for people to read (`Title can't be blank`):

```glacier
book = Book.new(isbn: "123", pages: 96, author_id: author.id, section_id: section.id)
refute book.save()!
assert book.errors_on("title") == ["can't be blank"]
assert book.errors_on("isbn") == ["is too short (minimum is 13 characters)"]
assert book.full_messages() == ["Title can't be blank", "Isbn is too short (minimum is 13 characters)"]
```

`errors()` returns the same errors as `(column, message)` pairs, and
`errors_json()` as a JSON object of each column's messages
(`{"title": ["can't be blank"]}`).

A custom message replaces the standard one. The full message still
starts with the column name ("Name Please give your name"):

```glacier
member = Member.new(email: "reader@example.com")
refute member.save()!
assert member.errors_on("name") == ["Please give your name"]
```

### Confirmation and Acceptance (new)

`confirmation` on a field checks it against `<field>_confirmation`, as
Rails' does, when the confirmation is given: the second password box on
a sign-up form. `acceptance` is for a terms checkbox: when it's sent, it
must be ticked (`true`, `1`, `yes` or `on`), or the record gets "must be
accepted". Both suit `virtual` fields, which are never stored:

```glacier
field password: Option<String>, virtual, confirmation
field password_confirmation: Option<String>, virtual
```

```glacier
test "a password must match its confirmation" do
  member = Member.new(name: "Ada", email: "ada@example.com")
  member.password = Some("correct horse".to_owned())
  member.password_confirmation = Some("wrong horse".to_owned())
  refute member.save()!
  assert member.errors_on("password_confirmation") == ["doesn't match Password"]
end
```

### Uniqueness

`unique` checks with a query before writing, so it works without a
unique index:

```glacier
book = Book.build(params_for(Book, isbn: "9780441013593"))
refute book.save()!
assert book.errors_on("isbn") == ["has already been taken"]
```

Two requests can still race past the check, so add a unique index in the
migration as well. If the index rejects a write, the save returns `false`
with "has already been taken" on the column instead of failing with a
database error. `unique(scope: …)` only compares rows that share the
scope's columns, so two authors may each have a book called "Dune":

```glacier
other_author = insert(Author, name: "Someone Else")
same_title = Book.build(params_for(Book, title: "Dune", isbn: "9780000000001", author_id: other_author.id))
assert same_title.save()!

again = Book.build(params_for(Book, title: "Dune", isbn: "9780000000002", author_id: first.author_id))
refute again.save()!
assert again.errors_on("title") == ["has already been taken"]
```

### Format, Inclusion and Numbers

`format:` checks text against a regular expression, which matches
anywhere in the text unless it's anchored with `^` and `$`, as the
`Member` model's `email` is. `numericality` checks that a value is a
number within any bounds given, as `Book`'s `pages` must be more than 0:

```glacier
member = Member.new(name: "Ada", email: "not an address")
refute member.save()!
assert member.errors_on("email") == ["is invalid"]

book = Book.build(params_for(Book, pages: "0"))
refute book.save()!
assert book.errors_on("pages") == ["must be greater than 0"]
```

The messages are "is invalid" for `format:`, "is not included in the
list" for `inclusion:`, and for `numericality` "is not a number", "must
be an integer", or the bound that failed ("must be less than or equal to
100"). A `None` passes all three, so combine them with `required` when
a value must be given.

### Input That Doesn't Fit

When a record is built from params, text that can't be cast to a field's
type ("lots" for an `i32`) becomes an error on that field. The record
keeps the text, so `text("column")` can put it back in the form:

```glacier
book = Book.build(params_for(Book, pages: "lots"))
refute book.save()!
assert book.errors_on("pages") == ["is not a number"]
assert book.text("pages") == "lots"
```

### Protected Fields

A `protected` field is never set from params, so a request can't make
someone an admin. Your code can still set it directly:

```glacier
member = Member.build(params_for(Member, admin: "true"))
member.save()!
refute member.admin

member.admin = true
member.save()!
assert Member.find(member.id)!.admin
```

The `id` and timestamps are never set from params either.

### Hidden Fields

A `hidden` field is never written to JSON: not by `json user`, not by a
serializer's association without a serializer of its own, and not in
the OpenAPI schema. A secret the app keeps for itself, such as an API
key for another service, belongs there:

```glacier
field api_key: Option<String>, hidden
```

Some fields are hidden without saying so: `encrypted` ones, and any
whose name ends in `_digest`, so `password_digest` and `token_digest`
never leave the app, as Laravel's `$hidden` keeps them out. A
serializer can still write a hidden field by naming it, as it can
anything.

### Encrypted Fields

An `encrypted` field is stored encrypted and read back as plain text, as
Rails' `encrypts` does, so a leaked database or backup doesn't give away
what's in it. The `accounts` app keeps its users' two-factor secrets so:

```glacier
field otp_secret: Option<String>, protected, encrypted
```

```glacier
test "keeps the secret encrypted in the database" do
  user = insert(User, email_address: "user@example.com")
  secret = glacier::auth::totp::new_secret()
  user.enable_two_factor(&secret, &code_for(&secret))!
  stored = Db.sql("SELECT otp_secret FROM users").values::<String>()!
  assert stored[0].starts_with("gl1:")
  refute stored[0].contains(&secret)
  assert User.find(user.id)!.otp_secret == Some(secret)
end
```

It works for `String` and `Option<String>` fields, with AES-256-GCM. The
key comes from the config: `encryption.key`, or the app's
`secret_key_base` when that isn't set. To change the key, move the old
one to `encryption.previous_keys`; values encrypted with it are still
read, and are encrypted with the new key when next saved changed:

```toml
[encryption]
key = "…"              # GLACIER__ENCRYPTION__KEY
previous_keys = ["…"]
```

Changing `secret_key_base` changes the key too, when `encryption.key`
isn't set, so set `encryption.key` in production.

Each value is encrypted with a new random nonce, so the same text is
stored differently every time. That means an encrypted field can't be
searched: `User.where(otp_secret: …)` finds nothing.

A field that must be found, such as an email address someone signs in
with, can be `encrypted(deterministic)` instead, as Rails'
`deterministic: true`:

```glacier
field email_address: String, encrypted(deterministic), unique
```

The same text is then stored the same way, so `where`, `find_by` and
`unique` work on it: Glacier encrypts the value a query compares the
field with, with the current key and any previous ones, so rows written
before a key changed are still found. The price is that the database
shows which rows hold the same value, so keep it for what needs
finding. A value stored
before the field was encrypted is read as it is, so a field can become
`encrypted` without a data migration. So is a value a Rails or Laravel
app encrypted, given its keys (see
[Existing Rails Apps](authentication.html#encrypted-columns)).

### Custom Validations

A `validate do … end` block holds a model's own checks. `errors.add`
records an error, and any error stops the save. This is also the place to
check a `virtual` field, which can't take validation options because it has
no column:

```glacier
member = Member.new(name: "Ada", email: "ada@example.com", password: "short")
refute member.save()!
assert member.errors_on("password") == ["is too short (minimum is 8 characters)"]

member.password = "long enough"
assert member.save()!
```

NOTE: Glacier's built-in validations are `required`, `unique`, `length`,
`format:`, `inclusion:` and `numericality`, plus `with:` for a check of
your own on one field. There's no built-in check that a confirmation
field matches or that a box was ticked; write those in a `validate`
block or a `with:` function.

### Named Changesets (updated)

Different forms often need different fields. A visitor signing up gives
a name and an email, while staff can also make someone an admin. A
`changeset` block names one such form. `cast` lists the fields it takes
from params, and `validate` adds rules of its own, written as a field's
options:

```glacier
# The sign-up form: a name and an email, nothing else.
changeset signup do
  cast :name, :email
  validate :name, length(max: 50)
end

# Staff can change a member's name and make them an admin, which
# `protected` keeps out of every other form.
changeset staff_edit do
  cast :name, :admin
end
```

Pass params to it, and the changeset it returns to `create`, or to
`update_changeset` for an existing record:

```glacier
form = params_for(Member, name: "Ada", email: "ada@example.com", admin: "true")
member = Member.create(Member.signup(form))!.unwrap()
refute member.admin

form = params_for(Member, admin: "true")
member = member.update_changeset(Member.staff_edit(form))!.unwrap()
assert member.admin
```

Only the listed fields are cast, so the sign-up form ignores `admin`.
`staff_edit` can set it even though it's `protected`, because naming a
field in `cast` is permission to set it. The model's field validations
still run, along with the changeset's own rules:

```glacier
form = params_for(Member, name: "Ada Augusta King, Countess of Lovelace, née Byron, of London")
saved = Member.create(Member.signup(form))!
assert errors_on(saved) =~ "name is too long (maximum is 50 characters)"
```

A named changeset saves with the model's callbacks too, as `create` and
`update_changeset` do: `before_validation` runs first, then the
changeset's rules and the model's validations, then `before_save` and
the rest.

In Rust, a named changeset is a function that returns a `Changeset`, and
`glacier::validate!` takes the `#[validate(…)]` syntax:

```rust
impl Member {
    pub fn signup(params: impl Into<Params>) -> Changeset<Member> {
        let params = &params.into();
        let changeset = Member::changeset()
            .cast(|m| m.name, params)
            .cast(|m| m.email, params);
        glacier::validate!(changeset, name, length = ..=50)
    }
}
```

## Callbacks

Callbacks are blocks that run at points in a record's life. `save()` runs
them in this order:

1. `before_validation`
2. the validations, including `validate`
3. `before_save`
4. `before_create` (a new record) or `before_update` (a saved one)
5. the insert or update
6. `after_create` or `after_update`
7. `after_save`

`destroy()` runs `before_destroy` while the row still exists, deletes
it, then runs `after_destroy`. A callback is `name do … end` in the model, after its
fields. The `Member` model above tidies the email address before it's
validated, and `Book` sets its `slug` before every save:

```glacier
member = Member.new(name: "Grace", email: "  Grace@Example.COM ")
member.save()!
assert member.email == "grace@example.com"

book = insert(Book, title: "The Left Hand of Darkness")
assert book.slug == "the-left-hand-of-darkness"
```

Callbacks can change fields. A change made in `before_save`, as `Book`
makes, is checked by the validations again and then written.

If a validation fails, the callbacks after it don't run and `save()`
returns `false`. If a callback returns an error, `save()` returns it. An
error in a `before_…` callback stops the write; by the time an `after_…`
callback runs, the row has already been written.

When a write happens, the record is replaced by the row the database
returned, so the `after_…` callbacks see what was stored. Anything that
isn't a column, such as a `virtual` field, is reset at that point. (Saving a
record with no changed columns writes nothing, so its virtual fields keep
their values.) Here a save that writes clears a virtual field:

```glacier
assert member.password == None
```

Use a `virtual` field's value in a callback that runs before the write,
such as `before_save`.

## Model Methods

`def` adds a method. A method that takes `self` is an instance method;
one without is called on the model (`Book.some_function()`). A method
whose body waits on the database with `!` becomes async and is called with
`!` too:

```glacier
assert book.summary() == "Dune (412 pages)"
book.publish()!
assert Book.find(book.id)!.status.is_published()
```

Parameters and return types are Rust types. A parameter can be a
closure (`keep: impl Fn(&Book) -> bool`), and one taken by value rather
than by reference can be changed in the body.

`defp` makes a private method. See [Glacier Language](glacier_language.html)
for the syntax inside methods.

## Dates and Times

A `Timestamp` field holds a time in UTC. The library's `Loan` model
records when a book is due back, and when it came back:

```glacier
module Loan do
  implements Model

  field due_at: Timestamp
  field returned_at: Option<Timestamp>
  belongs_to :member
  belongs_to :book, touch

  # The last second of `day` on the clock of `zone`, as UTC.
  def due_end_of(day: Date, zone: &str) -> Option<Timestamp> do
    end_of_day = day.and_hms_opt(23, 59, 59)?
    glacier::time::local_to_utc(end_of_day, zone).map(fn at -> at.and_utc() end)
  end

  def overdue(self) -> bool do
    self.returned_at.is_none() and self.due_at < Timestamp.now()
  end

  def due_on_clock(self, zone: &str) -> String do
    glacier::time::format_in_zone(self.due_at, zone, "%-d %b %Y, %H:%M").unwrap_or_default()
  end
end
```

### The Current Time

`Timestamp.now()` is the time now, in UTC, and `Date.today()` is today's
date. Lengths of time read as they do in Rails: `30.days()`,
`2.hours()`, `90.minutes()`, and `.ago()` or `.from_now()` turns one into
a time:

```glacier
def overdue(self) -> bool do
  self.returned_at.is_none() and self.due_at < Timestamp.now()
end
```

Use these rather than reading the system clock, so that tests can move
the time (see [Testing](testing.html#time-travel)).

### Timestamps from Params

When a record is built from params, a `Timestamp` field reads RFC 3339
text, which carries its own offset, and converts it to UTC:

```glacier
loan = Loan.build(params_for(Loan, due_at: "2026-10-19T17:00:00+01:00"))
assert loan.due_at.to_rfc3339() == "2026-10-19T16:00:00+00:00"
```

A date and time without an offset, separated by `T` or a space, is read
as UTC. That covers the value an HTML `datetime-local` input sends:

```glacier
loan = Loan.build(params_for(Loan, due_at: "2026-10-19T17:00"))
assert loan.due_at.to_rfc3339() == "2026-10-19T17:00:00+00:00"
```

Seconds and fractions of a second are optional, and a trailing ` UTC` is
allowed, so the text a `Timestamp` displays as
(`2026-10-19 16:00:00.250 UTC`) reads back as the same time. A timestamp
put into a form, or passed to a factory, comes back unchanged:

```glacier
due = Timestamp.now()
assert due.to_s().ends_with(" UTC")
loan = Loan.build(params_for(Loan, due_at: due))
assert loan.due_at == due
```

A `NaiveTimestamp` field reads the same forms, and a `Date` field reads a
date as an HTML `date` input sends it (`2026-10-19`). Anything else is an
error on the field, "is not a valid date and time" (or "is not a valid
date" for a `Date`), and the record keeps the text, as it does for any
[input that doesn't fit](#input-that-doesn-t-fit):

```glacier
loan = Loan.build(params_for(Loan, due_at: "next Tuesday"))
refute loan.save()!
assert loan.errors_on("due_at") == ["is not a valid date and time"]
```

### Times on a User's Clock (updated)

A `datetime-local` value is read as UTC, but the person who typed it
meant their own clock. `glacier::time::parse(text, zone)` reads a date or
time in one of the common forms on the clock of `zone`, an IANA name such
as `"Europe/London"`, and returns the time in UTC:

- a date alone (`2026-10-19`) is midnight on that clock;
- a date and time (`2026-10-19 17:00`, `2026-10-19T17:00:00.5`) is that
  time on that clock;
- RFC 3339 text keeps its own offset, and an HTTP date
  (`Mon, 19 Oct 2026 17:00:00 GMT`) is read as GMT.

It returns `None` for anything else, and for a date or time without an
offset when it can't place it on the clock: in a zone it doesn't know,
or at a time that never happens because the clocks go forward:

```glacier
london = "Europe/London"
midnight = glacier::time::parse("2026-10-19", london).unwrap()
assert midnight.to_rfc3339() == "2026-10-18T23:00:00+00:00"

five = glacier::time::parse("2026-10-19 17:00", london).unwrap()
assert five.to_rfc3339() == "2026-10-19T16:00:00+00:00"

with_offset = glacier::time::parse("2026-10-19T17:00:00Z", london).unwrap()
assert with_offset.to_rfc3339() == "2026-10-19T17:00:00+00:00"

assert glacier::time::parse("next Tuesday", london) == None
assert glacier::time::parse("2026-10-19 17:00", "Mars/Olympus") == None
# The clocks went from 01:00 to 02:00 on 29 March 2026.
assert glacier::time::parse("2026-03-29 01:30", london) == None
```

In a controller, read the param's text and assign what `parse` returns,
rather than letting `build` read it as UTC.

`glacier::time::local_to_utc(local, zone)` turns a `NaiveTimestamp` on
the clock of `zone` into one in UTC, for storing. A time that happens
twice, when the clocks go back, takes the earlier; a time that never
happens, when they go forward, gives `None`, as does a zone it doesn't
know. The `Loan` model's `due_end_of` uses it to find the last second of
a day on the member's clock, and `and_utc()` makes the result a
`Timestamp`:

```glacier
day = Date.from_ymd_opt(2026, 10, 19).unwrap()
due = Loan.due_end_of(day, "Europe/London").unwrap()
assert due.to_rfc3339() == "2026-10-19T22:59:59+00:00"

winter = Date.from_ymd_opt(2026, 12, 7).unwrap()
assert Loan.due_end_of(winter, "Europe/London").unwrap().to_rfc3339() == "2026-12-07T23:59:59+00:00"
```

To show a time, `glacier::time::format_in_zone(at, zone, format)` formats
a `Timestamp` on the clock of `zone` with a `strftime` pattern, and
`format_naive_in_zone` does the same for a `NaiveTimestamp` stored as
UTC. Both return `None` for a zone they don't know. `Loan`'s
`due_on_clock` uses it:

```glacier
loan = insert(Loan, due_at: "2026-10-19T22:59:59Z")
assert loan.due_on_clock("Europe/London") == "19 Oct 2026, 23:59"
assert loan.due_on_clock("America/New_York") == "19 Oct 2026, 18:59"
```

### The App's Own Clock (new)

`time_zone` in the config is the clock the app shows times on when it
has no user's own to use (UTC without one), as Rails'
`config.time_zone`:

```toml
# The clock pages show times on, when there's no user's own to use.
time_zone = "Europe/London"
```

`at.local(format)` formats a timestamp on it, `glacier::time::parse_local(text)`
reads what someone typed on it, and `glacier::time::zone()` names it:

```glacier
test "times on the app's clock" do
  noon = "2026-07-01T12:00:00Z".parse::<Timestamp>().unwrap()
  # London is an hour ahead of UTC in summer.
  assert noon.local("%H:%M") == "13:00"
  assert glacier::time::parse_local("2026-07-01 13:00") == Some(noon)
  assert glacier::time::zone() == "Europe/London"
end
```

Times are still stored in UTC. An app that knows each user's zone
passes it to the functions above instead. An unknown name stops the app
at start-up.

## Models in Another Database

Some data belongs in a database of its own: analytics that would slow
the main database down, or an existing system's tables. Name the database
in the config:

```toml
[databases.analytics]
url = "sqlite://db/analytics_development.sqlite3"
```

In production, set its URL through the environment, as
`GLACIER__DATABASES__ANALYTICS__URL`. A `replica_url` gives it a replica,
as for the main database (see
[Read Replicas](querying.html#read-replicas)).

A model names its database after its name. This one lives with the rest
of the analytics code, in `src/analytics/`: a model can go in any folder,
as `implements Model` is what makes it one, and every controller,
serializer and test sees it as they see the models in `src/models/`. So
can a controller, job, mailer, client or middleware: each is what it
implements, wherever it is:

```glacier
# A book's page was seen. Views live in the analytics database, apart from
# the catalogue, so counting them never slows lending down.
module BookView do
  implements Model, database: :analytics

  field book_id: i64
end
```

Everything else about the model is as usual. Its queries and saves go to
the analytics database, connected when a request first uses it, and
everything else stays in the main one:

```glacier
test "views are counted in the analytics database" do
  book = insert(Book, title: "Dune")
  BookView.new(book_id: book.id).save()!
  BookView.new(book_id: book.id).save()!

  assert BookView.where(book_id: book.id).count()! == 2
  assert Book.count()! == 1
end
```

The database's migrations live in `db/analytics/migrations/`, beside its
schema in `db/analytics/schema.sql`, laid out as `db/migrations/` is.
`glacier generate migration create_book_views --database analytics`
writes one there, and the app runs them, as
`analytics_migrations::all()`, with no line of its own:

```glacier
create_table :book_views do
  # Another database: an id, not a foreign key.
  integer :book_id, null: false
  timestamps
end
add_index :book_views, :book_id
```

`glacier db migrate` runs the main database's migrations, then each named
database's. `glacier db status` lists them all, and
`GLACIER_DATABASE=analytics glacier db rollback` rolls back the named one.
Tests migrate a fresh named database, as they do the main one.

A few things don't cross between databases. A `belongs_to` or `has_many`
can't join a model in another database, so `BookView` keeps a plain
`book_id`. A transaction covers one database, so `transaction do … end`
doesn't include `BookView`'s writes. In Rust, `db::transaction_in(Some("analytics"), …)`
runs a transaction on the analytics database.

## Transactions

`transaction do … end` runs its body in one database transaction (see
[The Glacier Language](glacier_language.html)). Every save inside it
commits together, or none do.

### Multi-step Transactions

When a transaction has several steps and the caller needs to know which
one failed, build a `Multi`, as Ecto's. Each step has a name, runs in
order, and can read the results of the steps before it. `insert` and
`update` take a changeset, `run` takes any async work, and `enqueue`
queues a background job that is only queued if everything commits. A
`Multi` is written in Rust for now:

```rust
use glacier::db::{Changeset, Done, Failure, Multi, MultiError};
use library::models::Loan;

/// A new member joins and borrows their first book, in one transaction.
async fn join_and_borrow(params: Params, book_id: i64) -> Result<Done, MultiError> {
    Multi::new()
        .insert("member", Member::signup(params))
        .run("loan", move |done| async move {
            let member: &Member = done.get("member").unwrap();
            let due_at = 21.days().from_now();
            let loan = Changeset::<Loan>::new()
                .set(|l| l.member_id, member.id)
                .set(|l| l.book_id, book_id)
                .set(|l| l.due_at, due_at);
            Loan::create(loan).await?.into_step()
        })
        .commit()
        .await
}
```

`commit` returns the results by step name, or a `MultiError` naming the
step that failed and why. A step whose changeset was invalid fails with
its validation errors, and nothing the earlier steps wrote is kept:

```rust
match join_and_borrow(form("Ada", "ada@example.com"), dune.id).await {
    Ok(done) => {
        let loan: &Loan = done.get("loan").unwrap();
        assert_eq!(loan.book_id, dune.id);
    }
    Err(err) => panic!("{err}"),
}

let long_name = "Augusta Ada King, Countess of Lovelace, née Byron, of Marylebone";
let err = join_and_borrow(form(long_name, "augusta@example.com"), dune.id)
    .await
    .unwrap_err();
assert_eq!(err.step, "member");
assert!(matches!(err.failure, Failure::Invalid { .. }));
assert_eq!(
    err.to_string(),
    "step `member` was invalid: Name is too long (maximum is 50 characters)"
);
```

In an action, `commit().await?` turns an invalid step into a 422
response carrying its errors, and any other failure into its own error.

## Models in Rust

A model can be a plain Rust file, `src/models/<name>.rs`. `models!`
finds it as it finds `.glacier` files, and `glacier generate model …
--dialect rust` writes one (declaring it with a `mod` line instead). The `.glacier` features are attributes there:
`#[glacier::model]` adds the id, timestamps and the record's state,
`#[validate(…)]` holds field validations, `#[glacier::enumeration]`
declares an enum stored as text, `#[glacier::scopes]` declares scopes,
and `impl Callbacks` holds callbacks (with `callbacks` passed to
`#[glacier::model]`):

```rust
use glacier::prelude::*;

#[glacier::model(callbacks)]
pub struct Review {
    #[validate(required)]
    body: Option<String>,
    rating: i32,
    status: ReviewStatus,
    book_id: i64,
}

#[glacier::enumeration]
pub enum ReviewStatus {
    Pending,
    Approved,
}

#[glacier::scopes]
impl Review {
    fn approved(reviews: Query<Self>) -> Query<Self> {
        reviews.filter(|r| r.status.eq(ReviewStatus::Approved))
    }

    fn rated_at_least(reviews: Query<Self>, stars: i32) -> Query<Self> {
        reviews.filter(|r| r.rating.ge(stars))
    }
}

impl Callbacks for Review {
    async fn before_save(&mut self) -> glacier::Result<()> {
        self.rating = self.rating.clamp(1, 5);
        Ok(())
    }
}
```

Rust and `.glacier` models use each other freely. The library's `Book`
has `has_many :reviews`, and tests in `.glacier` use `Review` like any
other model:

```glacier
review = Review.new()
review.book_id = book.id
review.body = Some("Gripping".to_owned())
review.rating = 9
review.save()!
assert review.rating == 5
assert review.status == ReviewStatus::Pending

review.update(status: :approved)!
assert Review.approved().rated_at_least(4).count()! == 1
```

Scopes in Rust take typed column closures; the [Querying](querying.html)
guide shows the methods they can use.