jade-sql 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/jade-sql/sql.jd CHANGED
@@ -1,15 +1,24 @@
1
1
  module Sql exposing (
2
+ Assignable,
2
3
  Assignment(..),
4
+ Col(..),
3
5
  Expr(..),
4
- Identified,
5
- Renderable,
6
+ NoJoins,
7
+ NoKey,
8
+ NoRequiredCols,
9
+ Pk,
6
10
  Selector(..),
7
11
  SqlError(..),
8
- SqlMapper,
12
+ FromSqlError,
13
+ from_sql_error,
9
14
  Table,
10
15
  TableRef(..),
16
+ Unique,
17
+ matching,
18
+ ToSql,
11
19
  aliased,
12
20
  and,
21
+ any_of,
13
22
  array_append,
14
23
  array_concat,
15
24
  array_contained_by,
@@ -19,41 +28,48 @@ module Sql exposing (
19
28
  array_overlaps,
20
29
  array_remove,
21
30
  assign,
22
- cast,
23
31
  coalesce,
24
32
  column,
25
33
  columns,
26
34
  count,
27
35
  count_all,
36
+ db_now,
28
37
  eq,
38
+ excluded,
29
39
  execute,
30
40
  execute_raw,
31
- fetch_many,
32
41
  fetch_many_raw,
33
- fetch_one,
34
42
  fetch_one_raw,
35
43
  gt,
36
44
  gte,
37
- in_,
45
+ ilike,
38
46
  is_not_null,
39
47
  is_null,
40
48
  jsonb_contains,
41
49
  jsonb_path_exists,
50
+ left_columns,
51
+ like,
42
52
  lt,
43
53
  lte,
44
- maybe_columns,
45
54
  neg,
46
- now,
47
- or,
55
+ neq,
56
+ no_joins,
57
+ not,
48
58
  nullable,
49
- pk_values,
50
- render,
51
- set_,
59
+ or,
60
+ pk,
61
+ set,
62
+ set_excluded,
63
+ set_expr,
52
64
  sum,
53
65
  table,
54
66
  to_assigns,
55
- to_expr,
56
67
  transaction,
68
+ unique,
69
+ unkeyed,
70
+ unsafe_cast,
71
+ val,
72
+ within,
57
73
  )
58
74
 
59
75
  import Encode exposing (Encodable, encode)
@@ -73,21 +89,137 @@ struct Selector(a) = {
73
89
  }
74
90
 
75
91
 
76
- struct Table(c, m) = {
92
+ # A table, as `structure.sql` declares it. `c` is its strict column struct
93
+ # (`Expr(String)` for a NOT NULL text column), `m` the same columns lifted into
94
+ # `Maybe` for left-join projections, `k` the type of its primary key, `o` a
95
+ # record of one join predicate per foreign key it declares, `r` a struct of the
96
+ # columns an insert has to write, and `s` a record of `Col` accessors for the
97
+ # left of a `SET`.
98
+ #
99
+ # `schema.jd` generates one of these per table, so you name the table rather
100
+ # than build it: `from(patients)`, `insert(row, patients)`. Reach for `table`
101
+ # yourself only for something the generator does not see.
102
+ struct Table(c, m, k, o, r, s) = {
77
103
  name: String,
78
104
  alias_: String,
79
105
  cols: String -> c,
80
- maybe_cols: String -> m,
81
- pk_columns: List(String)
106
+ left_cols: String -> m,
107
+ set_cols: s,
108
+ pk: Pk(c, k),
109
+ on: o
82
110
  }
83
111
 
84
112
 
113
+ # The `on` of a table that declares no foreign keys. There is no relation to
114
+ # name, so there is no join predicate to reach for — join such a table by
115
+ # writing the predicate yourself:
116
+ #
117
+ # from(patients) |> join(events, (e) -> { p.id |> Expr.eq(e.patient_id) })
118
+ type NoJoins = NoJoinsOnly
119
+
120
+
121
+ # The `r` of a table an insert can leave entirely to the database — every
122
+ # column is nullable, or has a default, or is filled by its sequence. There is
123
+ # no struct of required columns to name, so there is nothing to write.
124
+ type NoRequiredCols = NoRequiredColsOnly
125
+
126
+
127
+ # The `on` you hand `table` for such a table.
128
+ def no_joins -> NoJoins
129
+ NoJoinsOnly
130
+ end
131
+
132
+
85
133
  struct TableRef = {
86
134
  name: String,
87
135
  alias_: String
88
136
  }
89
137
 
90
138
 
139
+ # A table's primary key: the columns that make it up, and how to spread a key
140
+ # value across them. `k` is what you pass to `update` and `delete` — `Int` for
141
+ # a serial id, `Uuid` for a uuid one, a tuple like `(Int, Int)` for a composite
142
+ # key, in the order `structure.sql` declares the columns.
143
+ #
144
+ # Phantom in `c`, the table's column struct, so a key belongs to the one table
145
+ # it came from and cannot be passed to another. Both halves are generated from
146
+ # the same DDL, so the columns and the values cannot fall out of step.
147
+ struct Pk(c, k) = {
148
+ name: String,
149
+ columns: List(String),
150
+ values: k -> List(Value)
151
+ }
152
+
153
+
154
+ # The key type of a table that has none — a join table, an append-only log,
155
+ # anything `structure.sql` declares without a PRIMARY KEY. `NoKey` has no
156
+ # constructor you can reach, so there is no value to pass where a key is
157
+ # expected.
158
+ type NoKey = NoKey
159
+
160
+
161
+ # The primary key of a table: the constraint that declares it, its key columns
162
+ # in DDL order, and how to spread a key value across them.
163
+ #
164
+ # pk("patients_pkey", ["id"], (v) -> { [Encode.encode(v)] })
165
+ def pk(name: String, key_cols: List(String), values: k -> List(Value)) -> Pk(c, k)
166
+ Pk(name, key_cols, values)
167
+ end
168
+
169
+
170
+ # The `Pk` of a table with no primary key. Reaching for it means giving up
171
+ # `update` and `delete`, which take a key; write to the table with
172
+ # `update_all`/`delete_all`, which take a predicate:
173
+ #
174
+ # delete_all(events, (c) -> { c.recorded_at |> lt(cutoff) })
175
+ #
176
+ # The generator passes this for you. You need it by hand only when writing a
177
+ # table function yourself.
178
+ def unkeyed -> Pk(c, NoKey)
179
+ Pk("", [], (v) -> { [] })
180
+ end
181
+
182
+
183
+ # A column on the left of a `SET`, carrying the name rather than an
184
+ # expression that happens to look like one. `set` reads the name off it, so
185
+ # there is nothing to recover from a rendered string and nothing to get wrong:
186
+ # an aggregate or a `COALESCE` cannot be assigned to, and now cannot be
187
+ # offered.
188
+ #
189
+ # The generator emits one record of these per table, which `update_all` hands
190
+ # to the builder alongside the column accessors.
191
+ struct Col(a) = { name: String }
192
+
193
+
194
+ # A unique index, by the name Postgres reports when it is violated. Phantom
195
+ # in the column struct, so an index cannot be used with a table it is not on.
196
+ # The generator emits one per unique index and per table-level UNIQUE.
197
+ struct Unique(c, k) = {
198
+ name: String,
199
+ columns: List(String),
200
+ values: k -> List(Value)
201
+ }
202
+
203
+
204
+ def unique(name: String, cols: List(String), values: k -> List(Value)) -> Unique(c, k)
205
+ Unique(name, cols, values)
206
+ end
207
+
208
+
209
+
210
+ # `email = ?`, or every column of a composite index joined by AND. The key
211
+ # type comes from the index, so the wrong one does not compile and a
212
+ # composite cannot be given in the wrong order.
213
+ def matching(u: Unique(c, k), key: k) -> Expr(Bool)
214
+ Expr(
215
+ u.columns
216
+ |> List.map((col) -> { col ++ " = ?" })
217
+ |> String.join(" AND "),
218
+ key |> u.values,
219
+ )
220
+ end
221
+
222
+
91
223
  struct Assignment = {
92
224
  col: String,
93
225
  value_sql: String,
@@ -95,44 +227,56 @@ struct Assignment = {
95
227
  }
96
228
 
97
229
 
98
- def set_(col: Expr(a), value: Expr(a)) -> Assignment
99
- Assignment(strip_alias(col.sql), value.sql, value.params)
230
+ # The assignment `update` and `update_all` take, given a column accessor and
231
+ # the value to write:
232
+ #
233
+ # [c.archived |> set(True)]
234
+ def set(col: Col(a), value: a) -> Assignment
235
+ Assignment(col.name, "?", [encode(value)])
100
236
  end
101
237
 
102
238
 
103
- def assign(col: String, value: a) -> Assignment
104
- Assignment(col, "?", [encode(value)])
239
+ # `set`, where the new value is built from the row rather than held by the
240
+ # caller:
241
+ #
242
+ # [c.tags |> set_expr(array_append(c.tags, tag))]
243
+ def set_expr(col: Col(a), value: Expr(a)) -> Assignment
244
+ Assignment(col.name, value.sql, value.params)
105
245
  end
106
246
 
107
247
 
108
- def strip_alias(sql: String) -> String
109
- parts = String.split(sql, ".")
248
+ # The row that could not be inserted, for the `DO UPDATE` arm of an upsert.
249
+ def excluded(col: Col(a)) -> Expr(a)
250
+ Expr("EXCLUDED." ++ col.name, [])
251
+ end
110
252
 
111
- case parts
112
- in [] then sql
113
- in [name] then name
114
- in [_ | rest] then String.join(rest, ".")
115
- end
253
+
254
+ # `email = EXCLUDED.email`, which is what an upsert wants nearly every time.
255
+ def set_excluded(col: Col(a)) -> Assignment
256
+ Assignment(col.name, "EXCLUDED." ++ col.name, [])
116
257
  end
117
258
 
118
259
 
119
- interface SqlMapper(a) with
120
- to_assigns : a -> List(Assignment)
260
+ def assign(col: String, value: a) -> Assignment
261
+ Assignment(col, "?", [encode(value)])
121
262
  end
122
263
 
123
264
 
124
- implements SqlMapper(List(Assignment)) with
125
- to_assigns: (a) -> { a }
265
+
266
+ interface Assignable(a) with
267
+ to_assigns : a -> List(Assignment)
126
268
  end
127
269
 
128
270
 
129
- interface Identified(a) with
130
- pk_values : a -> List(Value)
271
+ implements Assignable(List(Assignment)) with
272
+ to_assigns: (a) -> { a }
131
273
  end
132
274
 
133
275
 
134
- interface Renderable(r) with
135
- render : r -> (String, List(Value))
276
+ # Anything that can be handed to the database: a query, a write. The pair is
277
+ # the statement and its parameters, in the order the statement binds them.
278
+ interface ToSql(r) with
279
+ to_sql : r -> (String, List(Value))
136
280
  end
137
281
 
138
282
 
@@ -141,40 +285,60 @@ def column(alias_: String, name: String) -> Expr(a)
141
285
  end
142
286
 
143
287
 
144
- def to_expr(value: a) -> Expr(a)
288
+ # A value where an expression is wanted. The operators take values directly,
289
+ # so this is for the positions that cannot: a constant field in a projection
290
+ # or a JSON document.
291
+ #
292
+ # select(Row(_, _)) |> field(c.id) |> field_as(val("patient"), "kind")
293
+ # Json.object(Row(_)) |> Json.prop("kind", val("patient"))
294
+ #
295
+ # Every library that has this calls it `val`, and none of them has a word for
296
+ # the other direction, since there the expression side is the default.
297
+ def val(value: a) -> Expr(a)
145
298
  Expr("?", [encode(value)])
146
299
  end
147
300
 
148
301
 
149
- # The DB clock (Postgres `now()`), for `where(col |> gt(now))`-style
150
- # comparisons. This is the transaction timestamp, not the app clock.
151
- def now -> Expr(Instant)
302
+ # The database's clock, as `now()` in the statement — evaluated by Postgres
303
+ # when the query runs, at the start of the enclosing transaction. Not the
304
+ # app's clock: `Sql.Write.timestamped` and `stamped` use that one, so two
305
+ # rows written by one request share an instant this would not give them.
306
+ #
307
+ # where(s.expires_at |> Expr.gt(db_now))
308
+ def db_now -> Expr(Instant)
152
309
  Expr("now()", [])
153
310
  end
154
311
 
155
312
 
156
- def eq(left: Expr(a), right: Expr(a)) -> Expr(Bool)
157
- Expr(left.sql ++ " = " ++ right.sql, left.params ++ right.params)
313
+ def eq(col: Expr(a), value: a) -> Expr(Bool)
314
+ Expr(col.sql ++ " = " ++ "?", col.params ++ [encode(value)])
158
315
  end
159
316
 
160
317
 
161
- def gt(left: Expr(a), right: Expr(a)) -> Expr(Bool)
162
- Expr(left.sql ++ " > " ++ right.sql, left.params ++ right.params)
318
+ # `NULL <> x` is NULL, not true, so a nullable column never matches this.
319
+ # Pair it with `is_null` when "different, or absent" is what you mean.
320
+ def neq(col: Expr(a), value: a) -> Expr(Bool)
321
+ Expr(col.sql ++ " <> " ++ "?", col.params ++ [encode(value)])
163
322
  end
164
323
 
165
324
 
166
- def gte(left: Expr(a), right: Expr(a)) -> Expr(Bool)
167
- Expr(left.sql ++ " >= " ++ right.sql, left.params ++ right.params)
325
+ def gt(col: Expr(a), value: a) -> Expr(Bool)
326
+ Expr(col.sql ++ " > " ++ "?", col.params ++ [encode(value)])
168
327
  end
169
328
 
170
329
 
171
- def lt(left: Expr(a), right: Expr(a)) -> Expr(Bool)
172
- Expr(left.sql ++ " < " ++ right.sql, left.params ++ right.params)
330
+ def gte(col: Expr(a), value: a) -> Expr(Bool)
331
+ Expr(col.sql ++ " >= " ++ "?", col.params ++ [encode(value)])
173
332
  end
174
333
 
175
334
 
176
- def lte(left: Expr(a), right: Expr(a)) -> Expr(Bool)
177
- Expr(left.sql ++ " <= " ++ right.sql, left.params ++ right.params)
335
+ def lt(col: Expr(a), value: a) -> Expr(Bool)
336
+ Expr(col.sql ++ " < " ++ "?", col.params ++ [encode(value)])
337
+ end
338
+
339
+
340
+ def lte(col: Expr(a), value: a) -> Expr(Bool)
341
+ Expr(col.sql ++ " <= " ++ "?", col.params ++ [encode(value)])
178
342
  end
179
343
 
180
344
 
@@ -193,11 +357,40 @@ def nullable(e: Expr(a)) -> Expr(Maybe(a))
193
357
  end
194
358
 
195
359
 
196
- def cast(e: Expr(a)) -> Expr(b)
360
+ # Re-labels what a column expression claims to hold, without touching the SQL
361
+ # or the value. Nothing converts, and nothing checks: if the database returns
362
+ # something the new type cannot decode, the failure lands at decode time.
363
+ #
364
+ # Reach for it where the schema is less specific than the code, and the code
365
+ # is right — a `text` column the application only ever writes enum labels to:
366
+ #
367
+ # select(Visit(_)) |> field(v.status |> unsafe_cast)
368
+ def unsafe_cast(e: Expr(a)) -> Expr(b)
197
369
  Expr(e.sql, e.params)
198
370
  end
199
371
 
200
372
 
373
+ def not(e: Expr(Bool)) -> Expr(Bool)
374
+ # `NOT a AND b` binds as `(NOT a) AND b`.
375
+ Expr("NOT (" ++ e.sql ++ ")", e.params)
376
+ end
377
+
378
+
379
+ # `LIKE` is case-sensitive, `ILIKE` is not. Neither escapes `%` or `_` in the
380
+ # pattern — a user-supplied string containing either is a wildcard, so escape
381
+ # it yourself before it gets here.
382
+ #
383
+ # c.name |> like("Ada%")
384
+ def like(col: Expr(String), pattern: String) -> Expr(Bool)
385
+ Expr(col.sql ++ " LIKE " ++ "?", col.params ++ [encode(pattern)])
386
+ end
387
+
388
+
389
+ def ilike(col: Expr(String), pattern: String) -> Expr(Bool)
390
+ Expr(col.sql ++ " ILIKE " ++ "?", col.params ++ [encode(pattern)])
391
+ end
392
+
393
+
201
394
  def and(left: Expr(Bool), right: Expr(Bool)) -> Expr(Bool)
202
395
  Expr(left.sql ++ " AND " ++ right.sql, left.params ++ right.params)
203
396
  end
@@ -215,7 +408,7 @@ end
215
408
 
216
409
 
217
410
  # SUM may return NULL on an empty group, so the result is Expr(Maybe(Int)).
218
- # Wrap with `coalesce(sum(x), to_expr(0))` for a strict total.
411
+ # Wrap with `coalesce(sum(x), 0)` for a strict total.
219
412
  def sum(e: Expr(Int)) -> Expr(Maybe(Int))
220
413
  Expr("SUM(" ++ e.sql ++ ")", e.params)
221
414
  end
@@ -231,10 +424,10 @@ def count_all -> Expr(Int)
231
424
  end
232
425
 
233
426
 
234
- def coalesce(maybe_e: Expr(Maybe(a)), default: Expr(a)) -> Expr(a)
427
+ def coalesce(col: Expr(Maybe(a)), default: a) -> Expr(a)
235
428
  Expr(
236
- "COALESCE(" ++ maybe_e.sql ++ ", " ++ default.sql ++ ")",
237
- maybe_e.params ++ default.params,
429
+ "COALESCE(" ++ col.sql ++ ", ?)",
430
+ col.params ++ [encode(default)],
238
431
  )
239
432
  end
240
433
 
@@ -297,8 +490,8 @@ def array_remove(col: Expr(List(a)), value: a) -> Expr(List(a))
297
490
  end
298
491
 
299
492
 
300
- def array_concat(left: Expr(List(a)), right: Expr(List(a))) -> Expr(List(a))
301
- Expr(left.sql ++ " || " ++ right.sql, left.params ++ right.params)
493
+ def array_concat(col: Expr(List(a)), values: List(a)) -> Expr(List(a))
494
+ Expr(col.sql ++ " || ?", col.params ++ [encode(values)])
302
495
  end
303
496
 
304
497
 
@@ -308,8 +501,14 @@ end
308
501
  # jsonb_contains(column("rules", "match"), { kind: "income" })
309
502
  #
310
503
  # pg binds Ruby Hash/Array as jsonb when paired with a jsonb column.
311
- # The `@?` jsonpath operator needs an explicit `::jsonpath` cast since
312
- # the param is bound as text.
504
+ #
505
+ # Four of Postgres' jsonb operators spell themselves with a `?` — `@?`, `?`,
506
+ # `?|` and `?&` — and the runtime rewrites every `?` outside a quoted span
507
+ # into a `$n` placeholder. It cannot tell an operator from a parameter, since
508
+ # `meta ? 'kind'` and `id = ?` are the same character in the same position.
509
+ # So these render as the functions Postgres gives them instead, which contain
510
+ # no `?` and need no escaping. The functions also declare their parameter
511
+ # types, which is what the operator form needed a cast to supply.
313
512
 
314
513
 
315
514
  def jsonb_contains(col: Expr(Value), value: a) -> Expr(Bool)
@@ -318,11 +517,39 @@ end
318
517
 
319
518
 
320
519
  def jsonb_path_exists(col: Expr(Value), path: String) -> Expr(Bool)
321
- Expr(col.sql ++ " @? ?::jsonpath", col.params ++ [encode(path)])
520
+ Expr(
521
+ "jsonb_path_exists(" ++ col.sql ++ ", ?)",
522
+ col.params ++ [encode(path)],
523
+ )
322
524
  end
323
525
 
324
526
 
325
- def in_(col: Expr(a), values: List(a)) -> Expr(Bool)
527
+ # A column against a range, as `a..b` builds one. Both ends are inclusive,
528
+ # matching `Range.contains?`, so the whole span renders as SQL rather than
529
+ # being tested row by row:
530
+ #
531
+ # where(c.seen_on |> within(start..finish))
532
+ # where(c.seen_on |> within(Range.from(start)))
533
+ #
534
+ # The degenerate ends render as constants: an empty range is `FALSE`, an
535
+ # unbounded one `TRUE`, the same way `any_of([])` is `FALSE`.
536
+ def within(col: Expr(a), range: Range(a)) -> Expr(Bool)
537
+ case (Range.empty?(range), Range.lower(range), Range.upper(range))
538
+ in (True, _, _) then Expr("FALSE", [])
539
+
540
+ in (_, Just(low), Just(high))
541
+ then Expr(col.sql ++ " BETWEEN ? AND ?", col.params ++ [encode(low), encode(high)])
542
+
543
+ in (_, Just(low), Nothing) then Expr(col.sql ++ " >= ?", col.params ++ [encode(low)])
544
+
545
+ in (_, Nothing, Just(high)) then Expr(col.sql ++ " <= ?", col.params ++ [encode(high)])
546
+
547
+ in (_, Nothing, Nothing) then Expr("TRUE", [])
548
+ end
549
+ end
550
+
551
+
552
+ def any_of(col: Expr(a), values: List(a)) -> Expr(Bool)
326
553
  case values
327
554
  in [] then Expr("FALSE", [])
328
555
  else in_nonempty(col, List.map(values, encode))
@@ -343,36 +570,76 @@ def table(
343
570
  name: String,
344
571
  alias_: String,
345
572
  cols: String -> c,
346
- maybe_cols: String -> m,
347
- pk_columns: List(String),
348
- ) -> Table(c, m)
349
- Table(name, alias_, cols, maybe_cols, pk_columns)
573
+ left_cols: String -> m,
574
+ set_cols: s,
575
+ pk_: Pk(c, k),
576
+ on_: o,
577
+ ) -> Table(c, m, k, o, r, s)
578
+ Table(name, alias_, cols, left_cols, set_cols, pk_, on_)
350
579
  end
351
580
 
352
581
 
353
- def columns(t: Table(c, m), alias_: String) -> c
354
- alias_ |> t.cols
582
+ # A table's columns, bound to the alias the table carries. Reach for it to
583
+ # build a predicate outside a bind chain, where `from` has not handed you the
584
+ # columns yet:
585
+ #
586
+ # p = columns(persons)
587
+ # from(persons) |> where(p.name |> eq("Paul"))
588
+ #
589
+ # Use `aliased` first to read the same table under another name.
590
+ def columns(t: Table(c, m, k, o, r, s)) -> c
591
+ t.alias_ |> t.cols
355
592
  end
356
593
 
357
594
 
358
- def maybe_columns(t: Table(c, m), alias_: String) -> m
359
- alias_ |> t.maybe_cols
595
+ # The same columns as a left join produces them, every one lifted into
596
+ # `Maybe`.
597
+ def left_columns(t: Table(c, m, k, o, r, s)) -> m
598
+ t.alias_ |> t.left_cols
360
599
  end
361
600
 
362
601
 
363
- def aliased(t: Table(c, m), alias_: String) -> Table(c, m)
364
- Table(t.name, alias_, t.cols, t.maybe_cols, t.pk_columns)
602
+ def aliased(t: Table(c, m, k, o, r, s), alias_: String) -> Table(c, m, k, o, r, s)
603
+ Table(t.name, alias_, t.cols, t.left_cols, t.set_cols, t.pk, t.on)
365
604
  end
366
605
 
367
606
 
368
- # `NotUnique` is a read-side failure (fetch_one saw more than one row).
369
- # `Conflict` is a write-side unique-index violation, carrying the violated
370
- # constraint name so callers route it to a field error without string-matching.
607
+ # A violated constraint carries the name Postgres reported. Compare it
608
+ # against a generated index rather than a literal: `i == users_email_key.name`
609
+ # stops compiling when the index is renamed, where a literal quietly stops
610
+ # matching. `NotNullViolation` carries the column, since Postgres names no
611
+ # constraint for one. `DbError` is what is left over.
371
612
  type SqlError
372
613
  = DbError(String)
373
614
  | NotFound
374
- | NotUnique
375
- | Conflict(String)
615
+ | TooManyRows
616
+ | UniqueViolation(String)
617
+ | ForeignKeyViolation(String)
618
+ | CheckViolation(String)
619
+ | ExclusionViolation(String)
620
+ | NotNullViolation(String)
621
+ | Deadlock
622
+ | SerializationFailure
623
+ | StatementTimeout
624
+ | LockTimeout
625
+
626
+
627
+ # The error a caller wants is rarely the error the database gives. An app
628
+ # with its own error type implements this once and the runners hand back
629
+ # that type instead, so a read is not followed by a line whose only job is
630
+ # to change the error's shape.
631
+ #
632
+ # Keyed on the type being produced, not the one being consumed: `SqlError`
633
+ # is named in the function, so the interface needs one parameter.
634
+ interface FromSqlError(e) with
635
+ from_sql_error : SqlError -> e
636
+ end
637
+
638
+
639
+ # Without this every existing `Task(a, SqlError)` would stop resolving.
640
+ implements FromSqlError(SqlError) with
641
+ from_sql_error: identity
642
+ end
376
643
 
377
644
 
378
645
  implements Encodable(SqlError) with
@@ -382,14 +649,27 @@ end
382
649
 
383
650
  def encode_sql_error(e: SqlError) -> Value
384
651
  case e
385
- in DbError(msg) then Encode.variant("DbError", [Encode.string(msg)])
652
+ in DbError(msg) then named_variant("DbError", msg)
386
653
  in NotFound then Encode.variant("NotFound", [])
387
- in NotUnique then Encode.variant("NotUnique", [])
388
- in Conflict(name) then Encode.variant("Conflict", [Encode.string(name)])
654
+ in TooManyRows then Encode.variant("TooManyRows", [])
655
+ in UniqueViolation(name) then named_variant("UniqueViolation", name)
656
+ in ForeignKeyViolation(name) then named_variant("ForeignKeyViolation", name)
657
+ in CheckViolation(name) then named_variant("CheckViolation", name)
658
+ in ExclusionViolation(name) then named_variant("ExclusionViolation", name)
659
+ in NotNullViolation(name) then named_variant("NotNullViolation", name)
660
+ in Deadlock then Encode.variant("Deadlock", [])
661
+ in SerializationFailure then Encode.variant("SerializationFailure", [])
662
+ in StatementTimeout then Encode.variant("StatementTimeout", [])
663
+ in LockTimeout then Encode.variant("LockTimeout", [])
389
664
  end
390
665
  end
391
666
 
392
667
 
668
+ def named_variant(tag: String, name: String) -> Value
669
+ Encode.variant(tag, [Encode.string(name)])
670
+ end
671
+
672
+
393
673
  implements Decodable(SqlError) with
394
674
  decoder: sql_error_decoder
395
675
  end
@@ -397,20 +677,23 @@ end
397
677
 
398
678
  def sql_error_decoder -> Decoder(SqlError)
399
679
  Decode.type_
400
- |> Decode.variant("DbError", db_error_decoder)
680
+ |> Decode.variant("DbError", named_decoder(DbError))
401
681
  |> Decode.variant("NotFound", Decode.succeed(NotFound))
402
- |> Decode.variant("NotUnique", Decode.succeed(NotUnique))
403
- |> Decode.variant("Conflict", conflict_decoder)
682
+ |> Decode.variant("TooManyRows", Decode.succeed(TooManyRows))
683
+ |> Decode.variant("UniqueViolation", named_decoder(UniqueViolation))
684
+ |> Decode.variant("ForeignKeyViolation", named_decoder(ForeignKeyViolation))
685
+ |> Decode.variant("CheckViolation", named_decoder(CheckViolation))
686
+ |> Decode.variant("ExclusionViolation", named_decoder(ExclusionViolation))
687
+ |> Decode.variant("NotNullViolation", named_decoder(NotNullViolation))
688
+ |> Decode.variant("Deadlock", Decode.succeed(Deadlock))
689
+ |> Decode.variant("SerializationFailure", Decode.succeed(SerializationFailure))
690
+ |> Decode.variant("StatementTimeout", Decode.succeed(StatementTimeout))
691
+ |> Decode.variant("LockTimeout", Decode.succeed(LockTimeout))
404
692
  end
405
693
 
406
694
 
407
- def db_error_decoder -> Decoder(SqlError)
408
- Decode.index(1, Decode.string) |> Decode.map(DbError)
409
- end
410
-
411
-
412
- def conflict_decoder -> Decoder(SqlError)
413
- Decode.index(1, Decode.string) |> Decode.map(Conflict)
695
+ def named_decoder(build: String -> SqlError) -> Decoder(SqlError)
696
+ Decode.index(1, Decode.string) |> Decode.map(build)
414
697
  end
415
698
 
416
699
 
@@ -442,39 +725,42 @@ def fetch_many_raw(p: (String, List(Value))) -> Task(List(a), SqlError)
442
725
  end
443
726
 
444
727
 
445
- def execute(r: r) -> Task(Int, SqlError)
446
- render(r) |> execute_raw
728
+ def execute(r: r) -> Task(Int, e)
729
+ to_sql(r) |> execute_raw |> Task.map_error(from_sql_error)
447
730
  end
448
731
 
449
732
 
450
- def fetch_one(r: r) -> Task(a, SqlError)
451
- render(r) |> fetch_one_raw
452
- end
453
733
 
454
734
 
455
- def fetch_many(r: r) -> Task(List(a), SqlError)
456
- render(r) |> fetch_many_raw
457
- end
458
735
 
459
736
 
460
737
  # Runs `task` inside a single DB transaction on the shared connection:
461
738
  # every execute/fetch the task performs participates in it. Commits on
462
- # Ok, rolls back and re-raises the error on Err. Does not nest wrapping
463
- # a transaction in a transaction is unsupported for now (no savepoints).
464
- def transaction(task: Task(a, SqlError)) -> Task(a, SqlError)
739
+ # Ok, rolls back and re-raises the error on Err. Nests as a savepoint of
740
+ # whichever transaction is already open, jade's or ActiveRecord's.
741
+ def transaction(task: Task(a, e)) -> Task(a, e)
465
742
  port_begin()
743
+ |> Task.map_error(from_sql_error)
466
744
  |> Task.and_then((_) -> { commit_on_ok(task) })
467
745
  end
468
746
 
469
747
 
470
- def commit_on_ok(task: Task(a, SqlError)) -> Task(a, SqlError)
748
+ def commit_on_ok(task: Task(a, e)) -> Task(a, e)
471
749
  task
472
- |> Task.and_then((value) -> { Task.map(port_commit(), (_) -> { value }) })
750
+ |> Task.and_then(commit_returning)
473
751
  |> Task.on_error(rollback_then_fail)
474
752
  end
475
753
 
476
754
 
477
- def rollback_then_fail(err: SqlError) -> Task(a, SqlError)
755
+ def commit_returning(value: a) -> Task(a, e)
756
+ port_commit()
757
+ |> Task.map_error(from_sql_error)
758
+ |> Task.map((_) -> { value })
759
+ end
760
+
761
+
762
+ def rollback_then_fail(err: e) -> Task(a, e)
478
763
  port_rollback()
764
+ |> Task.map_error(from_sql_error)
479
765
  |> Task.and_then((_) -> { Task.fail(err) })
480
766
  end