jade-sql 0.7.0 → 0.8.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,
3
+ Selectable,
4
+ selector,
2
5
  Assignment(..),
6
+ Col(..),
3
7
  Expr(..),
4
- Identified,
5
- Renderable,
8
+ NoJoins,
9
+ NoKey,
10
+ NoRequiredCols,
11
+ Pk,
6
12
  Selector(..),
7
13
  SqlError(..),
8
- SqlMapper,
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,8 +227,33 @@ 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)])
236
+ end
237
+
238
+
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)
245
+ end
246
+
247
+
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
252
+
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, [])
100
257
  end
101
258
 
102
259
 
@@ -105,34 +262,26 @@ def assign(col: String, value: a) -> Assignment
105
262
  end
106
263
 
107
264
 
108
- def strip_alias(sql: String) -> String
109
- parts = String.split(sql, ".")
110
265
 
111
- case parts
112
- in [] then sql
113
- in [name] then name
114
- in [_ | rest] then String.join(rest, ".")
115
- end
266
+ interface Selectable(a) with
267
+ selector : Selector(a)
116
268
  end
117
269
 
118
270
 
119
- interface SqlMapper(a) with
271
+ interface Assignable(a) with
120
272
  to_assigns : a -> List(Assignment)
121
273
  end
122
274
 
123
275
 
124
- implements SqlMapper(List(Assignment)) with
276
+ implements Assignable(List(Assignment)) with
125
277
  to_assigns: (a) -> { a }
126
278
  end
127
279
 
128
280
 
129
- interface Identified(a) with
130
- pk_values : a -> List(Value)
131
- end
132
-
133
-
134
- interface Renderable(r) with
135
- render : r -> (String, List(Value))
281
+ # Anything that can be handed to the database: a query, a write. The pair is
282
+ # the statement and its parameters, in the order the statement binds them.
283
+ interface ToSql(r) with
284
+ to_sql : r -> (String, List(Value))
136
285
  end
137
286
 
138
287
 
@@ -141,40 +290,60 @@ def column(alias_: String, name: String) -> Expr(a)
141
290
  end
142
291
 
143
292
 
144
- def to_expr(value: a) -> Expr(a)
293
+ # A value where an expression is wanted. The operators take values directly,
294
+ # so this is for the positions that cannot: a constant field in a projection
295
+ # or a JSON document.
296
+ #
297
+ # select(Row(_, _)) |> field(c.id) |> field_as(val("patient"), "kind")
298
+ # Json.object(Row(_)) |> Json.prop("kind", val("patient"))
299
+ #
300
+ # Every library that has this calls it `val`, and none of them has a word for
301
+ # the other direction, since there the expression side is the default.
302
+ def val(value: a) -> Expr(a)
145
303
  Expr("?", [encode(value)])
146
304
  end
147
305
 
148
306
 
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)
307
+ # The database's clock, as `now()` in the statement — evaluated by Postgres
308
+ # when the query runs, at the start of the enclosing transaction. Not the
309
+ # app's clock: `Sql.Write.timestamped` and `stamped` use that one, so two
310
+ # rows written by one request share an instant this would not give them.
311
+ #
312
+ # where(s.expires_at |> Expr.gt(db_now))
313
+ def db_now -> Expr(Instant)
152
314
  Expr("now()", [])
153
315
  end
154
316
 
155
317
 
156
- def eq(left: Expr(a), right: Expr(a)) -> Expr(Bool)
157
- Expr(left.sql ++ " = " ++ right.sql, left.params ++ right.params)
318
+ def eq(col: Expr(a), value: a) -> Expr(Bool)
319
+ Expr(col.sql ++ " = " ++ "?", col.params ++ [encode(value)])
320
+ end
321
+
322
+
323
+ # `NULL <> x` is NULL, not true, so a nullable column never matches this.
324
+ # Pair it with `is_null` when "different, or absent" is what you mean.
325
+ def neq(col: Expr(a), value: a) -> Expr(Bool)
326
+ Expr(col.sql ++ " <> " ++ "?", col.params ++ [encode(value)])
158
327
  end
159
328
 
160
329
 
161
- def gt(left: Expr(a), right: Expr(a)) -> Expr(Bool)
162
- Expr(left.sql ++ " > " ++ right.sql, left.params ++ right.params)
330
+ def gt(col: Expr(a), value: a) -> Expr(Bool)
331
+ Expr(col.sql ++ " > " ++ "?", col.params ++ [encode(value)])
163
332
  end
