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.
- checksums.yaml +4 -4
- data/README.md +16 -10
- data/docs/building.md +398 -96
- data/docs/running.md +37 -17
- data/exe/jade-sql +37 -9
- data/lib/jade-sql/bin/generate_schema.rb +655 -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/selectable.rb +76 -0
- data/lib/jade-sql/compiler.rb +34 -0
- data/lib/jade-sql/runtime.rb +15 -11
- 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 +284 -92
- data/lib/jade-sql/sql/write.jd +625 -0
- data/lib/jade-sql/sql.jd +328 -90
- data/lib/jade-sql/tasks.rake +41 -8
- data/lib/jade-sql/version.rb +1 -1
- data/lib/jade-sql.rb +8 -7
- metadata +16 -8
- data/lib/jade-sql/sql/mutation.jd +0 -464
data/lib/jade-sql/sql/query.jd
CHANGED
|
@@ -1,21 +1,44 @@
|
|
|
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,
|
|
12
|
+
not_exists,
|
|
7
13
|
join,
|
|
14
|
+
rows,
|
|
8
15
|
left_join,
|
|
9
16
|
limit,
|
|
10
17
|
offset,
|
|
11
18
|
order,
|
|
12
19
|
order_desc,
|
|
13
20
|
select,
|
|
21
|
+
fetch_at_most_one,
|
|
22
|
+
fetch_many,
|
|
23
|
+
fetch_one,
|
|
24
|
+
fetch_row,
|
|
25
|
+
fetch_rows,
|
|
26
|
+
selected,
|
|
14
27
|
to_sql,
|
|
28
|
+
subquery,
|
|
15
29
|
where,
|
|
30
|
+
filter,
|
|
16
31
|
)
|
|
17
32
|
|
|
18
|
-
import Sql exposing (
|
|
33
|
+
import Sql exposing (
|
|
34
|
+
Expr(..),
|
|
35
|
+
Selector(..),
|
|
36
|
+
SqlError(..),
|
|
37
|
+
Table,
|
|
38
|
+
TableRef(..),
|
|
39
|
+
ToSql,
|
|
40
|
+
selector,
|
|
41
|
+
)
|
|
19
42
|
import Decode exposing (Value)
|
|
20
43
|
|
|
21
44
|
|
|
@@ -50,39 +73,69 @@ struct GroupTerm = {
|
|
|
50
73
|
}
|
|
51
74
|
|
|
52
75
|
|
|
53
|
-
|
|
76
|
+
|
|
77
|
+
# A finished query: one you have projected columns into with `select` and
|
|
78
|
+
# `field`, and can hand to `fetch_one` / `fetch_many`. A bare `Query(c)` is a
|
|
79
|
+
# query mid-chain, still carrying a table's column accessors.
|
|
80
|
+
#
|
|
81
|
+
# def scheduled_visits -> Select(Visit)
|
|
82
|
+
type alias Select(a) = Query(Selector(a))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
struct Query(a) = {
|
|
54
86
|
tables: List(TableRef),
|
|
55
87
|
joins: List(Join),
|
|
56
88
|
wheres: List(Expr(Bool)),
|
|
57
89
|
groups: List(GroupTerm),
|
|
90
|
+
havings: List(Expr(Bool)),
|
|
58
91
|
orders: List(OrderTerm),
|
|
59
92
|
limit_: Maybe(Int),
|
|
60
93
|
offset_: Maybe(Int),
|
|
94
|
+
distinct_: Bool,
|
|
61
95
|
result: a
|
|
62
96
|
}
|
|
63
97
|
|
|
64
98
|
|
|
65
|
-
implements Chainable(
|
|
99
|
+
implements Chainable(Query(a)) with
|
|
66
100
|
and_then: q_and_then
|
|
67
101
|
end
|
|
68
102
|
|
|
69
103
|
|
|
70
|
-
def q_and_then(q:
|
|
104
|
+
def q_and_then(q: Query(a), fn: a -> Query(b)) -> Query(b)
|
|
71
105
|
next_ = q.result |> fn
|
|
72
106
|
|
|
73
|
-
|
|
74
|
-
q.tables
|
|
107
|
+
Query(
|
|
108
|
+
merge_tables(q.tables, next_.tables),
|
|
75
109
|
q.joins ++ next_.joins,
|
|
76
110
|
q.wheres ++ next_.wheres,
|
|
77
111
|
q.groups ++ next_.groups,
|
|
112
|
+
q.havings ++ next_.havings,
|
|
78
113
|
q.orders ++ next_.orders,
|
|
79
114
|
merge_paging(q.limit_, next_.limit_),
|
|
80
115
|
merge_paging(q.offset_, next_.offset_),
|
|
116
|
+
q.distinct_ || next_.distinct_,
|
|
81
117
|
next_.result,
|
|
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,160 +144,153 @@ 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([TableRef(t.name, t.alias_)], [], [], [], [], [], Nothing, Nothing, False, cols_)
|
|
98
151
|
end
|
|
99
152
|
|
|
100
153
|
|
|
101
|
-
def join(t: Table(c, m), on_: c -> Expr(Bool)) ->
|
|
154
|
+
def join(t: Table(c, m, k, o, r, s), on_: c -> Expr(Bool)) -> Query(c)
|
|
102
155
|
cols_ = t.alias_ |> t.cols
|
|
103
156
|
pred = cols_ |> on_
|
|
104
157
|
|
|
105
|
-
|
|
158
|
+
Query(
|
|
106
159
|
[],
|
|
107
160
|
[Join(InnerJ, t.name, t.alias_, pred)],
|
|
108
161
|
[],
|
|
109
162
|
[],
|
|
110
163
|
[],
|
|
164
|
+
[],
|
|
111
165
|
Nothing,
|
|
112
166
|
Nothing,
|
|
167
|
+
False,
|
|
113
168
|
cols_,
|
|
114
169
|
)
|
|
115
170
|
end
|
|
116
171
|
|
|
117
172
|
|
|
118
|
-
def left_join(t: Table(c, m), on_: c -> Expr(Bool)) ->
|
|
173
|
+
def left_join(t: Table(c, m, k, o, r, s), on_: c -> Expr(Bool)) -> Query(m)
|
|
119
174
|
cols_strict = t.alias_ |> t.cols
|
|
120
|
-
cols_maybe = t.alias_ |> t.
|
|
175
|
+
cols_maybe = t.alias_ |> t.left_cols
|
|
121
176
|
pred = cols_strict |> on_
|
|
122
177
|
|
|
123
|
-
|
|
178
|
+
Query(
|
|
124
179
|
[],
|
|
125
180
|
[Join(LeftJ, t.name, t.alias_, pred)],
|
|
126
181
|
[],
|
|
127
182
|
[],
|
|
128
183
|
[],
|
|
184
|
+
[],
|
|
129
185
|
Nothing,
|
|
130
186
|
Nothing,
|
|
187
|
+
False,
|
|
131
188
|
cols_maybe,
|
|
132
189
|
)
|
|
133
190
|
end
|
|
134
191
|
|
|
135
192
|
|
|
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
|
-
)
|
|
193
|
+
def where(q: Query(a), predicate: Expr(Bool)) -> Query(a)
|
|
194
|
+
{ q | wheres: q.wheres ++ [predicate] }
|
|
147
195
|
end
|
|
148
196
|
|
|
149
197
|
|
|
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
|
-
)
|
|
198
|
+
def filter(q: Query(a), build: a -> Expr(Bool)) -> Query(a)
|
|
199
|
+
where(q, build(q.result))
|
|
161
200
|
end
|
|
162
201
|
|
|
163
202
|
|
|
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
|
-
)
|
|
203
|
+
def group(q: Query(a), e: Expr(b)) -> Query(a)
|
|
204
|
+
{ q | groups: q.groups ++ [GroupTerm(e.sql, e.params)] }
|
|
175
205
|
end
|
|
176
206
|
|
|
177
207
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
q.limit_,
|
|
186
|
-
q.offset_,
|
|
187
|
-
q.result,
|
|
188
|
-
)
|
|
208
|
+
# Filters on an aggregate, after `group` has collapsed the rows. `where`
|
|
209
|
+
# cannot: it runs before the grouping, so `count_all` has nothing to count
|
|
210
|
+
# yet.
|
|
211
|
+
#
|
|
212
|
+
# group(p.id) |> having(count_all |> Expr.gt(val(3)))
|
|
213
|
+
def having(q: Query(a), predicate: Expr(Bool)) -> Query(a)
|
|
214
|
+
{ q | havings: q.havings ++ [predicate] }
|
|
189
215
|
end
|
|
190
216
|
|
|
191
217
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
q.wheres,
|
|
197
|
-
q.groups,
|
|
198
|
-
q.orders,
|
|
199
|
-
Just(n),
|
|
200
|
-
q.offset_,
|
|
201
|
-
q.result,
|
|
202
|
-
)
|
|
218
|
+
# Drops duplicate rows from the result. Applies to the whole row the query
|
|
219
|
+
# projects, which is `SELECT DISTINCT` rather than Postgres' `DISTINCT ON`.
|
|
220
|
+
def distinct(q: Query(a)) -> Query(a)
|
|
221
|
+
{ q | distinct_: True }
|
|
203
222
|
end
|
|
204
223
|
|
|
205
224
|
|
|
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
|
-
)
|
|
225
|
+
def order(q: Query(a), e: Expr(b)) -> Query(a)
|
|
226
|
+
{ q | orders: q.orders ++ [OrderTerm(Asc, e.sql, e.params)] }
|
|
217
227
|
end
|
|
218
228
|
|
|
219
229
|
|
|
220
|
-
def
|
|
221
|
-
|
|
230
|
+
def order_desc(q: Query(a), e: Expr(b)) -> Query(a)
|
|
231
|
+
{ q | orders: q.orders ++ [OrderTerm(Desc, e.sql, e.params)] }
|
|
222
232
|
end
|
|
223
233
|
|
|
224
234
|
|
|
225
|
-
def
|
|
226
|
-
|
|
235
|
+
def limit(q: Query(a), n: Int) -> Query(a)
|
|
236
|
+
{ q | limit_: Just(n) }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def offset(q: Query(a), n: Int) -> Query(a)
|
|
241
|
+
{ q | offset_: Just(n) }
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def select(make: a -> b) -> Query(Selector(a -> b))
|
|
246
|
+
Query([], [], [], [], [], [], Nothing, Nothing, False, Selector([], []))
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# `select`'s unprojected twin, for a query the caller carries on building
|
|
251
|
+
# rather than projects. It is what lets a subquery be written in a bind chain:
|
|
252
|
+
#
|
|
253
|
+
# v <- from(visits)
|
|
254
|
+
#
|
|
255
|
+
# rows(visits) |> where(...) |> order_desc(v.seen_on) |> limit(1)
|
|
256
|
+
#
|
|
257
|
+
# It names the table rather than the columns, so the query it starts stands on
|
|
258
|
+
# its own. A query that only borrowed columns would render without a `FROM`
|
|
259
|
+
# and read the outer query's table instead, which is legal SQL and a different
|
|
260
|
+
# question. Naming the same table the chain already bound costs nothing: a
|
|
261
|
+
# table is listed once however many times it is named.
|
|
262
|
+
def rows(t: Table(c, m, k, o, r, s)) -> Query(c)
|
|
263
|
+
from(t)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def field(qs: Query(Selector(a -> b)), e: Expr(a)) -> Select(b)
|
|
268
|
+
Query(
|
|
227
269
|
qs.tables,
|
|
228
270
|
qs.joins,
|
|
229
271
|
qs.wheres,
|
|
230
272
|
qs.groups,
|
|
273
|
+
qs.havings,
|
|
231
274
|
qs.orders,
|
|
232
275
|
qs.limit_,
|
|
233
276
|
qs.offset_,
|
|
277
|
+
qs.distinct_,
|
|
234
278
|
Selector(qs.result.columns_sql ++ [e.sql], qs.result.params ++ e.params),
|
|
235
279
|
)
|
|
236
280
|
end
|
|
237
281
|
|
|
238
282
|
|
|
239
|
-
def field_as(qs:
|
|
240
|
-
|
|
283
|
+
def field_as(qs: Query(Selector(a -> b)), e: Expr(a), name: String) -> Select(b)
|
|
284
|
+
Query(
|
|
241
285
|
qs.tables,
|
|
242
286
|
qs.joins,
|
|
243
287
|
qs.wheres,
|
|
244
288
|
qs.groups,
|
|
289
|
+
qs.havings,
|
|
245
290
|
qs.orders,
|
|
246
291
|
qs.limit_,
|
|
247
292
|
qs.offset_,
|
|
293
|
+
qs.distinct_,
|
|
248
294
|
Selector(
|
|
249
295
|
qs.result.columns_sql ++ [e.sql ++ " AS " ++ name],
|
|
250
296
|
qs.result.params ++ e.params,
|
|
@@ -274,10 +320,17 @@ def render_order_term(t: OrderTerm) -> String
|
|
|
274
320
|
end
|
|
275
321
|
|
|
276
322
|
|
|
323
|
+
# Every table the query was built from, which is a cross join when there is
|
|
324
|
+
# more than one. Rendering only the first would leave the statement
|
|
325
|
+
# disagreeing with the value: `from` twice puts two tables in `tables`, and
|
|
326
|
+
# accessors for both are in scope.
|
|
277
327
|
def render_from(tables: List(TableRef)) -> String
|
|
278
328
|
case tables
|
|
279
329
|
in [] then ""
|
|
280
|
-
|
|
330
|
+
|
|
331
|
+
in _
|
|
332
|
+
then " FROM "
|
|
333
|
+
++ String.join(List.map(tables, (t) -> { t.name ++ " " ++ t.alias_ }), ", ")
|
|
281
334
|
end
|
|
282
335
|
end
|
|
283
336
|
|
|
@@ -290,13 +343,71 @@ def render_int_clause(keyword: String, m: Maybe(Int)) -> String
|
|
|
290
343
|
end
|
|
291
344
|
|
|
292
345
|
|
|
293
|
-
implements
|
|
294
|
-
|
|
346
|
+
implements ToSql(Query(Selector(a))) with
|
|
347
|
+
to_sql: to_sql
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def to_sql(q: Select(a)) -> (String, List(Value))
|
|
352
|
+
render(q, q.result.columns_sql, q.result.params)
|
|
295
353
|
end
|
|
296
354
|
|
|
297
355
|
|
|
298
|
-
|
|
299
|
-
|
|
356
|
+
# `EXISTS (SELECT 1 FROM …)`, for asking whether a related row is there
|
|
357
|
+
# without joining to it and without projecting anything from it. The inner
|
|
358
|
+
# query may name the outer query's columns, which is what makes it correlated:
|
|
359
|
+
#
|
|
360
|
+
# p <- from(patients)
|
|
361
|
+
# where(exists(from(visits) |> filter((v) -> { v.patient_id |> Expr.eq(p.id) })))
|
|
362
|
+
def exists(q: Query(a)) -> Expr(Bool)
|
|
363
|
+
wrap_subquery("EXISTS (", q)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def not_exists(q: Query(a)) -> Expr(Bool)
|
|
368
|
+
wrap_subquery("NOT EXISTS (", q)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def wrap_subquery(keyword: String, q: Query(a)) -> Expr(Bool)
|
|
373
|
+
case render(q, ["1"], [])
|
|
374
|
+
in (sql, params) then Expr(keyword ++ sql ++ ")", params)
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# A subquery in a value position: `(SELECT v.seen_on FROM ... LIMIT 1)`.
|
|
380
|
+
# `Maybe`, because a subquery over no rows is NULL, same as `sum`.
|
|
381
|
+
#
|
|
382
|
+
# The column is picked rather than projected: `Select(a)` says nothing about
|
|
383
|
+
# having exactly one, and a subquery with two is a runtime error.
|
|
384
|
+
def subquery(q: Query(c), pick: c -> Expr(a)) -> Expr(Maybe(a))
|
|
385
|
+
picked = pick(q.result)
|
|
386
|
+
|
|
387
|
+
case render(q, [picked.sql], picked.params)
|
|
388
|
+
in (sql, params) then Expr("(" ++ sql ++ ")", params)
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def in_subquery(e: Expr(a), q: Query(c), pick: c -> Expr(a)) -> Expr(Bool)
|
|
394
|
+
picked = pick(q.result)
|
|
395
|
+
|
|
396
|
+
case render(q, [picked.sql], picked.params)
|
|
397
|
+
in (sql, params) then Expr(e.sql ++ " IN (" ++ sql ++ ")", e.params ++ params)
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# Every clause but the select list, which the caller supplies: `to_sql` passes
|
|
403
|
+
# the projected columns, a subquery passes `1`.
|
|
404
|
+
def render(
|
|
405
|
+
q: Query(a),
|
|
406
|
+
columns_sql: List(String),
|
|
407
|
+
result_params: List(Value),
|
|
408
|
+
) -> (String, List(Value))
|
|
409
|
+
select_clause = (q.distinct_ ? "SELECT DISTINCT " : "SELECT ")
|
|
410
|
+
++ String.join(columns_sql, ", ")
|
|
300
411
|
from_clause = render_from(q.tables)
|
|
301
412
|
joins_clause = q.joins
|
|
302
413
|
|> List.map((j) -> { " " ++ render_join(j) })
|
|
@@ -307,6 +418,9 @@ def to_sql(q: Q(Selector(a))) -> (String, List(Value))
|
|
|
307
418
|
group_clause = List.empty?(q.groups)
|
|
308
419
|
? ""
|
|
309
420
|
: " GROUP BY " ++ String.join(List.map(q.groups, (g) -> { g.sql }), ", ")
|
|
421
|
+
having_clause = List.empty?(q.havings)
|
|
422
|
+
? ""
|
|
423
|
+
: " HAVING " ++ String.join(List.map(q.havings, (h) -> { h.sql }), " AND ")
|
|
310
424
|
order_clause = List.empty?(q.orders)
|
|
311
425
|
? ""
|
|
312
426
|
: " ORDER BY " ++ String.join(List.map(q.orders, render_order_term), ", ")
|
|
@@ -315,6 +429,7 @@ def to_sql(q: Q(Selector(a))) -> (String, List(Value))
|
|
|
315
429
|
join_ps = List.fold(q.joins, [], (acc, j) -> { acc ++ j.on.params })
|
|
316
430
|
where_ps = List.fold(q.wheres, [], (acc, w) -> { acc ++ w.params })
|
|
317
431
|
group_ps = List.fold(q.groups, [], (acc, g) -> { acc ++ g.params })
|
|
432
|
+
having_ps = List.fold(q.havings, [], (acc, h) -> { acc ++ h.params })
|
|
318
433
|
order_ps = List.fold(q.orders, [], (acc, o) -> { acc ++ o.params })
|
|
319
434
|
|
|
320
435
|
Tuple.Tuple2(
|
|
@@ -323,9 +438,86 @@ def to_sql(q: Q(Selector(a))) -> (String, List(Value))
|
|
|
323
438
|
++ joins_clause
|
|
324
439
|
++ where_clause
|
|
325
440
|
++ group_clause
|
|
441
|
+
++ having_clause
|
|
326
442
|
++ order_clause
|
|
327
443
|
++ limit_clause
|
|
328
444
|
++ offset_clause,
|
|
329
|
-
|
|
445
|
+
result_params ++ join_ps ++ where_ps ++ group_ps ++ having_ps ++ order_ps,
|
|
446
|
+
)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
# Runs the query and decodes the single row it selected. Errors with
|
|
452
|
+
# `NotFound` on none and `TooManyRows` on more than one, so "exactly one" is
|
|
453
|
+
# checked rather than assumed.
|
|
454
|
+
def fetch_one(q: Select(a)) -> Task(a, SqlError)
|
|
455
|
+
to_sql(q) |> Sql.fetch_one_raw
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
# `fetch_one` where no row is an answer rather than an error. More than one
|
|
460
|
+
# is still `TooManyRows`: nothing is dropped to make the type fit.
|
|
461
|
+
def fetch_at_most_one(q: Select(a)) -> Task(Maybe(a), SqlError)
|
|
462
|
+
q
|
|
463
|
+
|> fetch_one
|
|
464
|
+
|> Task.map(Just)
|
|
465
|
+
|> Task.on_error((e) -> {
|
|
466
|
+
case e
|
|
467
|
+
in NotFound then Task.succeed(Nothing)
|
|
468
|
+
else Task.fail(e)
|
|
469
|
+
end
|
|
470
|
+
})
|
|
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), SqlError)
|
|
477
|
+
to_sql(q) |> Sql.fetch_many_raw
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
# The shape you asked for names the columns, so a read with nothing computed
|
|
482
|
+
# in it needs no `select`. `fetch_one` stays for a query that has one.
|
|
483
|
+
def fetch_row(q: Query(c)) -> Task(a, SqlError)
|
|
484
|
+
q |> selected |> fetch_one
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def fetch_rows(q: Query(c)) -> Task(List(a), SqlError)
|
|
489
|
+
q |> selected |> fetch_many
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def selected(q: Query(c)) -> Select(a)
|
|
494
|
+
Query(
|
|
495
|
+
q.tables,
|
|
496
|
+
q.joins,
|
|
497
|
+
q.wheres,
|
|
498
|
+
q.groups,
|
|
499
|
+
q.havings,
|
|
500
|
+
q.orders,
|
|
501
|
+
q.limit_,
|
|
502
|
+
q.offset_,
|
|
503
|
+
q.distinct_,
|
|
504
|
+
qualified(q.tables, selector),
|
|
330
505
|
)
|
|
331
506
|
end
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# The shape names columns, not tables, so the alias comes from where the read
|
|
510
|
+
# is rooted. Bare names would let Postgres choose: on a join it refuses an
|
|
511
|
+
# ambiguous one, but a name that exists in only one of the joined tables
|
|
512
|
+
# resolves to that one silently, whichever table the shape meant. Qualified,
|
|
513
|
+
# that case is `column patients.note does not exist` instead.
|
|
514
|
+
#
|
|
515
|
+
# A query with no table of its own is left alone; there is no alias to use.
|
|
516
|
+
def qualified(tables: List(TableRef), s: Selector(a)) -> Selector(a)
|
|
517
|
+
case tables
|
|
518
|
+
in [] then s
|
|
519
|
+
|
|
520
|
+
in [t | _]
|
|
521
|
+
then Selector(List.map(s.columns_sql, (c) -> { t.alias_ ++ "." ++ c }), s.params)
|
|
522
|
+
end
|
|
523
|
+
end
|