jade-sql 0.6.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,40 +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,
55
+ neq,
56
+ no_joins,
57
+ not,
47
58
  nullable,
48
- pk_values,
49
- render,
50
- set_,
59
+ or,
60
+ pk,
61
+ set,
62
+ set_excluded,
63
+ set_expr,
51
64
  sum,
52
65
  table,
53
66
  to_assigns,
54
- to_expr,
55
67
  transaction,
68
+ unique,
69
+ unkeyed,
70
+ unsafe_cast,
71
+ val,
72
+ within,
56
73
  )
57
74
 
58
75
  import Encode exposing (Encodable, encode)
@@ -72,21 +89,137 @@ struct Selector(a) = {
72
89
  }
73
90
 
74
91
 
75
- 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) = {
76
103
  name: String,
77
104
  alias_: String,
78
105
  cols: String -> c,
79
- maybe_cols: String -> m,
80
- pk_columns: List(String)
106
+ left_cols: String -> m,
107
+ set_cols: s,
108
+ pk: Pk(c, k),
109
+ on: o
81
110
  }
82
111
 
83
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
+
84
133
  struct TableRef = {
85
134
  name: String,
86
135
  alias_: String
87
136
  }
88
137
 
89
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
+
90
223
  struct Assignment = {
91
224
  col: String,
92
225
  value_sql: String,
@@ -94,8 +227,33 @@ struct Assignment = {
94
227
  }
95
228
 
96
229
 
97
- def set_(col: Expr(a), value: Expr(a)) -> Assignment
98
- 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, [])
99
257
  end
100
258
 
101
259
 
@@ -104,34 +262,26 @@ def assign(col: String, value: a) -> Assignment
104
262
  end
105
263
 
106
264
 
107
- def strip_alias(sql: String) -> String
108
- parts = String.split(sql, ".")
109
265
 
110
- case parts
111
- in [] then sql
112
- in [name] then name
113
- in [_ | rest] then String.join(rest, ".")
114
- end
266
+ interface Selectable(a) with
267
+ selector : Selector(a)
115
268
  end
116
269
 
117
270
 
118
- interface SqlMapper(a) with
271
+ interface Assignable(a) with
119
272
  to_assigns : a -> List(Assignment)
120
273
  end
121
274
 
122
275
 
123
- implements SqlMapper(List(Assignment)) with
276
+ implements Assignable(List(Assignment)) with
124
277
  to_assigns: (a) -> { a }
125
278
  end
126
279
 
127
280
 
128
- interface Identified(a) with
129
- pk_values : a -> List(Value)
130
- end
131
-
132
-
133
- interface Renderable(r) with
134
- 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))
135
285
  end
136
286
 
137
287
 
@@ -140,40 +290,60 @@ def column(alias_: String, name: String) -> Expr(a)
140
290
  end
141
291
 
142
292
 
143
- 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)
144
303
  Expr("?", [encode(value)])
145
304
  end
146
305
 
147
306
 
148
- # The DB clock (Postgres `now()`), for `where(col |> gt(now))`-style
149
- # comparisons. This is the transaction timestamp, not the app clock.
150
- 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)
151
314
  Expr("now()", [])
152
315
  end
153
316
 
154
317
 
155
- def eq(left: Expr(a), right: Expr(a)) -> Expr(Bool)
156
- 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)])
157
327
  end
158
328
 
159
329
 
160
- def gt(left: Expr(a), right: Expr(a)) -> Expr(Bool)
161
- 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)])
162
332
  end
163
333
 
164
334
 
165
- def gte(left: Expr(a), right: Expr(a)) -> Expr(Bool)
166
- 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)])
167
337
  end
168
338
 
169
339
 
170
- def lt(left: Expr(a), right: Expr(a)) -> Expr(Bool)
171
- 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)])
172
342
  end
173
343
 
174
344
 
175
- def lte(left: Expr(a), right: Expr(a)) -> Expr(Bool)
176
- 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)])
177
347
  end
178
348
 
179
349
 
@@ -192,18 +362,58 @@ def nullable(e: Expr(a)) -> Expr(Maybe(a))
192
362
  end
193
363
 
194
364
 
195
- 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)
196
374
  Expr(e.sql, e.params)
197
375
  end
198
376
 
199
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
+
200
399
  def and(left: Expr(Bool), right: Expr(Bool)) -> Expr(Bool)
201
400
  Expr(left.sql ++ " AND " ++ right.sql, left.params ++ right.params)
202
401
  end
203
402
 
204
403
 
404
+ # Parenthesised where `and` is not, because `where` joins its predicates with
405
+ # AND: without them `x = 1 AND a OR b` binds as `(x = 1 AND a) OR b`, which is
406
+ # not what anyone writing `or` meant.
407
+ def or(left: Expr(Bool), right: Expr(Bool)) -> Expr(Bool)
408
+ Expr(
409
+ "(" ++ left.sql ++ " OR " ++ right.sql ++ ")",
410
+ left.params ++ right.params,
411
+ )
412
+ end
413
+
414
+
205
415
  # SUM may return NULL on an empty group, so the result is Expr(Maybe(Int)).
206
- # Wrap with `coalesce(sum(x), to_expr(0))` for a strict total.
416
+ # Wrap with `coalesce(sum(x), 0)` for a strict total.
207
417
  def sum(e: Expr(Int)) -> Expr(Maybe(Int))
208
418
  Expr("SUM(" ++ e.sql ++ ")", e.params)
209
419
  end
@@ -219,10 +429,10 @@ def count_all -> Expr(Int)
219
429
  end
220
430
 
221
431
 