164
333
 
165
334
 
166
- def gte(left: Expr(a), right: Expr(a)) -> Expr(Bool)
167
- Expr(left.sql ++ " >= " ++ right.sql, left.params ++ right.params)
335
+ def gte(col: Expr(a), value: a) -> Expr(Bool)
336
+ Expr(col.sql ++ " >= " ++ "?", col.params ++ [encode(value)])
168
337
  end
169
338
 
170
339
 
171
- def lt(left: Expr(a), right: Expr(a)) -> Expr(Bool)
172
- Expr(left.sql ++ " < " ++ right.sql, left.params ++ right.params)
340
+ def lt(col: Expr(a), value: a) -> Expr(Bool)
341
+ Expr(col.sql ++ " < " ++ "?", col.params ++ [encode(value)])
173
342
  end
174
343
 
175
344
 
176
- def lte(left: Expr(a), right: Expr(a)) -> Expr(Bool)
177
- Expr(left.sql ++ " <= " ++ right.sql, left.params ++ right.params)
345
+ def lte(col: Expr(a), value: a) -> Expr(Bool)
346
+ Expr(col.sql ++ " <= " ++ "?", col.params ++ [encode(value)])
178
347
  end
179
348
 
180
349
 
@@ -193,11 +362,40 @@ def nullable(e: Expr(a)) -> Expr(Maybe(a))
193
362
  end
194
363
 
195
364
 
196
- def cast(e: Expr(a)) -> Expr(b)
365
+ # Re-labels what a column expression claims to hold, without touching the SQL
366
+ # or the value. Nothing converts, and nothing checks: if the database returns
367
+ # something the new type cannot decode, the failure lands at decode time.
368
+ #
369
+ # Reach for it where the schema is less specific than the code, and the code
370
+ # is right — a `text` column the application only ever writes enum labels to:
371
+ #
372
+ # select(Visit(_)) |> field(v.status |> unsafe_cast)
373
+ def unsafe_cast(e: Expr(a)) -> Expr(b)
197
374
  Expr(e.sql, e.params)
198
375
  end
199
376
 
200
377
 
378
+ def not(e: Expr(Bool)) -> Expr(Bool)
379
+ # `NOT a AND b` binds as `(NOT a) AND b`.
380
+ Expr("NOT (" ++ e.sql ++ ")", e.params)
381
+ end
382
+
383
+
384
+ # `LIKE` is case-sensitive, `ILIKE` is not. Neither escapes `%` or `_` in the
385
+ # pattern — a user-supplied string containing either is a wildcard, so escape
386
+ # it yourself before it gets here.
387
+ #
388
+ # c.name |> like("Ada%")
389
+ def like(col: Expr(String), pattern: String) -> Expr(Bool)
390
+ Expr(col.sql ++ " LIKE " ++ "?", col.params ++ [encode(pattern)])
391
+ end
392
+
393
+
394
+ def ilike(col: Expr(String), pattern: String) -> Expr(Bool)
395
+ Expr(col.sql ++ " ILIKE " ++ "?", col.params ++ [encode(pattern)])
396
+ end
397
+
398
+
201
399
  def and(left: Expr(Bool), right: Expr(Bool)) -> Expr(Bool)
202
400
  Expr(left.sql ++ " AND " ++ right.sql, left.params ++ right.params)
203
401
  end
@@ -215,7 +413,7 @@ end
215
413
 
216
414
 
217
415
  # 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.
416
+ # Wrap with `coalesce(sum(x), 0)` for a strict total.
219
417
  def sum(e: Expr(Int)) -> Expr(Maybe(Int))
220
418
  Expr("SUM(" ++ e.sql ++ ")", e.params)
221
419
  end
@@ -231,10 +429,10 @@ def count_all -> Expr(Int)
231
429
  end
232
430
 
233
431
 
234
- def coalesce(maybe_e: Expr(Maybe(a)), default: Expr(a)) -> Expr(a)
432
+ def coalesce(col: Expr(Maybe(a)), default: a) -> Expr(a)
235
433
  Expr(
236
- "COALESCE(" ++ maybe_e.sql ++ ", " ++ default.sql ++ ")",
237
- maybe_e.params ++ default.params,
434
+ "COALESCE(" ++ col.sql ++ ", ?)",
435
+ col.params ++ [encode(default)],
238
436
  )
