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
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
module Sql.Write exposing (
|
|
2
|
+
Action(..),
|
|
3
|
+
Conflict(..),
|
|
4
|
+
do_nothing,
|
|
5
|
+
do_update,
|
|
6
|
+
Write,
|
|
7
|
+
on_conflict,
|
|
8
|
+
delete,
|
|
9
|
+
execute,
|
|
10
|
+
fetch_many,
|
|
11
|
+
fetch_one,
|
|
12
|
+
delete_all,
|
|
13
|
+
insert,
|
|
14
|
+
insert_all,
|
|
15
|
+
Stamped,
|
|
16
|
+
returning,
|
|
17
|
+
timestamped,
|
|
18
|
+
to_sql,
|
|
19
|
+
update,
|
|
20
|
+
filter,
|
|
21
|
+
update_all,
|
|
22
|
+
update_many,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
import Sql exposing (
|
|
26
|
+
Assignable,
|
|
27
|
+
Assignment(..),
|
|
28
|
+
Expr(..),
|
|
29
|
+
ToSql,
|
|
30
|
+
Selector(..),
|
|
31
|
+
SqlError,
|
|
32
|
+
Pk,
|
|
33
|
+
Table,
|
|
34
|
+
Unique,
|
|
35
|
+
TableRef(..),
|
|
36
|
+
set,
|
|
37
|
+
to_assigns,
|
|
38
|
+
)
|
|
39
|
+
import Sql.Query exposing (Query)
|
|
40
|
+
import Decode exposing (Value)
|
|
41
|
+
import Encode
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
type WriteKind
|
|
45
|
+
= InsertK
|
|
46
|
+
| UpdateK
|
|
47
|
+
| DeleteK
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
struct Write(ret, c, s) = {
|
|
51
|
+
kind: WriteKind,
|
|
52
|
+
table: TableRef,
|
|
53
|
+
cols: c,
|
|
54
|
+
set_cols: s,
|
|
55
|
+
rows: List(List(Assignment)),
|
|
56
|
+
wheres: List(Expr(Bool)),
|
|
57
|
+
returning_selector: Selector(ret),
|
|
58
|
+
from_: Maybe(Expr(Bool)),
|
|
59
|
+
conflict: Maybe(Conflict)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# `ON CONFLICT (cols) DO NOTHING` or `DO UPDATE SET ...`. The columns are the
|
|
64
|
+
# conflict target, taken from a `Unique`, so the index it names is the one the
|
|
65
|
+
# database will actually use to detect the conflict.
|
|
66
|
+
type Conflict
|
|
67
|
+
= DoNothing(List(String))
|
|
68
|
+
| DoUpdate(List(String), List(Assignment))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# What to do with a row that collides, as a value: `do_nothing`, or
|
|
72
|
+
# `do_update` with what to write instead. Both name the SQL they render.
|
|
73
|
+
type Action(s)
|
|
74
|
+
= DoNothingA
|
|
75
|
+
| DoUpdateA(s -> List(Assignment))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def do_nothing -> Action(s)
|
|
79
|
+
DoNothingA
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# The build receives the table's `SET` columns. Reach for `set_excluded` to
|
|
84
|
+
# take a value from the row that could not be inserted.
|
|
85
|
+
def do_update(build: s -> List(Assignment)) -> Action(s)
|
|
86
|
+
DoUpdateA(build)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def insert(value: a, t: Table(c, m, k, o, r, s)) -> Write(Int, c, s)
|
|
91
|
+
Write(
|
|
92
|
+
InsertK,
|
|
93
|
+
TableRef(t.name, t.alias_),
|
|
94
|
+
t.alias_ |> t.cols,
|
|
95
|
+
t.set_cols,
|
|
96
|
+
[to_assigns(value)],
|
|
97
|
+
[],
|
|
98
|
+
Selector([], []),
|
|
99
|
+
Nothing,
|
|
100
|
+
Nothing,
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def insert_all(values: List(a), t: Table(c, m, k, o, r, s)) -> Write(Int, c, s)
|
|
106
|
+
Write(
|
|
107
|
+
InsertK,
|
|
108
|
+
TableRef(t.name, t.alias_),
|
|
109
|
+
t.alias_ |> t.cols,
|
|
110
|
+
t.set_cols,
|
|
111
|
+
List.map(values, (v) -> { to_assigns(v) }),
|
|
112
|
+
[],
|
|
113
|
+
Selector([], []),
|
|
114
|
+
Nothing,
|
|
115
|
+
Nothing,
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def update(value: a, t: Table(c, m, k, o, r, s), key: k) -> Write(Int, c, s)
|
|
121
|
+
Write(
|
|
122
|
+
UpdateK,
|
|
123
|
+
TableRef(t.name, t.alias_),
|
|
124
|
+
t.alias_ |> t.cols,
|
|
125
|
+
t.set_cols,
|
|
126
|
+
[without_created_at(without_key(t.pk, to_assigns(value)))],
|
|
127
|
+
[key_predicate(t.pk, key)],
|
|
128
|
+
Selector([], []),
|
|
129
|
+
Nothing,
|
|
130
|
+
Nothing,
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def delete(t: Table(c, m, k, o, r, s), key: k) -> Write(Int, c, s)
|
|
136
|
+
Write(
|
|
137
|
+
DeleteK,
|
|
138
|
+
TableRef(t.name, t.alias_),
|
|
139
|
+
t.alias_ |> t.cols,
|
|
140
|
+
t.set_cols,
|
|
141
|
+
[],
|
|
142
|
+
[key_predicate(t.pk, key)],
|
|
143
|
+
Selector([], []),
|
|
144
|
+
Nothing,
|
|
145
|
+
Nothing,
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def filter(m: Write(ret, c, s), build: c -> Expr(Bool)) -> Write(ret, c, s)
|
|
151
|
+
Write(
|
|
152
|
+
m.kind,
|
|
153
|
+
m.table,
|
|
154
|
+
m.cols,
|
|
155
|
+
m.set_cols,
|
|
156
|
+
m.rows,
|
|
157
|
+
m.wheres ++ [m.cols |> build],
|
|
158
|
+
m.returning_selector,
|
|
159
|
+
m.from_,
|
|
160
|
+
m.conflict,
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def update_all(
|
|
166
|
+
t: Table(c, m, k, o, r, s),
|
|
167
|
+
pred_fn: c -> Expr(Bool),
|
|
168
|
+
build: c, s -> List(Assignment),
|
|
169
|
+
) -> Write(Int, c, s)
|
|
170
|
+
cols_ = t.alias_ |> t.cols
|
|
171
|
+
pred = cols_ |> pred_fn
|
|
172
|
+
|
|
173
|
+
Write(
|
|
174
|
+
UpdateK,
|
|
175
|
+
TableRef(t.name, t.alias_),
|
|
176
|
+
cols_,
|
|
177
|
+
t.set_cols,
|
|
178
|
+
[build(cols_, t.set_cols)],
|
|
179
|
+
[pred],
|
|
180
|
+
Selector([], []),
|
|
181
|
+
Nothing,
|
|
182
|
+
Nothing,
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def update_many(rows_: List((k, a)), t: Table(c, m, k, o, r, s)) -> Write(Int, c, s)
|
|
188
|
+
rows = rows_ |> List.map((r) -> { keyed_assigns(t.pk, r) })
|
|
189
|
+
|
|
190
|
+
Write(
|
|
191
|
+
UpdateK,
|
|
192
|
+
TableRef(t.name, t.alias_),
|
|
193
|
+
t.alias_ |> t.cols,
|
|
194
|
+
t.set_cols,
|
|
195
|
+
[source_assigns(t.pk, rows)],
|
|
196
|
+
[pk_join(t.alias_, t.pk.columns)],
|
|
197
|
+
Selector([], []),
|
|
198
|
+
Just(source_of(t.name, rows)),
|
|
199
|
+
Nothing,
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# The key travels with each row so the JSON source carries the columns the
|
|
205
|
+
# join matches on, without the caller's struct having to hold them.
|
|
206
|
+
def keyed_assigns(pk_: Pk(c, k), r: (k, a)) -> List(Assignment)
|
|
207
|
+
(key, value) = r
|
|
208
|
+
|
|
209
|
+
key_assigns(pk_, key) ++ without_created_at(without_key(pk_, to_assigns(value)))
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def key_assigns(pk_: Pk(c, k), key: k) -> List(Assignment)
|
|
214
|
+
List.zip(pk_.columns, key |> pk_.values)
|
|
215
|
+
|> List.map((p) -> { Assignment(Tuple.first(p), "?", [Tuple.second(p)]) })
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# One JSON parameter carries every row, so the statement is the same size
|
|
220
|
+
# for two rows or two hundred. json_populate_recordset gives the record
|
|
221
|
+
# the table's own column types, which is what makes a bare `?` legal in a
|
|
222
|
+
# position Postgres cannot otherwise infer.
|
|
223
|
+
def source_of(name: String, rows: List(List(Assignment))) -> Expr(Bool)
|
|
224
|
+
Expr(
|
|
225
|
+
"json_populate_recordset(null::" ++ name ++ ", ?::json) AS jade_src",
|
|
226
|
+
[Encode.string(Encode.encode_to_string(json_rows(rows)))],
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def json_rows(rows: List(List(Assignment))) -> Value
|
|
232
|
+
Encode.list(json_row(_), rows)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def json_row(row: List(Assignment)) -> Value
|
|
237
|
+
Encode.object(row |> List.map((a) -> { Tuple.Tuple2(a.col, first_param(a)) }))
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def first_param(a: Assignment) -> Value
|
|
242
|
+
a.params |> List.head |> Maybe.with_default(Encode.null)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def source_assigns(pk_: Pk(c, k), rows: List(List(Assignment))) -> List(Assignment)
|
|
247
|
+
rows
|
|
248
|
+
|> touched_cols
|
|
249
|
+
|> List.filter((col) -> { List.member?(pk_.columns, col) |> Basics.not })
|
|
250
|
+
|> List.map((col) -> { Assignment(col, "jade_src." ++ col, []) })
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def touched_cols(rows: List(List(Assignment))) -> List(String)
|
|
255
|
+
rows
|
|
256
|
+
|> List.concat
|
|
257
|
+
|> List.map(.col)
|
|
258
|
+
|> List.fold([], (seen, c) -> { List.member?(seen, c) ? seen : seen ++ [c] })
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def pk_join(alias_: String, pk_columns: List(String)) -> Expr(Bool)
|
|
263
|
+
Expr(
|
|
264
|
+
pk_columns
|
|
265
|
+
|> List.map((col) -> { alias_ ++ "." ++ col ++ " = jade_src." ++ col })
|
|
266
|
+
|> String.join(" AND "),
|
|
267
|
+
[],
|
|
268
|
+
)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def delete_all(t: Table(c, m, k, o, r, s), pred_fn: c -> Expr(Bool)) -> Write(Int, c, s)
|
|
273
|
+
cols_ = t.alias_ |> t.cols
|
|
274
|
+
pred = cols_ |> pred_fn
|
|
275
|
+
|
|
276
|
+
Write(
|
|
277
|
+
DeleteK,
|
|
278
|
+
TableRef(t.name, t.alias_),
|
|
279
|
+
cols_,
|
|
280
|
+
t.set_cols,
|
|
281
|
+
[],
|
|
282
|
+
[pred],
|
|
283
|
+
Selector([], []),
|
|
284
|
+
Nothing,
|
|
285
|
+
Nothing,
|
|
286
|
+
)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# `ON CONFLICT (email) DO NOTHING`, or `... DO UPDATE SET ...`. The target
|
|
291
|
+
# comes from a `Unique`, so the index named is one the database has, and the
|
|
292
|
+
# action says which of the two it is:
|
|
293
|
+
#
|
|
294
|
+
# insert(row, users) |> on_conflict(users_email_key, do_nothing)
|
|
295
|
+
# insert(row, users) |> on_conflict(users_email_key, do_update((s) -> { ... }))
|
|
296
|
+
def on_conflict(m: Write(ret, c, s), u: Unique(c, uk), action: Action(s)) -> Write(ret, c, s)
|
|
297
|
+
case action
|
|
298
|
+
in DoNothingA then { m | conflict: Just(DoNothing(u.columns)) }
|
|
299
|
+
|
|
300
|
+
in DoUpdateA(build)
|
|
301
|
+
then { m | conflict: Just(DoUpdate(u.columns, build(m.set_cols))) }
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def returning(m: Write(a, c, s), build: c -> Query(Selector(b))) -> Write(b, c, s)
|
|
307
|
+
Write(
|
|
308
|
+
m.kind,
|
|
309
|
+
m.table,
|
|
310
|
+
m.cols,
|
|
311
|
+
m.set_cols,
|
|
312
|
+
m.rows,
|
|
313
|
+
m.wheres,
|
|
314
|
+
build(m.cols).result,
|
|
315
|
+
m.from_,
|
|
316
|
+
m.conflict,
|
|
317
|
+
)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# A value plus `created_at` and `updated_at`, filled with the app clock at
|
|
322
|
+
# execute time. A table declaring those columns NOT NULL requires them of the
|
|
323
|
+
# value being written, and this is what supplies them:
|
|
324
|
+
#
|
|
325
|
+
# insert(NewPatient("Ada") |> timestamped, patients)
|
|
326
|
+
# update(Patch(name) |> timestamped, patients, id)
|
|
327
|
+
#
|
|
328
|
+
# `update` writes only `updated_at`, since the row already has the other. The
|
|
329
|
+
# struct being wrapped keeps its own `Assignable`; the stamps are appended to
|
|
330
|
+
# whatever it writes.
|
|
331
|
+
struct Stamped(a) = { value: a }
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def timestamped(value: a) -> Stamped(a)
|
|
335
|
+
Stamped(value)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
implements Assignable(Stamped(a)) with
|
|
340
|
+
to_assigns: (s) -> {
|
|
341
|
+
to_assigns(s.value) ++ [now_assignment("created_at"), now_assignment("updated_at")]
|
|
342
|
+
},
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def now_assignment(col: String) -> Assignment
|
|
347
|
+
Assignment(col, "$JADE_SQL_NOW$", [])
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# Only the one `timestamped` added: it writes the clock token rather than a
|
|
352
|
+
# parameter, so a `created_at` the caller wrote by hand still reaches the row.
|
|
353
|
+
def without_created_at(assigns: List(Assignment)) -> List(Assignment)
|
|
354
|
+
assigns |> List.filter((a) -> { Basics.not(added_created_at?(a)) })
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def added_created_at?(a: Assignment) -> Bool
|
|
359
|
+
a.col == "created_at" && a.value_sql == "$JADE_SQL_NOW$"
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def without_key(pk_: Pk(c, k), assigns: List(Assignment)) -> List(Assignment)
|
|
364
|
+
assigns |> List.filter((a) -> { List.member?(pk_.columns, a.col) |> Basics.not })
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# The `WHERE` that selects one row by its key: `id = ?`, or
|
|
369
|
+
# `user_id = ? AND group_id = ?` for a composite key. Columns and values both
|
|
370
|
+
# come from the table's `Pk`, in the order `structure.sql` declares them, so a
|
|
371
|
+
# caller supplies a `k` and never a column name or an ordering.
|
|
372
|
+
def key_predicate(pk_: Pk(c, k), key: k) -> Expr(Bool)
|
|
373
|
+
sql = pk_.columns
|
|
374
|
+
|> List.map((col) -> { col ++ " = ?" })
|
|
375
|
+
|> String.join(" AND ")
|
|
376
|
+
|
|
377
|
+
Expr(sql, key |> pk_.values)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def render_row(row: List(Assignment)) -> String
|
|
382
|
+
cols_part = row
|
|
383
|
+
|> List.map((a) -> { a.value_sql })
|
|
384
|
+
|> String.join(", ")
|
|
385
|
+
|
|
386
|
+
"(" ++ cols_part ++ ")"
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def returning_clause(s: Selector(ret)) -> String
|
|
391
|
+
List.empty?(s.columns_sql) ? "" : " RETURNING " ++ String.join(s.columns_sql, ", ")
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def insert_cols_str(rows: List(List(Assignment))) -> String
|
|
396
|
+
case rows
|
|
397
|
+
in [] then ""
|
|
398
|
+
in [first | _]
|
|
399
|
+
"(" ++ String.join(List.map(first, (a) -> { a.col }), ", ") ++ ")"
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def insert_params(rows: List(List(Assignment))) -> List(Value)
|
|
405
|
+
List.fold(
|
|
406
|
+
rows,
|
|
407
|
+
[],
|
|
408
|
+
(acc, row) -> { acc ++ List.fold(row, [], (acc2, a) -> { acc2 ++ a.params }) },
|
|
409
|
+
)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def set_sqls_for(rows: List(List(Assignment))) -> List(String)
|
|
414
|
+
case rows
|
|
415
|
+
in [first | _] then List.map(first, (a) -> { a.col ++ " = " ++ a.value_sql })
|
|
416
|
+
in [] then []
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def set_params_for(rows: List(List(Assignment))) -> List(Value)
|
|
422
|
+
case rows
|
|
423
|
+
in [first | _] then List.fold(first, [], (acc, a) -> { acc ++ a.params })
|
|
424
|
+
in [] then []
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# The target carries the same alias the column accessors were built with, so
|
|
430
|
+
# a predicate reads the same in a write as it does in a query and nothing has
|
|
431
|
+
# to be rewritten after the fact. It is also what keeps `id = jade_src.id`
|
|
432
|
+
# unambiguous under a FROM, since json_populate_recordset gives the source
|
|
433
|
+
# every column the table has.
|
|
434
|
+
def target(m: Write(ret, c, s)) -> String
|
|
435
|
+
m.table.alias_ == m.table.name
|
|
436
|
+
? m.table.name
|
|
437
|
+
: m.table.name ++ " AS " ++ m.table.alias_
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def from_clause(from_: Maybe(Expr(Bool))) -> String
|
|
442
|
+
case from_
|
|
443
|
+
in Nothing then ""
|
|
444
|
+
in Just(e) then " FROM " ++ e.sql
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def from_params(from_: Maybe(Expr(Bool))) -> List(Value)
|
|
450
|
+
case from_
|
|
451
|
+
in Nothing then []
|
|
452
|
+
in Just(e) then e.params
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def where_sql(wheres: List(Expr(Bool))) -> String
|
|
458
|
+
List.empty?(wheres)
|
|
459
|
+
? ""
|
|
460
|
+
: " WHERE " ++ String.join(List.map(wheres, (w) -> { w.sql }), " AND ")
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def where_params(wheres: List(Expr(Bool))) -> List(Value)
|
|
465
|
+
List.fold(wheres, [], (acc, w) -> { acc ++ w.params })
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def render_insert(m: Write(ret, c, s)) -> (String, List(Value))
|
|
470
|
+
List.empty?(m.rows) ? render_nothing : render_insert_rows(m)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def render_insert_rows(m: Write(ret, c, s)) -> (String, List(Value))
|
|
475
|
+
sql = "INSERT INTO "
|
|
476
|
+
++ target(m)
|
|
477
|
+
++ " "
|
|
478
|
+
++ insert_body(m.rows)
|
|
479
|
+
++ conflict_clause(m.conflict)
|
|
480
|
+
++ returning_clause(m.returning_selector)
|
|
481
|
+
|
|
482
|
+
Tuple.Tuple2(
|
|
483
|
+
sql,
|
|
484
|
+
insert_params(m.rows) ++ conflict_params(m.conflict) ++ m.returning_selector.params,
|
|
485
|
+
)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def conflict_clause(c: Maybe(Conflict)) -> String
|
|
490
|
+
case c
|
|
491
|
+
in Nothing then ""
|
|
492
|
+
|
|
493
|
+
in Just(DoNothing(cols)) then
|
|
494
|
+
" ON CONFLICT (" ++ String.join(cols, ", ") ++ ") DO NOTHING"
|
|
495
|
+
|
|
496
|
+
in Just(DoUpdate(cols, assigns)) then
|
|
497
|
+
" ON CONFLICT ("
|
|
498
|
+
++ String.join(cols, ", ")
|
|
499
|
+
++ ") DO UPDATE SET "
|
|
500
|
+
++ String.join(List.map(assigns, (a) -> { a.col ++ " = " ++ a.value_sql }), ", ")
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def conflict_params(c: Maybe(Conflict)) -> List(Value)
|
|
506
|
+
case c
|
|
507
|
+
in Just(DoUpdate(_, assigns)) then
|
|
508
|
+
List.fold(assigns, [], (acc, a) -> { acc ++ a.params })
|
|
509
|
+
|
|
510
|
+
else []
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
# `DEFAULT VALUES` for the one row that names no columns: that is a row of
|
|
516
|
+
# defaults, which is what was asked for, and `() VALUES ()` is not SQL. Not
|
|
517
|
+
# the same as having no rows at all, which writes nothing.
|
|
518
|
+
def insert_body(rows: List(List(Assignment))) -> String
|
|
519
|
+
case rows
|
|
520
|
+
in [[]] then "DEFAULT VALUES"
|
|
521
|
+
|
|
522
|
+
else insert_cols_str(rows)
|
|
523
|
+
++ " VALUES "
|
|
524
|
+
++ String.join(List.map(rows, render_row), ", ")
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def render_update(m: Write(ret, c, s)) -> (String, List(Value))
|
|
530
|
+
sets = set_sqls_for(m.rows)
|
|
531
|
+
|
|
532
|
+
List.empty?(sets) ? render_nothing : render_update_set(m, sets)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
# A write with nothing to write. A changeset whose fields all held their old
|
|
537
|
+
# values, and an empty batch, are both ordinary — so this affects no rows and
|
|
538
|
+
# reports so, rather than failing.
|
|
539
|
+
#
|
|
540
|
+
# It has to be a statement, because `to_sql` owes its caller one. `WHERE FALSE`
|
|
541
|
+
# is the whole point: the previous no-op update kept the caller's predicate and
|
|
542
|
+
# rendered `SELECT ... WHERE id = ?`, which `exec_update` counts as a row
|
|
543
|
+
# updated. It matched, so it reported 1, took no lock, and a `returning` read
|
|
544
|
+
# handed back the row as it stood before the update that never happened.
|
|
545
|
+
#
|
|
546
|
+
# The predicate's parameters go with it. A statement that does not bind them
|
|
547
|
+
# must not carry them.
|
|
548
|
+
def render_nothing -> (String, List(Value))
|
|
549
|
+
Tuple.Tuple2("SELECT 1 WHERE FALSE", [])
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def render_update_set(
|
|
554
|
+
m: Write(ret, c, s),
|
|
555
|
+
sets: List(String),
|
|
556
|
+
) -> (String, List(Value))
|
|
557
|
+
set_clause = "SET " ++ String.join(sets, ", ")
|
|
558
|
+
where = where_sql(m.wheres)
|
|
559
|
+
returning_ = returning_clause(m.returning_selector)
|
|
560
|
+
sql = "UPDATE "
|
|
561
|
+
++ target(m)
|
|
562
|
+
++ " "
|
|
563
|
+
++ set_clause
|
|
564
|
+
++ from_clause(m.from_)
|
|
565
|
+
++ where
|
|
566
|
+
++ returning_
|
|
567
|
+
|
|
568
|
+
Tuple.Tuple2(
|
|
569
|
+
sql,
|
|
570
|
+
set_params_for(m.rows)
|
|
571
|
+
++ from_params(m.from_)
|
|
572
|
+
++ where_params(m.wheres)
|
|
573
|
+
++ m.returning_selector.params,
|
|
574
|
+
)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def render_delete(m: Write(ret, c, s)) -> (String, List(Value))
|
|
581
|
+
where = where_sql(m.wheres)
|
|
582
|
+
returning_ = returning_clause(m.returning_selector)
|
|
583
|
+
sql = "DELETE FROM "
|
|
584
|
+
++ target(m)
|
|
585
|
+
++ where
|
|
586
|
+
++ returning_
|
|
587
|
+
|
|
588
|
+
Tuple.Tuple2(sql, where_params(m.wheres) ++ m.returning_selector.params)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def to_sql(m: Write(ret, c, s)) -> (String, List(Value))
|
|
593
|
+
case m.kind
|
|
594
|
+
in InsertK then render_insert(m)
|
|
595
|
+
in UpdateK then render_update(m)
|
|
596
|
+
in DeleteK then render_delete(m)
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
implements ToSql(Write(ret, c, s)) with
|
|
602
|
+
to_sql: to_sql
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
# The number of rows the statement affected. Any `RETURNING` clause is
|
|
607
|
+
# rendered and discarded.
|
|
608
|
+
def execute(m: Write(ret, c, s)) -> Task(Int, SqlError)
|
|
609
|
+
to_sql(m) |> Sql.execute_raw
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
# Decodes the single row a `returning` write gives back — the id of an
|
|
614
|
+
# insert, the new state of an update. A write with no `returning` selects
|
|
615
|
+
# nothing, so this errors with `NotFound`.
|
|
616
|
+
def fetch_one(m: Write(ret, c, s)) -> Task(ret, SqlError)
|
|
617
|
+
to_sql(m) |> Sql.fetch_one_raw
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
# The same for a write that returns many rows, as `insert_all` and
|
|
622
|
+
# `update_all` do.
|
|
623
|
+
def fetch_many(m: Write(ret, c, s)) -> Task(List(ret), SqlError)
|
|
624
|
+
to_sql(m) |> Sql.fetch_many_raw
|
|
625
|
+
end
|