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.
@@ -1,21 +1,44 @@
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,
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 (Expr, Renderable, Selector(..), Table, TableRef(..))
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
- struct Q(a) = {
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(Q(a)) with
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: Q(a), fn: a -> Q(b)) -> Q(b)
104
+ def q_and_then(q: Query(a), fn: a -> Query(b)) -> Query(b)
71
105
  next_ = q.result |> fn
72
106
 
73
- Q(
74
- q.tables ++ next_.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)) -> 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([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)) -> Q(c)
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
- Q(
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)) -> Q(m)
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.maybe_cols
175
+ cols_maybe = t.alias_ |> t.left_cols
121
176
  pred = cols_strict |> on_
122
177
 
123
- Q(
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: 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
- )
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 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
- )
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 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
- )
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
- 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
- )
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
- 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
- )
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 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
- )
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 select(make: a -> b) -> Q(Selector(a -> b))
221
- Q([], [], [], [], [], Nothing, Nothing, Selector([], []))
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 field(qs: Q(Selector(a -> b)), e: Expr(a)) -> Q(Selector(b))
226
- Q(
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: Q(Selector(a -> b)), e: Expr(a), name: String) -> Q(Selector(b))
240
- Q(
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
- in [first | _] then " FROM " ++ first.name ++ " " ++ first.alias_
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 Renderable(Q(Selector(a))) with
294
- render: to_sql
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
- def to_sql(q: Q(Selector(a))) -> (String, List(Value))
299
- select_clause = "SELECT " ++ String.join(q.result.columns_sql, ", ")
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
- q.result.params ++ join_ps ++ where_ps ++ group_ps ++ order_ps,
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