239
437
  end
240
438
 
@@ -297,8 +495,8 @@ def array_remove(col: Expr(List(a)), value: a) -> Expr(List(a))
297
495
  end
298
496
 
299
497
 
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)
498
+ def array_concat(col: Expr(List(a)), values: List(a)) -> Expr(List(a))
499
+ Expr(col.sql ++ " || ?", col.params ++ [encode(values)])
302
500
  end
303
501
 
304
502
 
@@ -308,8 +506,14 @@ end
308
506
  # jsonb_contains(column("rules", "match"), { kind: "income" })
309
507
  #
310
508
  # 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.
509
+ #
510
+ # Four of Postgres' jsonb operators spell themselves with a `?` — `@?`, `?`,
511
+ # `?|` and `?&` — and the runtime rewrites every `?` outside a quoted span
512
+ # into a `$n` placeholder. It cannot tell an operator from a parameter, since
513
+ # `meta ? 'kind'` and `id = ?` are the same character in the same position.
514
+ # So these render as the functions Postgres gives them instead, which contain
515
+ # no `?` and need no escaping. The functions also declare their parameter
516
+ # types, which is what the operator form needed a cast to supply.
313
517
 
314
518
 
315
519
  def jsonb_contains(col: Expr(Value), value: a) -> Expr(Bool)
@@ -318,11 +522,39 @@ end
318
522
 
319
523
 
320
524
  def jsonb_path_exists(col: Expr(Value), path: String) -> Expr(Bool)
321
- Expr(col.sql ++ " @? ?::jsonpath", col.params ++ [encode(path)])
525
+ Expr(
526
+ "jsonb_path_exists(" ++ col.sql ++ ", ?)",
527
+ col.params ++ [encode(path)],
528
+ )
322
529
  end
323
530
 
324
531
 
325
- def in_(col: Expr(a), values: List(a)) -> Expr(Bool)
532
+ # A column against a range, as `a..b` builds one. Both ends are inclusive,
533
+ # matching `Range.contains?`, so the whole span renders as SQL rather than
534
+ # being tested row by row:
535
+ #
536
+ # where(c.seen_on |> within(start..finish))
537
+ # where(c.seen_on |> within(Range.from(start)))
538
+ #
539
+ # The degenerate ends render as constants: an empty range is `FALSE`, an
540
+ # unbounded one `TRUE`, the same way `any_of([])` is `FALSE`.
541
+ def within(col: Expr(a), range: Range(a)) -> Expr(Bool)
542
+ case (Range.empty?(range), Range.lower(range), Range.upper(range))
543
+ in (True, _, _) then Expr("FALSE", [])
544
+
545
+ in (_, Just(low), Just(high))
546
+ then Expr(col.sql ++ " BETWEEN ? AND ?", col.params ++ [encode(low), encode(high)])
547
+
548
+ in (_, Just(low), Nothing) then Expr(col.sql ++ " >= ?", col.params ++ [encode(low)])
549
+
550
+ in (_, Nothing, Just(high)) then Expr(col.sql ++ " <= ?", col.params ++ [encode(high)])
551
+
552
+ in (_, Nothing, Nothing) then Expr("TRUE", [])
553
+ end
554
+ end
555
+
556
+
557
+ def any_of(col: Expr(a), values: List(a)) -> Expr(Bool)
326
558
  case values
327
559
  in [] then Expr("FALSE", [])
328
560
  else in_nonempty(col, List.map(values, encode))
@@ -343,36 +575,48 @@ def table(
343
575
  name: String,
344
576
  alias_: String,
345
577
  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)
578
+ left_cols: String -> m,
579
+ set_cols: s,
580
+ pk_: Pk(c, k),
581
+ on_: o,
582
+ ) -> Table(c, m, k, o, r, s)
583
+ Table(name, alias_, cols, left_cols, set_cols, pk_, on_)
350
584
  end
351
585
 
352
586
 
353
- def columns(t: Table(c, m), alias_: String) -> c
354
- alias_ |> t.cols
587
+ # A table's columns, bound to the alias the table carries. Reach for it to
588
+ # build a predicate outside a bind chain, where `from` has not handed you the
589
+ # columns yet:
590
+ #
591
+ # p = columns(persons)
592
+ # from(persons) |> where(p.name |> eq("Paul"))
593
+ #
594
+ # Use `aliased` first to read the same table under another name.
595
+ def columns(t: Table(c, m, k, o, r, s)) -> c
596
+ t.alias_ |> t.cols
355
597
  end
