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.
@@ -1,9 +1,14 @@
1
1
  module Sql.Query exposing (
2
- Q,
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 (Expr, Renderable, Selector(..), Table, TableRef(..))
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
- struct Q(a) = {
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
- result: a
92
+ distinct_: Bool,
93
+ result: a,
94
+ result_alias: Maybe(String)
62
95
  }
63
96
 
64
97
 
65
- implements Chainable(Q(a)) with
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: Q(a), fn: a -> Q(b)) -> Q(b)
103
+ def q_and_then(q: Query(a), fn: a -> Query(b)) -> Query(b)
71
104
  next_ = q.result |> fn
72
105
 
73
- Q(
74
- q.tables ++ next_.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)) -> Q(c)
147
+ def from(t: Table(c, m, k, o, r, s)) -> Query(c)
95
148
  cols_ = t.alias_ |> t.cols
96
149
 
97
- Q([TableRef(t.name, t.alias_)], [], [], [], [], Nothing, Nothing, cols_)
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)) -> Q(c)
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
- Q(
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)) -> Q(m)
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.maybe_cols
188
+ cols_maybe = t.alias_ |> t.left_cols
121
189
  pred = cols_strict |> on_
122
190
 
123
- Q(
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: Q(a), predicate: Expr(Bool)) -> Q(a)
137
- Q(
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 group(q: Q(a), e: Expr(b)) -> Q(a)
151
- Q(
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 order(q: Q(a), e: Expr(b)) -> Q(a)
165
- Q(
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
- def order_desc(q: Q(a), e: Expr(b)) -> Q(a)
179
- Q(
180
- q.tables,
181
- q.joins,
182
- q.wheres,
183
- q.groups,
184
- q.orders ++ [OrderTerm(Desc, e.sql, e.params)],
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
- def limit(q: Q(a), n: Int) -> Q(a)
193
- Q(
194
- q.tables,
195
- q.joins,
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 offset(q: Q(a), n: Int) -> Q(a)
207
- Q(
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 select(make: a -> b) -> Q(Selector(a -> b))
221
- Q([], [], [], [], [], Nothing, Nothing, Selector([], []))
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 field(qs: Q(Selector(a -> b)), e: Expr(a)) -> Q(Selector(b))
226
- Q(
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: Q(Selector(a -> b)), e: Expr(a), name: String) -> Q(Selector(b))
240
- Q(
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
- in [first | _] then " FROM " ++ first.name ++ " " ++ first.alias_
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 Renderable(Q(Selector(a))) with
294
- render: to_sql
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
- def to_sql(q: Q(Selector(a))) -> (String, List(Value))
299
- select_clause = "SELECT " ++ String.join(q.result.columns_sql, ", ")
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
- q.result.params ++ join_ps ++ where_ps ++ group_ps ++ order_ps,
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
+