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.
- checksums.yaml +4 -4
- data/README.md +16 -10
- data/docs/building.md +411 -113
- data/docs/running.md +103 -18
- data/exe/jade-sql +37 -9
- data/lib/jade-sql/bin/generate_schema.rb +702 -44
- data/lib/jade-sql/compiler/assignable.rb +146 -0
- data/lib/jade-sql/compiler/columns.rb +220 -0
- data/lib/jade-sql/compiler/errors.rb +82 -0
- data/lib/jade-sql/compiler.rb +32 -0
- data/lib/jade-sql/runtime.rb +74 -23
- data/lib/jade-sql/schema_drift.rb +115 -0
- data/lib/jade-sql/sql/expr.jd +65 -0
- data/lib/jade-sql/sql/json.jd +154 -0
- data/lib/jade-sql/sql/query.jd +309 -93
- data/lib/jade-sql/sql/write.jd +618 -0
- data/lib/jade-sql/sql.jd +389 -103
- data/lib/jade-sql/tasks.rake +41 -8
- data/lib/jade-sql/version.rb +1 -1
- data/lib/jade-sql.rb +92 -14
- metadata +15 -8
- data/lib/jade-sql/sql/mutation.jd +0 -464
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
|
-
|
|
5
|
-
|
|
6
|
+
NoJoins,
|
|
7
|
+
NoKey,
|
|
8
|
+
NoRequiredCols,
|
|
9
|
+
Pk,
|
|
6
10
|
Selector(..),
|
|
7
11
|
SqlError(..),
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
55
|
+
neq,
|
|
56
|
+
no_joins,
|
|
57
|
+
not,
|
|
48
58
|
nullable,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
120
|
-
|
|
260
|
+
def assign(col: String, value: a) -> Assignment
|
|
261
|
+
Assignment(col, "?", [encode(value)])
|
|
121
262
|
end
|
|
122
263
|
|
|
123
264
|
|
|
124
|
-
|
|
125
|
-
|
|
265
|
+
|
|
266
|
+
interface Assignable(a) with
|
|
267
|
+
to_assigns : a -> List(Assignment)
|
|
126
268
|
end
|
|
127
269
|
|
|
128
270
|
|
|
129
|
-
|
|
130
|
-
|
|
271
|
+
implements Assignable(List(Assignment)) with
|
|
272
|
+
to_assigns: (a) -> { a }
|
|
131
273
|
end
|
|
132
274
|
|
|
133
275
|
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
|
150
|
-
#
|
|
151
|
-
|
|
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(
|
|
157
|
-
Expr(
|
|
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
|
-
|
|
162
|
-
|
|
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
|
|
167
|
-
Expr(
|
|
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
|
|
172
|
-
Expr(
|
|
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
|
|
177
|
-
Expr(
|
|
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
|
-
|
|
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),
|
|
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(
|
|
427
|
+
def coalesce(col: Expr(Maybe(a)), default: a) -> Expr(a)
|
|
235
428
|
Expr(
|
|
236
|
-
"COALESCE(" ++
|
|
237
|
-
|
|
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(
|
|
301
|
-
Expr(
|
|
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
|
-
#
|
|
312
|
-
#
|
|
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(
|
|
520
|
+
Expr(
|
|
521
|
+
"jsonb_path_exists(" ++ col.sql ++ ", ?)",
|
|
522
|
+
col.params ++ [encode(path)],
|
|
523
|
+
)
|
|
322
524
|
end
|
|
323
525
|
|
|
324
526
|
|
|
325
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
359
|
-
|
|
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.
|
|
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
|
-
#
|
|
369
|
-
#
|
|
370
|
-
#
|
|
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
|
-
|
|
|
375
|
-
|
|
|
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
|
|
652
|
+
in DbError(msg) then named_variant("DbError", msg)
|
|
386
653
|
in NotFound then Encode.variant("NotFound", [])
|
|
387
|
-
in
|
|
388
|
-
in
|
|
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",
|
|
680
|
+
|> Decode.variant("DbError", named_decoder(DbError))
|
|
401
681
|
|> Decode.variant("NotFound", Decode.succeed(NotFound))
|
|
402
|
-
|> Decode.variant("
|
|
403
|
-
|> Decode.variant("
|
|
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
|
|
408
|
-
Decode.index(1, Decode.string) |> Decode.map(
|
|
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,
|
|
446
|
-
|
|
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.
|
|
463
|
-
#
|
|
464
|
-
def transaction(task: Task(a,
|
|
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,
|
|
748
|
+
def commit_on_ok(task: Task(a, e)) -> Task(a, e)
|
|
471
749
|
task
|
|
472
|
-
|> Task.and_then(
|
|
750
|
+
|> Task.and_then(commit_returning)
|
|
473
751
|
|> Task.on_error(rollback_then_fail)
|
|
474
752
|
end
|
|
475
753
|
|
|
476
754
|
|
|
477
|
-
def
|
|
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
|