356
598
 
357
599
 
358
- def maybe_columns(t: Table(c, m), alias_: String) -> m
359
- alias_ |> t.maybe_cols
600
+ # The same columns as a left join produces them, every one lifted into
601
+ # `Maybe`.
602
+ def left_columns(t: Table(c, m, k, o, r, s)) -> m
603
+ t.alias_ |> t.left_cols
360
604
  end
361
605
 
362
606
 
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)
607
+ def aliased(t: Table(c, m, k, o, r, s), alias_: String) -> Table(c, m, k, o, r, s)
608
+ Table(t.name, alias_, t.cols, t.left_cols, t.set_cols, t.pk, t.on)
365
609
  end
366
610
 
367
611
 
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.
612
+ # `UniqueViolation` carries the violated constraint name. Compare it against
613
+ # a generated index rather than a literal: `i == users_email_key.name` stops
614
+ # compiling when the index is renamed, where a literal quietly stops matching.
371
615
  type SqlError
372
616
  = DbError(String)
373
617
  | NotFound
374
- | NotUnique
375
- | Conflict(String)
618
+ | TooManyRows
619
+ | UniqueViolation(String)
376
620
 
377
621
 
378
622
  implements Encodable(SqlError) with
@@ -384,8 +628,8 @@ def encode_sql_error(e: SqlError) -> Value
384
628
  case e
385
629
  in DbError(msg) then Encode.variant("DbError", [Encode.string(msg)])
386
630
  in NotFound then Encode.variant("NotFound", [])
387
- in NotUnique then Encode.variant("NotUnique", [])
388
- in Conflict(name) then Encode.variant("Conflict", [Encode.string(name)])
631
+ in TooManyRows then Encode.variant("TooManyRows", [])
632
+ in UniqueViolation(name) then Encode.variant("UniqueViolation", [Encode.string(name)])
389
633
  end
390
634
  end
391
635
 
@@ -399,8 +643,8 @@ def sql_error_decoder -> Decoder(SqlError)
399
643
  Decode.type_
400
644
  |> Decode.variant("DbError", db_error_decoder)
401
645
  |> Decode.variant("NotFound", Decode.succeed(NotFound))
402
- |> Decode.variant("NotUnique", Decode.succeed(NotUnique))
403
- |> Decode.variant("Conflict", conflict_decoder)
646
+ |> Decode.variant("TooManyRows", Decode.succeed(TooManyRows))
647
+ |> Decode.variant("UniqueViolation", unique_violation_decoder)
404
648
  end
405
649
 
406
650
 
@@ -409,8 +653,8 @@ def db_error_decoder -> Decoder(SqlError)
409
653
  end
410
654
 
411
655
 
412
- def conflict_decoder -> Decoder(SqlError)
413
- Decode.index(1, Decode.string) |> Decode.map(Conflict)
656
+ def unique_violation_decoder -> Decoder(SqlError)
657
+ Decode.index(1, Decode.string) |> Decode.map(UniqueViolation)
414
658
  end
415
659
 
416
660
 
@@ -443,24 +687,18 @@ end
443
687
 
444
688
 
445
689
  def execute(r: r) -> Task(Int, SqlError)
446
- render(r) |> execute_raw
690
+ to_sql(r) |> execute_raw
447
691
  end
448
692
 
449
693
 
450
- def fetch_one(r: r) -> Task(a, SqlError)
451
- render(r) |> fetch_one_raw
452
- end
453
694
 
454
695
 
455
- def fetch_many(r: r) -> Task(List(a), SqlError)
456
- render(r) |> fetch_many_raw
457
- end
458
696
 
459
697
 
460
698
  # Runs `task` inside a single DB transaction on the shared connection:
461
699
  # 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).
700
+ # Ok, rolls back and re-raises the error on Err. Nests as a savepoint of
701
+ # whichever transaction is already open, jade's or ActiveRecord's.
464
702
  def transaction(task: Task(a, SqlError)) -> Task(a, SqlError)
465
703
  port_begin()
466
704
  |> Task.and_then((_) -> { commit_on_ok(task) })