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,464 +0,0 @@
1
- module Sql.Mutation exposing (
2
- Mutation,
3
- delete,
4
- delete_all,
5
- insert,
6
- insert_all,
7
- returning,
8
- timestamped,
9
- to_sql,
10
- update,
11
- update_all,
12
- update_many,
13
- )
14
-
15
- import Sql exposing (
16
- Assignment(..),
17
- Expr(..),
18
- Renderable,
19
- Selector(..),
20
- Table,
21
- TableRef(..),
22
- pk_values,
23
- set_,
24
- to_assigns,
25
- )
26
- import Sql.Query exposing (Q)
27
- import Decode exposing (Value)
28
- import Encode
29
-
30
-
31
- type MutationKind
32
- = InsertK
33
- | UpdateK
34
- | DeleteK
35
-
36
-
37
- struct Mutation(ret, c) = {
38
- kind: MutationKind,
39
- table: TableRef,
40
- cols: c,
41
- rows: List(List(Assignment)),
42
- wheres: List(Expr(Bool)),
43
- returning_selector: Selector(ret),
44
- from_: Maybe(Expr(Bool))
45
- }
46
-
47
-
48
- def insert(value: a, t: Table(c, m)) -> Mutation(Int, c)
49
- Mutation(
50
- InsertK,
51
- TableRef(t.name, t.alias_),
52
- t.alias_ |> t.cols,
53
- [to_assigns(value)],
54
- [],
55
- Selector([], []),
56
- Nothing,
57
- )
58
- end
59
-
60
-
61
- def insert_all(values: List(a), t: Table(c, m)) -> Mutation(Int, c)
62
- Mutation(
63
- InsertK,
64
- TableRef(t.name, t.alias_),
65
- t.alias_ |> t.cols,
66
- List.map(values, (v) -> { to_assigns(v) }),
67
- [],
68
- Selector([], []),
69
- Nothing,
70
- )
71
- end
72
-
73
-
74
- def update(value: a, t: Table(c, m)) -> Mutation(Int, c)
75
- pk_pred = pk_predicate(t.pk_columns, pk_values(value))
76
-
77
- Mutation(
78
- UpdateK,
79
- TableRef(t.name, t.alias_),
80
- t.alias_ |> t.cols,
81
- [to_assigns(value)],
82
- [pk_pred],
83
- Selector([], []),
84
- Nothing,
85
- )
86
- end
87
-
88
-
89
- def delete(value: a, t: Table(c, m)) -> Mutation(Int, c)
90
- pk_pred = pk_predicate(t.pk_columns, pk_values(value))
91
-
92
- Mutation(
93
- DeleteK,
94
- TableRef(t.name, t.alias_),
95
- t.alias_ |> t.cols,
96
- [],
97
- [pk_pred],
98
- Selector([], []),
99
- Nothing,
100
- )
101
- end
102
-
103
-
104
- def update_all(
105
- t: Table(c, m),
106
- pred_fn: c -> Expr(Bool),
107
- build: c -> List(Assignment),
108
- ) -> Mutation(Int, c)
109
- cols_ = t.alias_ |> t.cols
110
- pred = cols_ |> pred_fn
111
-
112
- Mutation(
113
- UpdateK,
114
- TableRef(t.name, t.alias_),
115
- cols_,
116
- [cols_ |> build],
117
- [pred],
118
- Selector([], []),
119
- Nothing,
120
- )
121
- end
122
-
123
-
124
- def update_many(values: List(a), t: Table(c, m)) -> Mutation(Int, c)
125
- rows = List.zip(
126
- values |> List.map((v) -> { pk_values(v) }),
127
- values |> List.map((v) -> { to_assigns(v) }),
128
- )
129
- |> List.map((p) -> { RowUpdate(Tuple.first(p), Tuple.second(p)) })
130
-
131
- Mutation(
132
- UpdateK,
133
- TableRef(t.name, t.alias_),
134
- t.alias_ |> t.cols,
135
- [source_assigns(rows)],
136
- [pk_join(t.pk_columns)],
137
- Selector([], []),
138
- Just(source_of(t.name, t.pk_columns, rows)),
139
- )
140
- end
141
-
142
-
143
- struct RowUpdate = {
144
- keys: List(Value),
145
- assigns: List(Assignment)
146
- }
147
-
148
-
149
- # One JSON parameter carries every row, so the statement is the same size
150
- # for two rows or two hundred. json_populate_recordset gives the record
151
- # the table's own column types, which is what makes a bare `?` legal in a
152
- # position Postgres cannot otherwise infer.
153
- def source_of(
154
- name: String,
155
- pk_columns: List(String),
156
- rows: List(RowUpdate),
157
- ) -> Expr(Bool)
158
- Expr(
159
- "json_populate_recordset(null::" ++ name ++ ", ?::json) AS jade_src",
160
- [Encode.string(Encode.encode_to_string(json_rows(pk_columns, rows)))],
161
- )
162
- end
163
-
164
-
165
- def json_rows(pk_columns: List(String), rows: List(RowUpdate)) -> Value
166
- Encode.list(json_row(pk_columns, _), rows)
167
- end
168
-
169
-
170
- def json_row(pk_columns: List(String), r: RowUpdate) -> Value
171
- Encode.object(
172
- List.zip(pk_columns, r.keys)
173
- ++ (r.assigns |> List.map((a) -> { Tuple.Tuple2(a.col, first_param(a)) })),
174
- )
175
- end
176
-
177
-
178
- def first_param(a: Assignment) -> Value
179
- a.params |> List.head |> Maybe.with_default(Encode.null)
180
- end
181
-
182
-
183
- def source_assigns(rows: List(RowUpdate)) -> List(Assignment)
184
- rows
185
- |> touched_cols
186
- |> List.map((col) -> { Assignment(col, "jade_src." ++ col, []) })
187
- end
188
-
189
-
190
- def touched_cols(rows: List(RowUpdate)) -> List(String)
191
- rows
192
- |> List.and_then(.assigns)
193
- |> List.map(.col)
194
- |> List.fold([], (seen, c) -> { List.member?(seen, c) ? seen : seen ++ [c] })
195
- end
196
-
197
-
198
- def pk_join(pk_columns: List(String)) -> Expr(Bool)
199
- Expr(
200
- pk_columns
201
- |> List.map((col) -> { "jade_tgt." ++ col ++ " = jade_src." ++ col })
202
- |> String.join(" AND "),
203
- [],
204
- )
205
- end
206
-
207
-
208
- def delete_all(t: Table(c, m), pred_fn: c -> Expr(Bool)) -> Mutation(Int, c)
209
- cols_ = t.alias_ |> t.cols
210
- pred = cols_ |> pred_fn
211
-
212
- Mutation(
213
- DeleteK,
214
- TableRef(t.name, t.alias_),
215
- cols_,
216
- [],
217
- [pred],
218
- Selector([], []),
219
- Nothing,
220
- )
221
- end
222
-
223
-
224
- def returning(m: Mutation(a, c), build: c -> Q(Selector(b))) -> Mutation(b, c)
225
- Mutation(
226
- m.kind,
227
- m.table,
228
- m.cols,
229
- m.rows,
230
- m.wheres,
231
- build(m.cols).result,
232
- m.from_,
233
- )
234
- end
235
-
236
-
237
- # Opt-in ActiveRecord-style timestamps: fills created_at + updated_at on
238
- # insert and updated_at on update, with the app clock at execute time.
239
- # The "$JADE_SQL_NOW$" token carries no param; the runtime swaps it for a
240
- # single UTC timestamp literal per statement (so created_at == updated_at).
241
- # A plain `insert`/`update` without this stays timestamp-free.
242
- def timestamped(m: Mutation(ret, c)) -> Mutation(ret, c)
243
- case m.kind
244
- in InsertK then with_rows(m, List.map(m.rows, append_insert_stamps))
245
- in UpdateK then with_rows(m, List.map(m.rows, append_update_stamps))
246
- in DeleteK then m
247
- end
248
- end
249
-
250
-
251
- def append_insert_stamps(row: List(Assignment)) -> List(Assignment)
252
- row ++ [now_assignment("created_at"), now_assignment("updated_at")]
253
- end
254
-
255
-
256
- def append_update_stamps(row: List(Assignment)) -> List(Assignment)
257
- row ++ [now_assignment("updated_at")]
258
- end
259
-
260
-
261
- def now_assignment(col: String) -> Assignment
262
- Assignment(col, "$JADE_SQL_NOW$", [])
263
- end
264
-
265
-
266
- def with_rows(m: Mutation(ret, c), rows: List(List(Assignment))) -> Mutation(ret, c)
267
- { m | rows: rows }
268
- end
269
-
270
-
271
- def pk_predicate(cols: List(String), values: List(Value)) -> Expr(Bool)
272
- sql = cols
273
- |> List.map((col) -> { col ++ " = ?" })
274
- |> String.join(" AND ")
275
-
276
- Expr(sql, values)
277
- end
278
-
279
-
280
- def render_row(row: List(Assignment)) -> String
281
- cols_part = row
282
- |> List.map((a) -> { a.value_sql })
283
- |> String.join(", ")
284
-
285
- "(" ++ cols_part ++ ")"
286
- end
287
-
288
-
289
- def returning_clause(s: Selector(ret)) -> String
290
- List.empty?(s.columns_sql) ? "" : " RETURNING " ++ String.join(s.columns_sql, ", ")
291
- end
292
-
293
-
294
- def insert_cols_str(rows: List(List(Assignment))) -> String
295
- case rows
296
- in [] then ""
297
- in [first | _]
298
- "(" ++ String.join(List.map(first, (a) -> { a.col }), ", ") ++ ")"
299
- end
300
- end
301
-
302
-
303
- def insert_params(rows: List(List(Assignment))) -> List(Value)
304
- List.fold(
305
- rows,
306
- [],
307
- (acc, row) -> { acc ++ List.fold(row, [], (acc2, a) -> { acc2 ++ a.params }) },
308
- )
309
- end
310
-
311
-
312
- def set_sqls_for(rows: List(List(Assignment))) -> List(String)
313
- case rows
314
- in [first | _] then List.map(first, (a) -> { a.col ++ " = " ++ a.value_sql })
315
- in [] then []
316
- end
317
- end
318
-
319
-
320
- def set_params_for(rows: List(List(Assignment))) -> List(Value)
321
- case rows
322
- in [first | _] then List.fold(first, [], (acc, a) -> { acc ++ a.params })
323
- in [] then []
324
- end
325
- end
326
-
327
-
328
- # A FROM'd UPDATE needs the target aliased: json_populate_recordset gives
329
- # the source every column the table has, so a bare `id = jade_src.id` is
330
- # ambiguous.
331
- def target_ref(m: Mutation(ret, c)) -> String
332
- case m.from_
333
- in Nothing then m.table.name
334
- in Just(_) then m.table.name ++ " AS jade_tgt"
335
- end
336
- end
337
-
338
-
339
- def from_clause(from_: Maybe(Expr(Bool))) -> String
340
- case from_
341
- in Nothing then ""
342
- in Just(e) then " FROM " ++ e.sql
343
- end
344
- end
345
-
346
-
347
- def from_params(from_: Maybe(Expr(Bool))) -> List(Value)
348
- case from_
349
- in Nothing then []
350
- in Just(e) then e.params
351
- end
352
- end
353
-
354
-
355
- def where_sql(wheres: List(Expr(Bool))) -> String
356
- List.empty?(wheres)
357
- ? ""
358
- : " WHERE " ++ String.join(List.map(wheres, (w) -> { w.sql }), " AND ")
359
- end
360
-
361
-
362
- def where_params(wheres: List(Expr(Bool))) -> List(Value)
363
- List.fold(wheres, [], (acc, w) -> { acc ++ w.params })
364
- end
365
-
366
-
367
- def strip_table_alias(sql: String, alias_: String) -> String
368
- String.replace(sql, alias_ ++ ".", "")
369
- end
370
-
371
-
372
- def render_insert(m: Mutation(ret, c)) -> (String, List(Value))
373
- cols_str = insert_cols_str(m.rows)
374
- values_str = m.rows
375
- |> List.map(render_row)
376
- |> String.join(", ")
377
- returning_ = strip_table_alias(returning_clause(m.returning_selector), m.table.alias_)
378
- sql = "INSERT INTO "
379
- ++ m.table.name
380
- ++ " "
381
- ++ cols_str
382
- ++ " VALUES "
383
- ++ values_str
384
- ++ returning_
385
-
386
- Tuple.Tuple2(sql, insert_params(m.rows) ++ m.returning_selector.params)
387
- end
388
-
389
-
390
- def render_update(m: Mutation(ret, c)) -> (String, List(Value))
391
- sets = set_sqls_for(m.rows)
392
-
393
- List.empty?(sets) ? render_no_op_update(m) : render_update_set(m, sets)
394
- end
395
-
396
-
397
- def render_no_op_update(m: Mutation(ret, c)) -> (String, List(Value))
398
- where = strip_table_alias(where_sql(m.wheres), m.table.alias_)
399
- cols = List.empty?(m.returning_selector.columns_sql)
400
- ? "1"
401
- : strip_table_alias(
402
- String.join(m.returning_selector.columns_sql, ", "),
403
- m.table.alias_,
404
- )
405
- sql = "SELECT " ++ cols ++ " FROM " ++ m.table.name ++ where
406
-
407
- Tuple.Tuple2(sql, m.returning_selector.params ++ where_params(m.wheres))
408
- end
409
-
410
-
411
- def render_update_set(
412
- m: Mutation(ret, c),
413
- sets: List(String),
414
- ) -> (String, List(Value))
415
- set_clause = strip_table_alias(
416
- "SET " ++ String.join(sets, ", "),
417
- m.table.alias_,
418
- )
419
- where = strip_table_alias(where_sql(m.wheres), m.table.alias_)
420
- returning_ = strip_table_alias(returning_clause(m.returning_selector), m.table.alias_)
421
- sql = "UPDATE "
422
- ++ target_ref(m)
423
- ++ " "
424
- ++ set_clause
425
- ++ from_clause(m.from_)
426
- ++ where
427
- ++ returning_
428
-
429
- Tuple.Tuple2(
430
- sql,
431
- set_params_for(m.rows)
432
- ++ from_params(m.from_)
433
- ++ where_params(m.wheres)
434
- ++ m.returning_selector.params,
435
- )
436
- end
437
-
438
-
439
-
440
-
441
- def render_delete(m: Mutation(ret, c)) -> (String, List(Value))
442
- where = strip_table_alias(where_sql(m.wheres), m.table.alias_)
443
- returning_ = strip_table_alias(returning_clause(m.returning_selector), m.table.alias_)
444
- sql = "DELETE FROM "
445
- ++ m.table.name
446
- ++ where
447
- ++ returning_
448
-
449
- Tuple.Tuple2(sql, where_params(m.wheres) ++ m.returning_selector.params)
450
- end
451
-
452
-
453
- def to_sql(m: Mutation(ret, c)) -> (String, List(Value))
454
- case m.kind
455
- in InsertK then render_insert(m)
456
- in UpdateK then render_update(m)
457
- in DeleteK then render_delete(m)
458
- end
459
- end
460
-
461
-
462
- implements Renderable(Mutation(ret, c)) with
463
- render: to_sql
464
- end