a database layer insipred by caqti and ecto
1# Changesets
2
3Changesets are the heart of data validation in repodb. They track changes to your data and validate them before persisting to the database.
4
5## Creating Changesets
6
7Start with a record value and create a changeset:
8
9```ocaml
10open Repodb
11
12type user = { id : int; name : string; email : string; age : int }
13
14let empty_user = { id = 0; name = ""; email = ""; age = 0 }
15
16(* Create a changeset from a record *)
17let cs = Changeset.create empty_user
18```
19
20### For Insert vs Update
21
22Use explicit action markers for clarity:
23
24```ocaml
25(* For new records *)
26let cs = Changeset.for_insert empty_user
27
28(* For existing records *)
29let cs = Changeset.for_update existing_user
30```
31
32## Casting Parameters
33
34Cast external input (e.g., from HTTP requests) to your changeset:
35
36```ocaml
37let params = [("name", "Alice"); ("email", "alice@example.com"); ("age", "25")]
38
39let cs =
40 Changeset.create empty_user
41 |> Changeset.cast params ~fields:[name_field; email_field; age_field]
42```
43
44Only fields listed in `~fields` are accepted. Other parameters are ignored, protecting against mass assignment.
45
46### Working with Changes
47
48```ocaml
49(* Add a change programmatically *)
50let cs = Changeset.put_change name_field "Bob" cs
51
52(* Remove a change *)
53let cs = Changeset.delete_change name_field cs
54
55(* Get a specific change *)
56let name_change = Changeset.get_change cs name_field (* string option *)
57
58(* Get all changes as list *)
59let all_changes = Changeset.changes cs (* (string * change) list *)
60```
61
62## Validations
63
64### Required Fields
65
66```ocaml
67let cs =
68 Changeset.create empty_user
69 |> Changeset.cast params ~fields:[name_field; email_field]
70 |> Changeset.validate_required [name_field; email_field]
71```
72
73Adds error "can't be blank" if field is missing or empty string.
74
75### Format Validation
76
77```ocaml
78let cs =
79 cs |> Changeset.validate_format email_field ~pattern:"^[^@]+@[^@]+$"
80```
81
82Uses PCRE regular expressions. Adds error "has invalid format" on mismatch.
83
84### Length Validation
85
86```ocaml
87let cs =
88 cs
89 |> Changeset.validate_length name_field ~min:2 ~max:100
90 |> Changeset.validate_length password_field ~min:8
91 |> Changeset.validate_length pin_field ~is:4 (* exact length *)
92```
93
94Error messages:
95- "should be at least N character(s)"
96- "should be at most N character(s)"
97- "should be N character(s)"
98
99### Number Validation
100
101```ocaml
102let cs =
103 cs
104 |> Changeset.validate_number age_field ~greater_than_or_equal:0
105 |> Changeset.validate_number age_field ~less_than:150
106 |> Changeset.validate_number price_field ~greater_than:0
107```
108
109Options:
110- `~greater_than:n`
111- `~less_than:n`
112- `~greater_than_or_equal:n`
113- `~less_than_or_equal:n`
114
115### Inclusion/Exclusion
116
117```ocaml
118(* Value must be one of these *)
119let cs =
120 cs |> Changeset.validate_inclusion status_field ~values:["active"; "inactive"]
121
122(* Value must NOT be any of these *)
123let cs =
124 cs |> Changeset.validate_exclusion username_field ~values:["admin"; "root"]
125```
126
127### Acceptance (for checkboxes)
128
129```ocaml
130let cs = cs |> Changeset.validate_acceptance terms_field
131```
132
133Validates that value is `true`, `"true"`, or `"1"`.
134
135### Confirmation (password confirmation)
136
137```ocaml
138let cs =
139 cs |> Changeset.validate_confirmation password_field
140 ~confirmation_field:password_confirmation_field
141```
142
143Adds error "does not match" on the confirmation field if values differ.
144
145### Custom Validation
146
147```ocaml
148let cs =
149 cs |> Changeset.validate_change email_field (fun email ->
150 if String.contains email '+' then
151 Error "plus signs not allowed in email"
152 else
153 Ok ()
154 )
155```
156
157### Generic Validation Function
158
159For complex multi-field validation:
160
161```ocaml
162let cs =
163 cs |> Changeset.validate (fun cs ->
164 match Changeset.get_change cs start_date_field,
165 Changeset.get_change cs end_date_field with
166 | Some start_date, Some end_date when end_date < start_date ->
167 Changeset.add_error cs
168 ~field:"end_date"
169 ~message:"must be after start date"
170 ~validation:"date_range"
171 | _ -> cs
172 )
173```
174
175## Checking Validity
176
177```ocaml
178if Changeset.is_valid cs then
179 (* proceed with insert/update *)
180 let user = Changeset.data cs in
181 ...
182else
183 (* handle errors *)
184 let errors = Changeset.errors cs in
185 ...
186```
187
188### Error Handling
189
190```ocaml
191(* Get all errors *)
192let errors = Changeset.errors cs (* Error.validation_error list *)
193
194(* Get error messages as strings *)
195let messages = Changeset.error_messages cs (* ["name can't be blank"; ...] *)
196
197(* Check if specific field has error *)
198let has_name_error = Changeset.has_error cs name_field
199
200(* Get error for specific field *)
201let name_error = Changeset.get_error cs name_field (* validation_error option *)
202
203(* Iterate over errors *)
204Changeset.traverse_errors cs (fun field message ->
205 Printf.printf "%s: %s\n" field message
206)
207```
208
209### Extract Data
210
211```ocaml
212(* Get the underlying data (with changes applied - not automatic!) *)
213let user = Changeset.data cs
214
215(* Apply action - returns Ok data or Error errors *)
216let result = Changeset.apply_action cs
217(* result : (user, Error.validation_error list) result *)
218```
219
220## Database Constraints
221
222Mark fields for database-level constraint checking:
223
224```ocaml
225let cs =
226 Changeset.create empty_user
227 |> Changeset.cast params ~fields:[name_field; email_field]
228 |> Changeset.unique_constraint email_field
229 |> Changeset.foreign_key_constraint author_id_field
230 ~references:("users", "id")
231 |> Changeset.check_constraint ~name:"positive_age" age_field
232 ~expression:"age >= 0"
233```
234
235These don't validate in OCaml but help map database constraint violations back to changeset errors.
236
237## Nested Changesets
238
239### Single Association
240
241```ocaml
242let cs =
243 Changeset.create empty_post
244 |> Changeset.cast params ~fields:[title_field; body_field]
245 |> Changeset.cast_assoc_one ~assoc_name:"author"
246 ~params:author_params
247 ~cast_fn:create_author_changeset
248```
249
250Errors are prefixed: `"author.name can't be blank"`
251
252### Multiple Associations
253
254```ocaml
255let cs =
256 Changeset.create empty_post
257 |> Changeset.cast_assoc_many ~assoc_name:"comments"
258 ~params_list:[comment1_params; comment2_params]
259 ~cast_fn:create_comment_changeset
260```
261
262Errors are indexed: `"comments[0].body can't be blank"`
263
264### Embedded Data
265
266For JSON/embedded documents:
267
268```ocaml
269let cs =
270 Changeset.create empty_user
271 |> Changeset.cast_embed ~embed_name:"settings"
272 ~params:settings_params
273 ~parse:parse_settings
274```
275
276## Complete Example
277
278```ocaml
279let create_user_changeset params =
280 let empty_user = { id = 0; name = ""; email = ""; age = 0 } in
281 Changeset.create empty_user
282 |> Changeset.cast params ~fields:[name_field; email_field; age_field]
283 |> Changeset.validate_required [name_field; email_field]
284 |> Changeset.validate_format email_field ~pattern:"^[^@]+@[^@]+\\.[^@]+$"
285 |> Changeset.validate_length name_field ~min:2 ~max:100
286 |> Changeset.validate_number age_field ~greater_than_or_equal:0 ~less_than:150
287 |> Changeset.unique_constraint email_field
288
289let insert_user conn params =
290 let cs = create_user_changeset params in
291 if Changeset.is_valid cs then
292 let user = Changeset.data cs in
293 Repo.insert conn
294 ~table:users_table
295 ~columns:["name"; "email"; "age"]
296 ~values:[
297 Driver.Value.text user.name;
298 Driver.Value.text user.email;
299 Driver.Value.int user.age;
300 ]
301 else
302 Error (Error.Validation_failed (Changeset.error_messages cs))
303```
304
305## Next Steps
306
307- [Queries](queries.md) - Build and execute queries
308- [Repo](repo.md) - Database operations
309- [Associations](associations.md) - Define relationships