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/query.jd
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
module Sql.Query exposing (
|
|
2
|
-
|
|
2
|
+
Query,
|
|
3
|
+
Select,
|
|
3
4
|
field,
|
|
4
5
|
field_as,
|
|
6
|
+
distinct,
|
|
7
|
+
exists,
|
|
5
8
|
from,
|
|
6
9
|
group,
|
|
10
|
+
in_subquery,
|
|
11
|
+
having,
|
|
7
12
|
join,
|
|
8
13
|
left_join,
|
|
9
14
|
limit,
|
|
@@ -11,11 +16,27 @@ module Sql.Query exposing (
|
|
|
11
16
|
order,
|
|
12
17
|
order_desc,
|
|
13
18
|
select,
|
|
19
|
+
fetch_at_most_one,
|
|
20
|
+
fetch_count,
|
|
21
|
+
fetch_exists,
|
|
22
|
+
fetch_many,
|
|
23
|
+
fetch_one,
|
|
24
|
+
fetch_values,
|
|
14
25
|
to_sql,
|
|
26
|
+
subquery,
|
|
15
27
|
where,
|
|
28
|
+
filter,
|
|
16
29
|
)
|
|
17
30
|
|
|
18
|
-
import Sql exposing (
|
|
31
|
+
import Sql exposing (
|
|
32
|
+
Expr(..),
|
|
33
|
+
Selector(..),
|
|
34
|
+
SqlError(..),
|
|
35
|
+
Table,
|
|
36
|
+
TableRef(..),
|
|
37
|
+
ToSql,
|
|
38
|
+
from_sql_error,
|
|
39
|
+
)
|
|
19
40
|
import Decode exposing (Value)
|
|
20
41
|
|
|
21
42
|
|
|
@@ -50,39 +71,71 @@ struct GroupTerm = {
|
|
|
50
71
|
}
|
|
51
72
|
|
|
52
73
|
|
|
53
|
-
|
|
74
|
+
|
|
75
|
+
# A finished query: one you have projected columns into with `select` and
|
|
76
|
+
# `field`, and can hand to `fetch_one` / `fetch_many`. A bare `Query(c)` is a
|
|
77
|
+
# query mid-chain, still carrying a table's column accessors.
|
|
78
|
+
#
|
|
79
|
+
# def scheduled_visits -> Select(Visit)
|
|
80
|
+
type alias Select(a) = Query(Selector(a))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
struct Query(a) = {
|
|
54
84
|
tables: List(TableRef),
|
|
55
85
|
joins: List(Join),
|
|
56
86
|
wheres: List(Expr(Bool)),
|
|
57
87
|
groups: List(GroupTerm),
|
|
88
|
+
havings: List(Expr(Bool)),
|
|
58
89
|
orders: List(OrderTerm),
|
|
59
90
|
limit_: Maybe(Int),
|
|
60
91
|
offset_: Maybe(Int),
|
|
61
|
-
|
|
92
|
+
distinct_: Bool,
|
|
93
|
+
result: a,
|
|
94
|
+
result_alias: Maybe(String)
|
|
62
95
|
}
|
|
63
96
|
|
|
64
97
|
|
|
65
|
-
implements Chainable(
|
|
98
|
+
implements Chainable(Query(a)) with
|
|
66
99
|
and_then: q_and_then
|
|
67
100
|
end
|
|
68
101
|
|
|
69
102
|
|
|
70
|
-
def q_and_then(q:
|
|
103
|
+
def q_and_then(q: Query(a), fn: a -> Query(b)) -> Query(b)
|
|
71
104
|
next_ = q.result |> fn
|
|
72
105
|
|
|
73
|
-
|
|
74
|
-
q.tables
|
|
106
|
+
Query(
|
|
107
|
+
merge_tables(q.tables, next_.tables),
|
|
75
108
|
q.joins ++ next_.joins,
|
|
76
109
|
q.wheres ++ next_.wheres,
|
|
77
110
|
q.groups ++ next_.groups,
|
|
111
|
+
q.havings ++ next_.havings,
|
|
78
112
|
q.orders ++ next_.orders,
|
|
79
113
|
merge_paging(q.limit_, next_.limit_),
|
|
80
114
|
merge_paging(q.offset_, next_.offset_),
|
|
115
|
+
q.distinct_ || next_.distinct_,
|
|
81
116
|
next_.result,
|
|
117
|
+
next_.result_alias,
|
|
82
118
|
)
|
|
83
119
|
end
|
|
84
120
|
|
|
85
121
|
|
|
122
|
+
# One entry per name-and-alias. Two `from`s of different tables still cross
|
|
123
|
+
# join, and the same table under two aliases is still two tables; only the
|
|
124
|
+
# exact repeat collapses, and that repeat was a duplicate-alias error before.
|
|
125
|
+
def merge_tables(prev: List(TableRef), next_: List(TableRef)) -> List(TableRef)
|
|
126
|
+
List.fold(
|
|
127
|
+
next_,
|
|
128
|
+
prev,
|
|
129
|
+
(seen, t) -> { List.any?(seen, (s) -> { same_table?(s, t) }) ? seen : seen ++ [t] },
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def same_table?(a: TableRef, b: TableRef) -> Bool
|
|
135
|
+
a.name == b.name && a.alias_ == b.alias_
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
|
|
86
139
|
def merge_paging(prev: Maybe(Int), next_: Maybe(Int)) -> Maybe(Int)
|
|
87
140
|
case next_
|
|
88
141
|
in Just(_) then next_
|
|
@@ -91,164 +144,156 @@ def merge_paging(prev: Maybe(Int), next_: Maybe(Int)) -> Maybe(Int)
|
|
|
91
144
|
end
|
|
92
145
|
|
|
93
146
|
|
|
94
|
-
def from(t: Table(c, m)) ->
|
|
147
|
+
def from(t: Table(c, m, k, o, r, s)) -> Query(c)
|
|
95
148
|
cols_ = t.alias_ |> t.cols
|
|
96
149
|
|
|
97
|
-
|
|
150
|
+
Query(
|
|
151
|
+
[TableRef(t.name, t.alias_)],
|
|
152
|
+
[],
|
|
153
|
+
[],
|
|
154
|
+
[],
|
|
155
|
+
[],
|
|
156
|
+
[],
|
|
157
|
+
Nothing,
|
|
158
|
+
Nothing,
|
|
159
|
+
False,
|
|
160
|
+
cols_,
|
|
161
|
+
Just(t.alias_),
|
|
162
|
+
)
|
|
98
163
|
end
|
|
99
164
|
|
|
100
165
|
|
|
101
|
-
def join(t: Table(c, m), on_: c -> Expr(Bool)) ->
|
|
166
|
+
def join(t: Table(c, m, k, o, r, s), on_: c -> Expr(Bool)) -> Query(c)
|
|
102
167
|
cols_ = t.alias_ |> t.cols
|
|
103
168
|
pred = cols_ |> on_
|
|
104
169
|
|
|
105
|
-
|
|
170
|
+
Query(
|
|
106
171
|
[],
|
|
107
172
|
[Join(InnerJ, t.name, t.alias_, pred)],
|
|
108
173
|
[],
|
|
109
174
|
[],
|
|
110
175
|
[],
|
|
176
|
+
[],
|
|
111
177
|
Nothing,
|
|
112
178
|
Nothing,
|
|
179
|
+
False,
|
|
113
180
|
cols_,
|
|
181
|
+
Just(t.alias_),
|
|
114
182
|
)
|
|
115
183
|
end
|
|
116
184
|
|
|
117
185
|
|
|
118
|
-
def left_join(t: Table(c, m), on_: c -> Expr(Bool)) ->
|
|
186
|
+
def left_join(t: Table(c, m, k, o, r, s), on_: c -> Expr(Bool)) -> Query(m)
|
|
119
187
|
cols_strict = t.alias_ |> t.cols
|
|
120
|
-
cols_maybe = t.alias_ |> t.
|
|
188
|
+
cols_maybe = t.alias_ |> t.left_cols
|
|
121
189
|
pred = cols_strict |> on_
|
|
122
190
|
|
|
123
|
-
|
|
191
|
+
Query(
|
|
124
192
|
[],
|
|
125
193
|
[Join(LeftJ, t.name, t.alias_, pred)],
|
|
126
194
|
[],
|
|
127
195
|
[],
|
|
128
196
|
[],
|
|
197
|
+
[],
|
|
129
198
|
Nothing,
|
|
130
199
|
Nothing,
|
|
200
|
+
False,
|
|
131
201
|
cols_maybe,
|
|
202
|
+
Just(t.alias_),
|
|
132
203
|
)
|
|
133
204
|
end
|
|
134
205
|
|
|
135
206
|
|
|
136
|
-
def where(q:
|
|
137
|
-
|
|
138
|
-
q.tables,
|
|
139
|
-
q.joins,
|
|
140
|
-
q.wheres ++ [predicate],
|
|
141
|
-
q.groups,
|
|
142
|
-
q.orders,
|
|
143
|
-
q.limit_,
|
|
144
|
-
q.offset_,
|
|
145
|
-
q.result,
|
|
146
|
-
)
|
|
207
|
+
def where(q: Query(a), predicate: Expr(Bool)) -> Query(a)
|
|
208
|
+
{ q | wheres: q.wheres ++ [predicate] }
|
|
147
209
|
end
|
|
148
210
|
|
|
149
211
|
|
|
150
|
-
def
|
|
151
|
-
|
|
152
|
-
q.tables,
|
|
153
|
-
q.joins,
|
|
154
|
-
q.wheres,
|
|
155
|
-
q.groups ++ [GroupTerm(e.sql, e.params)],
|
|
156
|
-
q.orders,
|
|
157
|
-
q.limit_,
|
|
158
|
-
q.offset_,
|
|
159
|
-
q.result,
|
|
160
|
-
)
|
|
212
|
+
def filter(q: Query(a), build: a -> Expr(Bool)) -> Query(a)
|
|
213
|
+
where(q, build(q.result))
|
|
161
214
|
end
|
|
162
215
|
|
|
163
216
|
|
|
164
|
-
def
|
|
165
|
-
|
|
166
|
-
q.tables,
|
|
167
|
-
q.joins,
|
|
168
|
-
q.wheres,
|
|
169
|
-
q.groups,
|
|
170
|
-
q.orders ++ [OrderTerm(Asc, e.sql, e.params)],
|
|
171
|
-
q.limit_,
|
|
172
|
-
q.offset_,
|
|
173
|
-
q.result,
|
|
174
|
-
)
|
|
217
|
+
def group(q: Query(a), e: Expr(b)) -> Query(a)
|
|
218
|
+
{ q | groups: q.groups ++ [GroupTerm(e.sql, e.params)] }
|
|
175
219
|
end
|
|
176
220
|
|
|
177
221
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
q.limit_,
|
|
186
|
-
q.offset_,
|
|
187
|
-
q.result,
|
|
188
|
-
)
|
|
222
|
+
# Filters on an aggregate, after `group` has collapsed the rows. `where`
|
|
223
|
+
# cannot: it runs before the grouping, so `count_all` has nothing to count
|
|
224
|
+
# yet.
|
|
225
|
+
#
|
|
226
|
+
# group(p.id) |> having(count_all |> Expr.gt(val(3)))
|
|
227
|
+
def having(q: Query(a), predicate: Expr(Bool)) -> Query(a)
|
|
228
|
+
{ q | havings: q.havings ++ [predicate] }
|
|
189
229
|
end
|
|
190
230
|
|
|
191
231
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
q.wheres,
|
|
197
|
-
q.groups,
|
|
198
|
-
q.orders,
|
|
199
|
-
Just(n),
|
|
200
|
-
q.offset_,
|
|
201
|
-
q.result,
|
|
202
|
-
)
|
|
232
|
+
# Drops duplicate rows from the result. Applies to the whole row the query
|
|
233
|
+
# projects, which is `SELECT DISTINCT` rather than Postgres' `DISTINCT ON`.
|
|
234
|
+
def distinct(q: Query(a)) -> Query(a)
|
|
235
|
+
{ q | distinct_: True }
|
|
203
236
|
end
|
|
204
237
|
|
|
205
238
|
|
|
206
|
-
def
|
|
207
|
-
|
|
208
|
-
q.tables,
|
|
209
|
-
q.joins,
|
|
210
|
-
q.wheres,
|
|
211
|
-
q.groups,
|
|
212
|
-
q.orders,
|
|
213
|
-
q.limit_,
|
|
214
|
-
Just(n),
|
|
215
|
-
q.result,
|
|
216
|
-
)
|
|
239
|
+
def order(q: Query(a), e: Expr(b)) -> Query(a)
|
|
240
|
+
{ q | orders: q.orders ++ [OrderTerm(Asc, e.sql, e.params)] }
|
|
217
241
|
end
|
|
218
242
|
|
|
219
243
|
|
|
220
|
-
def
|
|
221
|
-
|
|
244
|
+
def order_desc(q: Query(a), e: Expr(b)) -> Query(a)
|
|
245
|
+
{ q | orders: q.orders ++ [OrderTerm(Desc, e.sql, e.params)] }
|
|
222
246
|
end
|
|
223
247
|
|
|
224
248
|
|
|
225
|
-
def
|
|
226
|
-
|
|
249
|
+
def limit(q: Query(a), n: Int) -> Query(a)
|
|
250
|
+
{ q | limit_: Just(n) }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def offset(q: Query(a), n: Int) -> Query(a)
|
|
255
|
+
{ q | offset_: Just(n) }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def select(make: a -> b) -> Query(Selector(a -> b))
|
|
260
|
+
Query([], [], [], [], [], [], Nothing, Nothing, False, Selector([], []), Nothing)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def field(qs: Query(Selector(a -> b)), e: Expr(a)) -> Select(b)
|
|
265
|
+
Query(
|
|
227
266
|
qs.tables,
|
|
228
267
|
qs.joins,
|
|
229
268
|
qs.wheres,
|
|
230
269
|
qs.groups,
|
|
270
|
+
qs.havings,
|
|
231
271
|
qs.orders,
|
|
232
272
|
qs.limit_,
|
|
233
273
|
qs.offset_,
|
|
274
|
+
qs.distinct_,
|
|
234
275
|
Selector(qs.result.columns_sql ++ [e.sql], qs.result.params ++ e.params),
|
|
276
|
+
qs.result_alias,
|
|
235
277
|
)
|
|
236
278
|
end
|
|
237
279
|
|
|
238
280
|
|
|
239
|
-
def field_as(qs:
|
|
240
|
-
|
|
281
|
+
def field_as(qs: Query(Selector(a -> b)), e: Expr(a), name: String) -> Select(b)
|
|
282
|
+
Query(
|
|
241
283
|
qs.tables,
|
|
242
284
|
qs.joins,
|
|
243
285
|
qs.wheres,
|
|
244
286
|
qs.groups,
|
|
287
|
+
qs.havings,
|
|
245
288
|
qs.orders,
|
|
246
289
|
qs.limit_,
|
|
247
290
|
qs.offset_,
|
|
291
|
+
qs.distinct_,
|
|
248
292
|
Selector(
|
|
249
293
|
qs.result.columns_sql ++ [e.sql ++ " AS " ++ name],
|
|
250
294
|
qs.result.params ++ e.params,
|
|
251
295
|
),
|
|
296
|
+
qs.result_alias,
|
|
252
297
|
)
|
|
253
298
|
end
|
|
254
299
|
|
|
@@ -274,10 +319,17 @@ def render_order_term(t: OrderTerm) -> String
|
|
|
274
319
|
end
|
|
275
320
|
|
|
276
321
|
|
|
322
|
+
# Every table the query was built from, which is a cross join when there is
|
|
323
|
+
# more than one. Rendering only the first would leave the statement
|
|
324
|
+
# disagreeing with the value: `from` twice puts two tables in `tables`, and
|
|
325
|
+
# accessors for both are in scope.
|
|
277
326
|
def render_from(tables: List(TableRef)) -> String
|
|
278
327
|
case tables
|
|
279
328
|
in [] then ""
|
|
280
|
-
|
|
329
|
+
|
|
330
|
+
in _
|
|
331
|
+
then " FROM "
|
|
332
|
+
++ String.join(List.map(tables, (t) -> { t.name ++ " " ++ t.alias_ }), ", ")
|
|
281
333
|
end
|
|
282
334
|
end
|
|
283
335
|
|
|
@@ -290,13 +342,66 @@ def render_int_clause(keyword: String, m: Maybe(Int)) -> String
|
|
|
290
342
|
end
|
|
291
343
|
|
|
292
344
|
|
|
293
|
-
implements
|
|
294
|
-
|
|
345
|
+
implements ToSql(Query(Selector(a))) with
|
|
346
|
+
to_sql: to_sql
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def to_sql(q: Select(a)) -> (String, List(Value))
|
|
351
|
+
render(q, q.result.columns_sql, q.result.params)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# `EXISTS (SELECT 1 FROM …)`, for asking whether a related row is there
|
|
356
|
+
# without joining to it and without projecting anything from it. The inner
|
|
357
|
+
# query may name the outer query's columns, which is what makes it correlated:
|
|
358
|
+
#
|
|
359
|
+
# p <- from(patients)
|
|
360
|
+
# where(exists(from(visits) |> filter((v) -> { v.patient_id |> Expr.eq(p.id) })))
|
|
361
|
+
def exists(q: Query(a)) -> Expr(Bool)
|
|
362
|
+
wrap_subquery("EXISTS (", q)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def wrap_subquery(keyword: String, q: Query(a)) -> Expr(Bool)
|
|
367
|
+
case render(q, ["1"], [])
|
|
368
|
+
in (sql, params) then Expr(keyword ++ sql ++ ")", params)
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# A subquery in a value position: `(SELECT v.seen_on FROM ... LIMIT 1)`.
|
|
374
|
+
# `Maybe`, because a subquery over no rows is NULL, same as `sum`.
|
|
375
|
+
#
|
|
376
|
+
# The column is picked rather than projected: `Select(a)` says nothing about
|
|
377
|
+
# having exactly one, and a subquery with two is a runtime error.
|
|
378
|
+
def subquery(q: Query(c), pick: c -> Expr(a)) -> Expr(Maybe(a))
|
|
379
|
+
picked = pick(q.result)
|
|
380
|
+
|
|
381
|
+
case render(q, [picked.sql], picked.params)
|
|
382
|
+
in (sql, params) then Expr("(" ++ sql ++ ")", params)
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def in_subquery(e: Expr(a), q: Query(c), pick: c -> Expr(a)) -> Expr(Bool)
|
|
388
|
+
picked = pick(q.result)
|
|
389
|
+
|
|
390
|
+
case render(q, [picked.sql], picked.params)
|
|
391
|
+
in (sql, params) then Expr(e.sql ++ " IN (" ++ sql ++ ")", e.params ++ params)
|
|
392
|
+
end
|
|
295
393
|
end
|
|
296
394
|
|
|
297
395
|
|
|
298
|
-
|
|
299
|
-
|
|
396
|
+
# Every clause but the select list, which the caller supplies: `to_sql` passes
|
|
397
|
+
# the projected columns, a subquery passes `1`.
|
|
398
|
+
def render(
|
|
399
|
+
q: Query(a),
|
|
400
|
+
columns_sql: List(String),
|
|
401
|
+
result_params: List(Value),
|
|
402
|
+
) -> (String, List(Value))
|
|
403
|
+
select_clause = (q.distinct_ ? "SELECT DISTINCT " : "SELECT ")
|
|
404
|
+
++ String.join(columns_sql, ", ")
|
|
300
405
|
from_clause = render_from(q.tables)
|
|
301
406
|
joins_clause = q.joins
|
|
302
407
|
|> List.map((j) -> { " " ++ render_join(j) })
|
|
@@ -307,6 +412,9 @@ def to_sql(q: Q(Selector(a))) -> (String, List(Value))
|
|
|
307
412
|
group_clause = List.empty?(q.groups)
|
|
308
413
|
? ""
|
|
309
414
|
: " GROUP BY " ++ String.join(List.map(q.groups, (g) -> { g.sql }), ", ")
|
|
415
|
+
having_clause = List.empty?(q.havings)
|
|
416
|
+
? ""
|
|
417
|
+
: " HAVING " ++ String.join(List.map(q.havings, (h) -> { h.sql }), " AND ")
|
|
310
418
|
order_clause = List.empty?(q.orders)
|
|
311
419
|
? ""
|
|
312
420
|
: " ORDER BY " ++ String.join(List.map(q.orders, render_order_term), ", ")
|
|
@@ -315,6 +423,7 @@ def to_sql(q: Q(Selector(a))) -> (String, List(Value))
|
|
|
315
423
|
join_ps = List.fold(q.joins, [], (acc, j) -> { acc ++ j.on.params })
|
|
316
424
|
where_ps = List.fold(q.wheres, [], (acc, w) -> { acc ++ w.params })
|
|
317
425
|
group_ps = List.fold(q.groups, [], (acc, g) -> { acc ++ g.params })
|
|
426
|
+
having_ps = List.fold(q.havings, [], (acc, h) -> { acc ++ h.params })
|
|
318
427
|
order_ps = List.fold(q.orders, [], (acc, o) -> { acc ++ o.params })
|
|
319
428
|
|
|
320
429
|
Tuple.Tuple2(
|
|
@@ -323,9 +432,116 @@ def to_sql(q: Q(Selector(a))) -> (String, List(Value))
|
|
|
323
432
|
++ joins_clause
|
|
324
433
|
++ where_clause
|
|
325
434
|
++ group_clause
|
|
435
|
+
++ having_clause
|
|
326
436
|
++ order_clause
|
|
327
437
|
++ limit_clause
|
|
328
438
|
++ offset_clause,
|
|
329
|
-
|
|
439
|
+
result_params ++ join_ps ++ where_ps ++ group_ps ++ having_ps ++ order_ps,
|
|
330
440
|
)
|
|
331
441
|
end
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
# Runs the query and decodes the single row it selected. Errors with
|
|
446
|
+
# `NotFound` on none and `TooManyRows` on more than one, so "exactly one" is
|
|
447
|
+
# checked rather than assumed.
|
|
448
|
+
def fetch_one(q: Select(a)) -> Task(a, e)
|
|
449
|
+
to_sql(q) |> Sql.fetch_one_raw |> Task.map_error(from_sql_error)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# `fetch_one` where no row is an answer rather than an error. More than one
|
|
454
|
+
# is still `TooManyRows`: nothing is dropped to make the type fit.
|
|
455
|
+
def fetch_at_most_one(q: Select(a)) -> Task(Maybe(a), e)
|
|
456
|
+
to_sql(q)
|
|
457
|
+
|> Sql.fetch_one_raw
|
|
458
|
+
|> Task.map(Just)
|
|
459
|
+
|> Task.on_error(nothing_if_missing)
|
|
460
|
+
|> Task.map_error(from_sql_error)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# Before the widening, because whether a missing row is an error is a
|
|
465
|
+
# question about `SqlError` and the caller's type may not be able to say.
|
|
466
|
+
def nothing_if_missing(e: SqlError) -> Task(Maybe(a), SqlError)
|
|
467
|
+
case e
|
|
468
|
+
in NotFound then Task.succeed(Nothing)
|
|
469
|
+
else Task.fail(e)
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# Runs the query and decodes every row it selected. No rows is an empty list,
|
|
475
|
+
# not an error.
|
|
476
|
+
def fetch_many(q: Select(a)) -> Task(List(a), e)
|
|
477
|
+
to_sql(q) |> Sql.fetch_many_raw |> Task.map_error(from_sql_error)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
# A read with nothing to decode renders its own select list over the query's
|
|
482
|
+
# clauses, so it needs no projection and carries no `Selectable`. The port
|
|
483
|
+
# hands back a record either way, which is what these one-field shapes are
|
|
484
|
+
# for.
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
struct CountRow = { tally: Int }
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
struct PresentRow = { present: Bool }
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
struct ValueRow(a) = { value: a }
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def fetch_count(q: Query(a)) -> Task(Int, e)
|
|
497
|
+
render(q, ["COUNT(*) AS tally"], [])
|
|
498
|
+
|> Sql.fetch_one_raw
|
|
499
|
+
|> Task.map(count_of)
|
|
500
|
+
|> Task.map_error(from_sql_error)
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def count_of(r: CountRow) -> Int
|
|
505
|
+
r.tally
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# `SELECT EXISTS (...)`, which stops at the first row Postgres finds rather
|
|
510
|
+
# than counting every one of them. `exists` is the `Expr(Bool)` for a `WHERE`,
|
|
511
|
+
# where the keyword actually appears.
|
|
512
|
+
def fetch_exists(q: Query(a)) -> Task(Bool, e)
|
|
513
|
+
present = exists(q)
|
|
514
|
+
|
|
515
|
+
Tuple.Tuple2("SELECT " ++ present.sql ++ " AS present", present.params)
|
|
516
|
+
|> Sql.fetch_one_raw
|
|
517
|
+
|> Task.map(present_of)
|
|
518
|
+
|> Task.map_error(from_sql_error)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def present_of(r: PresentRow) -> Bool
|
|
523
|
+
r.present
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
# One column out of every row, for the reads whose answer is a list of values
|
|
528
|
+
# rather than a list of rows:
|
|
529
|
+
#
|
|
530
|
+
# from(patients) |> where(p.archived |> eq(False)) |> fetch_values(p.id)
|
|
531
|
+
#
|
|
532
|
+
# Worth it only for a single column. Two would come back as a tuple, and a
|
|
533
|
+
# tuple of two columns of the same type is the projection bug with no field
|
|
534
|
+
# names to catch it — that is what `select |> field` is for.
|
|
535
|
+
def fetch_values(q: Query(c), e: Expr(b)) -> Task(List(b), err)
|
|
536
|
+
render(q, [e.sql ++ " AS value"], e.params)
|
|
537
|
+
|> Sql.fetch_many_raw
|
|
538
|
+
|> Task.map((rows) -> { List.map(rows, value_of) })
|
|
539
|
+
|> Task.map_error(from_sql_error)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def value_of(r: ValueRow(a)) -> a
|
|
544
|
+
r.value
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
|