jade-sql 0.6.0 → 0.7.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/lib/jade-sql/runtime.rb +11 -1
- data/lib/jade-sql/sql/mutation.jd +141 -6
- data/lib/jade-sql/sql.jd +12 -0
- data/lib/jade-sql/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 82ae87a5571e7ef32804036dd347134fc11bcf20a66ebabe7562ac898f943262
|
|
4
|
+
data.tar.gz: c81268fa4accc4f8bb8f945a8ba164a64ac0ca629d554c9d99874ecb4fb697d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c1cbadd648ec547bcd888c79504d40b630b7e808f0ef0b1e0f379d8b581816d1c3110112fbed3b597dae9987270b13082e68b0561a6ea5bfec2637e7fc81bc4d
|
|
7
|
+
data.tar.gz: 2864083ceebdd23042ddcd31a3c81d2446b1533b5a7ba8614565138899de6d8fea76bb6a485a8781d16315174cf1856f989890664157bb371ab56cfae2a4e63f
|
data/lib/jade-sql/runtime.rb
CHANGED
|
@@ -81,8 +81,18 @@ module JadeSql
|
|
|
81
81
|
# numeric/decimal columns come back as ::BigDecimal; the schema generator
|
|
82
82
|
# maps them to jade's stdlib Decimal, whose decoder reads the exact
|
|
83
83
|
# "<coefficient>e<exponent>" wire form. Float would lose precision, so don't.
|
|
84
|
+
# Most rows have nothing to convert — Integers, plain Strings and nils
|
|
85
|
+
# all come back as themselves — so the copy only happens once a value
|
|
86
|
+
# actually changes, and each value is still only looked at once.
|
|
84
87
|
def self.coerce_row(row)
|
|
85
|
-
|
|
88
|
+
out = nil
|
|
89
|
+
|
|
90
|
+
row.each_pair do |k, v|
|
|
91
|
+
coerced = coerce_value(v)
|
|
92
|
+
(out ||= row.dup)[k] = coerced unless coerced.equal?(v)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
out || row
|
|
86
96
|
end
|
|
87
97
|
|
|
88
98
|
def self.coerce_value(v)
|
|
@@ -9,6 +9,7 @@ module Sql.Mutation exposing (
|
|
|
9
9
|
to_sql,
|
|
10
10
|
update,
|
|
11
11
|
update_all,
|
|
12
|
+
update_many,
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
import Sql exposing (
|
|
@@ -24,6 +25,7 @@ import Sql exposing (
|
|
|
24
25
|
)
|
|
25
26
|
import Sql.Query exposing (Q)
|
|
26
27
|
import Decode exposing (Value)
|
|
28
|
+
import Encode
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
type MutationKind
|
|
@@ -38,7 +40,8 @@ struct Mutation(ret, c) = {
|
|
|
38
40
|
cols: c,
|
|
39
41
|
rows: List(List(Assignment)),
|
|
40
42
|
wheres: List(Expr(Bool)),
|
|
41
|
-
returning_selector: Selector(ret)
|
|
43
|
+
returning_selector: Selector(ret),
|
|
44
|
+
from_: Maybe(Expr(Bool))
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
|
|
@@ -50,6 +53,7 @@ def insert(value: a, t: Table(c, m)) -> Mutation(Int, c)
|
|
|
50
53
|
[to_assigns(value)],
|
|
51
54
|
[],
|
|
52
55
|
Selector([], []),
|
|
56
|
+
Nothing,
|
|
53
57
|
)
|
|
54
58
|
end
|
|
55
59
|
|
|
@@ -62,6 +66,7 @@ def insert_all(values: List(a), t: Table(c, m)) -> Mutation(Int, c)
|
|
|
62
66
|
List.map(values, (v) -> { to_assigns(v) }),
|
|
63
67
|
[],
|
|
64
68
|
Selector([], []),
|
|
69
|
+
Nothing,
|
|
65
70
|
)
|
|
66
71
|
end
|
|
67
72
|
|
|
@@ -76,6 +81,7 @@ def update(value: a, t: Table(c, m)) -> Mutation(Int, c)
|
|
|
76
81
|
[to_assigns(value)],
|
|
77
82
|
[pk_pred],
|
|
78
83
|
Selector([], []),
|
|
84
|
+
Nothing,
|
|
79
85
|
)
|
|
80
86
|
end
|
|
81
87
|
|
|
@@ -90,6 +96,7 @@ def delete(value: a, t: Table(c, m)) -> Mutation(Int, c)
|
|
|
90
96
|
[],
|
|
91
97
|
[pk_pred],
|
|
92
98
|
Selector([], []),
|
|
99
|
+
Nothing,
|
|
93
100
|
)
|
|
94
101
|
end
|
|
95
102
|
|
|
@@ -109,6 +116,91 @@ def update_all(
|
|
|
109
116
|
[cols_ |> build],
|
|
110
117
|
[pred],
|
|
111
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
|
+
[],
|
|
112
204
|
)
|
|
113
205
|
end
|
|
114
206
|
|
|
@@ -124,12 +216,21 @@ def delete_all(t: Table(c, m), pred_fn: c -> Expr(Bool)) -> Mutation(Int, c)
|
|
|
124
216
|
[],
|
|
125
217
|
[pred],
|
|
126
218
|
Selector([], []),
|
|
219
|
+
Nothing,
|
|
127
220
|
)
|
|
128
221
|
end
|
|
129
222
|
|
|
130
223
|
|
|
131
224
|
def returning(m: Mutation(a, c), build: c -> Q(Selector(b))) -> Mutation(b, c)
|
|
132
|
-
Mutation(
|
|
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
|
+
)
|
|
133
234
|
end
|
|
134
235
|
|
|
135
236
|
|
|
@@ -163,7 +264,7 @@ end
|
|
|
163
264
|
|
|
164
265
|
|
|
165
266
|
def with_rows(m: Mutation(ret, c), rows: List(List(Assignment))) -> Mutation(ret, c)
|
|
166
|
-
|
|
267
|
+
{ m | rows: rows }
|
|
167
268
|
end
|
|
168
269
|
|
|
169
270
|
|
|
@@ -224,6 +325,33 @@ def set_params_for(rows: List(List(Assignment))) -> List(Value)
|
|
|
224
325
|
end
|
|
225
326
|
|
|
226
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
|
+
|
|
227
355
|
def where_sql(wheres: List(Expr(Bool))) -> String
|
|
228
356
|
List.empty?(wheres)
|
|
229
357
|
? ""
|
|
@@ -284,19 +412,26 @@ def render_update_set(
|
|
|
284
412
|
m: Mutation(ret, c),
|
|
285
413
|
sets: List(String),
|
|
286
414
|
) -> (String, List(Value))
|
|
287
|
-
set_clause =
|
|
415
|
+
set_clause = strip_table_alias(
|
|
416
|
+
"SET " ++ String.join(sets, ", "),
|
|
417
|
+
m.table.alias_,
|
|
418
|
+
)
|
|
288
419
|
where = strip_table_alias(where_sql(m.wheres), m.table.alias_)
|
|
289
420
|
returning_ = strip_table_alias(returning_clause(m.returning_selector), m.table.alias_)
|
|
290
421
|
sql = "UPDATE "
|
|
291
|
-
++ m
|
|
422
|
+
++ target_ref(m)
|
|
292
423
|
++ " "
|
|
293
424
|
++ set_clause
|
|
425
|
+
++ from_clause(m.from_)
|
|
294
426
|
++ where
|
|
295
427
|
++ returning_
|
|
296
428
|
|
|
297
429
|
Tuple.Tuple2(
|
|
298
430
|
sql,
|
|
299
|
-
set_params_for(m.rows)
|
|
431
|
+
set_params_for(m.rows)
|
|
432
|
+
++ from_params(m.from_)
|
|
433
|
+
++ where_params(m.wheres)
|
|
434
|
+
++ m.returning_selector.params,
|
|
300
435
|
)
|
|
301
436
|
end
|
|
302
437
|
|
data/lib/jade-sql/sql.jd
CHANGED
|
@@ -44,6 +44,7 @@ module Sql exposing (
|
|
|
44
44
|
maybe_columns,
|
|
45
45
|
neg,
|
|
46
46
|
now,
|
|
47
|
+
or,
|
|
47
48
|
nullable,
|
|
48
49
|
pk_values,
|
|
49
50
|
render,
|
|
@@ -202,6 +203,17 @@ def and(left: Expr(Bool), right: Expr(Bool)) -> Expr(Bool)
|
|
|
202
203
|
end
|
|
203
204
|
|
|
204
205
|
|
|
206
|
+
# Parenthesised where `and` is not, because `where` joins its predicates with
|
|
207
|
+
# AND: without them `x = 1 AND a OR b` binds as `(x = 1 AND a) OR b`, which is
|
|
208
|
+
# not what anyone writing `or` meant.
|
|
209
|
+
def or(left: Expr(Bool), right: Expr(Bool)) -> Expr(Bool)
|
|
210
|
+
Expr(
|
|
211
|
+
"(" ++ left.sql ++ " OR " ++ right.sql ++ ")",
|
|
212
|
+
left.params ++ right.params,
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
|
|
205
217
|
# SUM may return NULL on an empty group, so the result is Expr(Maybe(Int)).
|
|
206
218
|
# Wrap with `coalesce(sum(x), to_expr(0))` for a strict total.
|
|
207
219
|
def sum(e: Expr(Int)) -> Expr(Maybe(Int))
|
data/lib/jade-sql/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jade-sql
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- agustin
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-08-
|
|
10
|
+
date: 2026-08-14 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: jade-lang
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.6.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.6.0
|
|
26
26
|
description: Query and mutation builders, schema generation from db/structure.sql,
|
|
27
27
|
and an ActiveRecord-backed runtime for the Jade language. Renders typed queries
|
|
28
28
|
to (String, List(Value)) and decodes rows into Jade structs.
|