222
- def coalesce(maybe_e: Expr(Maybe(a)), default: Expr(a)) -> Expr(a)
432
+ def coalesce(col: Expr(Maybe(a)), default: a) -> Expr(a)
223
433
  Expr(
224
- "COALESCE(" ++ maybe_e.sql ++ ", " ++ default.sql ++ ")",
225
- maybe_e.params ++ default.params,
434
+ "COALESCE(" ++ col.sql ++ ", ?)",
435
+ col.params ++ [encode(default)],
226
436
  )
227
437
  end
228
438
 
@@ -285,8 +495,8 @@ def array_remove(col: Expr(List(a)), value: a) -> Expr(List(a))
285
495
  end
286
496
 
287
497
 
288
- def array_concat(left: Expr(List(a)), right: Expr(List(a))) -> Expr(List(a))
289
- 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)])
290
500
  end
291
501
 
292
502
 
@@ -296,8 +506,14 @@ end
296
506
  # jsonb_contains(column("rules", "match"), { kind: "income" })
297
507
  #
298
508
  # pg binds Ruby Hash/Array as jsonb when paired with a jsonb column.
299
- # The `@?` jsonpath operator needs an explicit `::jsonpath` cast since
300
- # 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.
301
517
 
302
518
 
303
519
  def jsonb_contains(col: Expr(Value), value: a) -> Expr(Bool)
@@ -306,11 +522,39 @@ end
306
522
 
307
523
 
308
524
  def jsonb_path_exists(col: Expr(Value), path: String) -> Expr(Bool)
309
- Expr(col.sql ++ " @? ?::jsonpath", col.params ++ [encode(path)])
525
+ Expr(
526
+ "jsonb_path_exists(" ++ col.sql ++ ", ?)",
527
+ col.params ++ [encode(path)],
528
+ )
529
+ end
530
+
531
+
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
310
554
  end
311
555
 
312
556
 
313
- def in_(col: Expr(a), values: List(a)) -> Expr(Bool)
557
+ def any_of(col: Expr(a), values: List(a)) -> Expr(Bool)
314
558
  case values
315
559
  in [] then Expr("FALSE", [])
316
560
  else in_nonempty(col, List.map(values, encode))
@@ -331,36 +575,48 @@ def table(
331
575
  name: String,
332
576
  alias_: String,
333
577
  cols: String -> c,
334
- maybe_cols: String -> m,
335
- pk_columns: List(String),
336
- ) -> Table(c, m)
337
- 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_)
338
584
  end
339
585
 
340
586
 
341
- def columns(t: Table(c, m), alias_: String) -> c
342
- 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
343
597
  end
344
598
 
345
599
 
346
- def maybe_columns(t: Table(c, m), alias_: String) -> m
347
- 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
348
604
  end
349
605
 
350
606
 
351
- def aliased(t: Table(c, m), alias_: String) -> Table(c, m)
352
- 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)
353
609
  end
354
610
 
355
611
 
356
- # `NotUnique` is a read-side failure (fetch_one saw more than one row).
357
- # `Conflict` is a write-side unique-index violation, carrying the violated
358
- # 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.
359
615
  type SqlError
360
616
  = DbError(String)
361
617
  | NotFound
362
- | NotUnique
363
- | Conflict(String)
618
+ | TooManyRows
619
+ | UniqueViolation(String)
364
620
 
365
621
 
366
622
  implements Encodable(SqlError) with
@@ -372,8 +628,8 @@ def encode_sql_error(e: SqlError) -> Value
372
628
  case e
373
629
  in DbError(msg) then Encode.variant("DbError", [Encode.string(msg)])
374
630
  in NotFound then Encode.variant("NotFound", [])
375
- in NotUnique then Encode.variant("NotUnique", [])
376
- 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)])
377
633
  end
378
634
  end
379
635
 
@@ -387,8 +643,8 @@ def sql_error_decoder -> Decoder(SqlError)
387
643
  Decode.type_
388
644
  |> Decode.variant("DbError", db_error_decoder)
389
645
  |> Decode.variant("NotFound", Decode.succeed(NotFound))
390
- |> Decode.variant("NotUnique", Decode.succeed(NotUnique))
391
- |> Decode.variant("Conflict", conflict_decoder)
646
+ |> Decode.variant("TooManyRows", Decode.succeed(TooManyRows))
647
+ |> Decode.variant("UniqueViolation", unique_violation_decoder)
392
648
  end
393
649
 
394
650
 
@@ -397,8 +653,8 @@ def db_error_decoder -> Decoder(SqlError)
397
653
  end
398
654
 
399
655
 
400
- def conflict_decoder -> Decoder(SqlError)
401
- Decode.index(1, Decode.string) |> Decode.map(Conflict)
656
+ def unique_violation_decoder -> Decoder(SqlError)
657
+ Decode.index(1, Decode.string) |> Decode.map(UniqueViolation)
402
658
  end
403
659
 
404
660
 
@@ -431,24 +687,18 @@ end
431
687
 
432
688
 
433
689
  def execute(r: r) -> Task(Int, SqlError)
434
- render(r) |> execute_raw
690
+ to_sql(r) |> execute_raw
435
691
  end
436
692
 
437
693
 
438
- def fetch_one(r: r) -> Task(a, SqlError)
439
- render(r) |> fetch_one_raw
440
- end
441
694
 
442
695
 
443
- def fetch_many(r: r) -> Task(List(a), SqlError)
444
- render(r) |> fetch_many_raw
445
- end
446
696
 
447
697
 
448
698
  # Runs `task` inside a single DB transaction on the shared connection:
449
699
  # every execute/fetch the task performs participates in it. Commits on
450
- # Ok, rolls back and re-raises the error on Err. Does not nest wrapping
451
- # 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.
452
702
  def transaction(task: Task(a, SqlError)) -> Task(a, SqlError)
453
703
  port_begin()
454
704
  |> Task.and_then((_) -> { commit_on_ok(task) })