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.
- checksums.yaml +4 -4
- data/README.md +16 -10
- data/docs/building.md +411 -113
- data/docs/running.md +103 -18
- data/exe/jade-sql +37 -9
- data/lib/jade-sql/bin/generate_schema.rb +702 -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.rb +32 -0
- data/lib/jade-sql/runtime.rb +74 -23
- 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 +309 -93
- data/lib/jade-sql/sql/write.jd +618 -0
- data/lib/jade-sql/sql.jd +389 -103
- data/lib/jade-sql/tasks.rake +41 -8
- data/lib/jade-sql/version.rb +1 -1
- data/lib/jade-sql.rb +92 -14
- metadata +15 -8
- data/lib/jade-sql/sql/mutation.jd +0 -464
|
@@ -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
|