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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91ccd6b6039a017fd7233f251784fd6275d0b889ad6dbf4a0254e8de8bab4037
4
- data.tar.gz: 075edb4238edd0cd475b0d4814043801691a0a6d30ecc06486893c456c6fa7b4
3
+ metadata.gz: 82ae87a5571e7ef32804036dd347134fc11bcf20a66ebabe7562ac898f943262
4
+ data.tar.gz: c81268fa4accc4f8bb8f945a8ba164a64ac0ca629d554c9d99874ecb4fb697d5
5
5
  SHA512:
6
- metadata.gz: 25c6e87aae4190eae3d94eb852ee3cd2cee2fcf3fa786e416af8b8cf4963950586cc7dcd38a1928c6fadd119b3832cd2419c4202573ac02253040952828df2c8
7
- data.tar.gz: e621c02ea5a122c726583584cd41ae3fa8a98dfec34d3e11e869a9d43f9c9c30d3e576c0589aab1070328f81e8c212eadc5c93a62c2939e77d5fe799634cdb55
6
+ metadata.gz: c1cbadd648ec547bcd888c79504d40b630b7e808f0ef0b1e0f379d8b581816d1c3110112fbed3b597dae9987270b13082e68b0561a6ea5bfec2637e7fc81bc4d
7
+ data.tar.gz: 2864083ceebdd23042ddcd31a3c81d2446b1533b5a7ba8614565138899de6d8fea76bb6a485a8781d16315174cf1856f989890664157bb371ab56cfae2a4e63f
@@ -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
- row.transform_values { |v| coerce_value(v) }
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(m.kind, m.table, m.cols, m.rows, m.wheres, build(m.cols).result)
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
- Mutation(m.kind, m.table, m.cols, rows, m.wheres, m.returning_selector)
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 = "SET " ++ String.join(sets, ", ")
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.table.name
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) ++ where_params(m.wheres) ++ m.returning_selector.params,
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))
@@ -1,3 +1,3 @@
1
1
  module JadeSql
2
- VERSION = '0.6.0'
2
+ VERSION = '0.7.0'
3
3
  end
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.6.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-03 00:00:00.000000000 Z
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.4.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.4.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.