okmodel 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.
Files changed (157) hide show
  1. package/README.md +100 -1131
  2. package/dist/adapters/capabilities.d.ts +3 -0
  3. package/dist/adapters/failure.d.ts +13 -2
  4. package/dist/adapters/lazy-errors.d.ts +22 -0
  5. package/dist/adapters/pg/bunsql-extra.d.ts +54 -3
  6. package/dist/adapters/pg/bunsql.d.ts +14 -1
  7. package/dist/adapters/pg/cursor.d.ts +17 -0
  8. package/dist/adapters/pg/no-inflight-timeout.d.ts +11 -8
  9. package/dist/adapters/pg/nodepostgres-extra.d.ts +22 -1
  10. package/dist/adapters/pg/nodepostgres.d.ts +12 -0
  11. package/dist/adapters/pg/pglite-wire.d.ts +24 -0
  12. package/dist/adapters/pg/pglite.d.ts +10 -0
  13. package/dist/adapters/pg/postgres-extra.d.ts +27 -11
  14. package/dist/adapters/pg/reserved.d.ts +42 -0
  15. package/dist/adapters/pg/session.d.ts +0 -18
  16. package/dist/contracts/connection.d.ts +7 -6
  17. package/dist/dialects/pg/floor.d.ts +68 -0
  18. package/dist/dialects/pg/index.d.ts +6 -1
  19. package/dist/dialects/pg/index.js +49 -159
  20. package/dist/dialects/pg/keys.d.ts +11 -8
  21. package/dist/dialects/pg/model.d.ts +40 -0
  22. package/dist/dialects/pg/morph.d.ts +43 -0
  23. package/dist/dialects/pg/operator-fit.d.ts +2 -2
  24. package/dist/dialects/pg/operators.d.ts +14 -7
  25. package/dist/dialects/pg/ops/bucket.d.ts +65 -0
  26. package/dist/dialects/pg/ops/inc.d.ts +2 -2
  27. package/dist/dialects/pg/ops/matches.d.ts +9 -12
  28. package/dist/dialects/pg/ops/relevance.d.ts +13 -0
  29. package/dist/dialects/pg/ops/search-query.d.ts +22 -0
  30. package/dist/dialects/pg/ops/window.d.ts +103 -0
  31. package/dist/dialects/pg/preset.d.ts +1 -1
  32. package/dist/dialects/pg/relations.d.ts +100 -3
  33. package/dist/dialects/pg/schema.d.ts +31 -3
  34. package/dist/dialects/pg/view/index.js +1 -1
  35. package/dist/runtime/aggregate.d.ts +8 -2
  36. package/dist/runtime/bucket-group.d.ts +70 -0
  37. package/dist/runtime/conflict.d.ts +22 -4
  38. package/dist/runtime/include.d.ts +0 -8
  39. package/dist/runtime/inflight.d.ts +1 -1
  40. package/dist/runtime/page.d.ts +4 -1
  41. package/dist/runtime/pg/bun.js +12 -6
  42. package/dist/runtime/pg/pg.js +5 -5
  43. package/dist/runtime/pg/pglite.d.ts +1 -0
  44. package/dist/runtime/pg/pglite.js +9 -9
  45. package/dist/runtime/pg/postgresjs.js +5 -5
  46. package/dist/runtime/plan.d.ts +95 -5
  47. package/dist/runtime/read-stream.d.ts +13 -16
  48. package/dist/runtime/result-name.d.ts +17 -0
  49. package/dist/runtime/server-check.d.ts +3 -3
  50. package/dist/runtime/sort-bind.d.ts +21 -0
  51. package/dist/runtime/sort.d.ts +56 -0
  52. package/dist/runtime/sync.d.ts +70 -0
  53. package/dist/runtime/tenancy/index.js +7 -5
  54. package/dist/runtime/text-search.d.ts +20 -0
  55. package/dist/runtime/traits/index.d.ts +80 -0
  56. package/dist/runtime/traits/index.js +175 -11
  57. package/dist/runtime/tree.d.ts +38 -0
  58. package/dist/runtime/types.d.ts +218 -22
  59. package/dist/runtime/validate/index.js +2 -2
  60. package/dist/runtime/version.d.ts +39 -0
  61. package/dist/runtime/window.d.ts +21 -0
  62. package/dist/runtime/write.d.ts +54 -2
  63. package/dist/shared/{wkme6tep.js → 14vx95q4.js} +8 -16
  64. package/dist/shared/1est4ezg.js +313 -0
  65. package/dist/shared/{4az561fn.js → 1qa0qajz.js} +8 -23
  66. package/dist/shared/{2cg3wnxc.js → 1w508jbr.js} +593 -130
  67. package/dist/shared/2c13q96h.js +18 -0
  68. package/dist/shared/{b4v57v3x.js → 2d3xq4ps.js} +2 -2
  69. package/dist/shared/{1nw1f3vs.js → 2shdbn8q.js} +7 -7
  70. package/dist/shared/3v7t1yrv.js +208 -0
  71. package/dist/shared/{x3jc53kg.js → 5e3tf63j.js} +2 -2
  72. package/dist/shared/5fe3kbsh.js +10 -0
  73. package/dist/shared/5v3xf80k.js +37 -0
  74. package/dist/shared/{c58secgj.js → 63qtkrca.js} +38 -4
  75. package/dist/shared/{2z7wv5tj.js → 67txcrcw.js} +55 -34
  76. package/dist/shared/79dt02vc.js +12 -0
  77. package/dist/shared/{8x72s86k.js → 7k9grvf6.js} +10 -4
  78. package/dist/shared/7kms6fvj.js +20 -0
  79. package/dist/shared/7mnme5tn.js +8 -0
  80. package/dist/shared/7n7ddwt4.js +6 -0
  81. package/dist/shared/{5j36z8dn.js → 81ebdx9p.js} +39 -70
  82. package/dist/shared/{knmes2kh.js → 8skf0cam.js} +1 -1
  83. package/dist/shared/9a4ae4p8.js +31 -0
  84. package/dist/shared/9y3mmmh1.js +436 -0
  85. package/dist/shared/ab9gmbwy.js +213 -0
  86. package/dist/shared/{f0f1n281.js → ax7qv55k.js} +2 -2
  87. package/dist/shared/{g90m9b0g.js → aytqh5y2.js} +6 -5
  88. package/dist/shared/{vjpnzwjz.js → b5pftnn2.js} +1 -1
  89. package/dist/shared/{ag4c7516.js → b76b8d95.js} +2 -2
  90. package/dist/shared/{vmfkhszw.js → bg3rym3g.js} +33 -26
  91. package/dist/shared/{mb2ycs1p.js → bm22fvzr.js} +15 -15
  92. package/dist/shared/{q3j1tq7a.js → bz84tkzm.js} +148 -23
  93. package/dist/shared/{31vm914h.js → cm3w0ywp.js} +15 -16
  94. package/dist/shared/cn957f8d.js +179 -0
  95. package/dist/shared/{gwn6v667.js → cnkky55j.js} +25 -14
  96. package/dist/shared/{ys0xzyaj.js → dkxgzaqd.js} +224 -19
  97. package/dist/shared/dzj09cye.js +10 -0
  98. package/dist/shared/exn3sd90.js +359 -0
  99. package/dist/shared/g6evj1j3.js +66 -0
  100. package/dist/shared/ga9j92qj.js +25 -0
  101. package/dist/shared/{t3g0k88x.js → gs74dbj5.js} +16 -12
  102. package/dist/shared/gzaveazc.js +285 -0
  103. package/dist/shared/{2jd1sn0e.js → hyh73qk6.js} +1 -1
  104. package/dist/shared/{r1fdd0f6.js → je4axnm8.js} +5 -2
  105. package/dist/shared/{1f0xa3cb.js → jvd0mqag.js} +3 -3
  106. package/dist/shared/jznfax0y.js +155 -0
  107. package/dist/shared/{cgbamx3e.js → kb2sdhfb.js} +7 -4
  108. package/dist/shared/{wt5rzv1h.js → kr0jzbw2.js} +5 -5
  109. package/dist/shared/{p6bjzq9q.js → ktz42yez.js} +12 -4
  110. package/dist/shared/{p248q79y.js → m6p12kbr.js} +2 -2
  111. package/dist/shared/{r6nwkxnt.js → m7n9h2a5.js} +4 -4
  112. package/dist/shared/{672t5vym.js → mss6ctgv.js} +2 -2
  113. package/dist/shared/{929h19xp.js → mtaszv5a.js} +40 -10
  114. package/dist/shared/{b3awxn7h.js → n816peh1.js} +1 -1
  115. package/dist/shared/nbw1z4yd.js +18 -0
  116. package/dist/shared/{nx9hh281.js → p6qc5gcp.js} +12 -11
  117. package/dist/shared/{pjr6xrm5.js → pbp1ss85.js} +20 -17
  118. package/dist/shared/pt94kz7f.js +6 -0
  119. package/dist/shared/q5c6ngn3.js +13 -0
  120. package/dist/shared/{xq0mvhts.js → rs73fca9.js} +4 -4
  121. package/dist/shared/{r11n8grd.js → sfz1ksn6.js} +7 -4
  122. package/dist/shared/{wynzeggs.js → svncdeh5.js} +6 -6
  123. package/dist/shared/{j60w0dvr.js → tbrvgw6z.js} +57 -38
  124. package/dist/shared/te5as7z7.js +622 -0
  125. package/dist/shared/{ttmg09vd.js → tsvww22q.js} +1 -1
  126. package/dist/shared/{y2t3jktd.js → ttsqgp4p.js} +19 -12
  127. package/dist/shared/{x72jb7ad.js → v8g1vgv8.js} +1 -1
  128. package/dist/shared/{sbdme3vy.js → vb6qfyve.js} +60 -10
  129. package/dist/shared/{5d8t84hg.js → vczs8gd4.js} +14 -14
  130. package/dist/shared/{5yv4k6a6.js → vqrjpcqn.js} +144 -2
  131. package/dist/shared/{3fqwmr09.js → w72kgg4q.js} +4 -4
  132. package/dist/shared/{1z04fa85.js → w79hsckn.js} +12 -57
  133. package/dist/shared/{09p049xa.js → wa07eas9.js} +42 -14
  134. package/dist/shared/{32cnm7kn.js → x933k9a3.js} +74 -38
  135. package/dist/shared/{n076ydsc.js → xhfsfkgk.js} +62 -28
  136. package/dist/shared/xjk7hnn5.js +272 -0
  137. package/dist/shared/{hazqqecq.js → xnxsed62.js} +50 -21
  138. package/dist/shared/y6njgyda.js +249 -0
  139. package/dist/shared/{8etn7d8w.js → z7s6fpxv.js} +1 -1
  140. package/dist/shared/{n7mcc1gk.js → zchaghkf.js} +4 -3
  141. package/dist/shared/zm0ptry0.js +40 -0
  142. package/dist/shared/{nsva2a6b.js → znm0bpp5.js} +3 -0
  143. package/dist/shared/zp9fmend.js +145 -0
  144. package/dist/tooling/cli.js +11 -11
  145. package/dist/tooling/migrate/engine.d.ts +3 -2
  146. package/dist/tooling/migrate/index.js +7 -7
  147. package/dist/tooling/testing/index.js +1 -1
  148. package/package.json +4 -2
  149. package/dist/shared/3qnhc7w5.js +0 -38
  150. package/dist/shared/7m88x3e9.js +0 -45
  151. package/dist/shared/8dqhth8c.js +0 -12
  152. package/dist/shared/d5azex6s.js +0 -6
  153. package/dist/shared/j3rkk835.js +0 -99
  154. package/dist/shared/k68eedmy.js +0 -85
  155. package/dist/shared/venqrjna.js +0 -216
  156. package/dist/shared/wxr8cwns.js +0 -6
  157. package/dist/shared/zm0mafk4.js +0 -92
@@ -1,19 +1,14 @@
1
+ import {
2
+ stack2
3
+ } from "./mss6ctgv.js";
1
4
  import {
2
5
  effectivePredicate
3
- } from "./b3awxn7h.js";
6
+ } from "./n816peh1.js";
4
7
  import {
5
8
  assertOperatorFits,
6
9
  textArray,
7
10
  arrayElementType
8
- } from "./n7mcc1gk.js";
9
- import {
10
- stack
11
- } from "./672t5vym.js";
12
- import {
13
- touchFields,
14
- fieldSealed,
15
- sealingTrait
16
- } from "./7m88x3e9.js";
11
+ } from "./zchaghkf.js";
17
12
  import {
18
13
  runWrite
19
14
  } from "./n4jqhpqz.js";
@@ -36,8 +31,8 @@ import {
36
31
  quote,
37
32
  isRecord,
38
33
  registerFailFix,
39
- fail
40
- } from "./2z7wv5tj.js";
34
+ fail2
35
+ } from "./67txcrcw.js";
41
36
  import {
42
37
  safetyInstalled,
43
38
  runSafety
@@ -52,10 +47,54 @@ import {
52
47
  throwNamed2
53
48
  } from "./52szzakm.js";
54
49
 
50
+ // src/runtime/trait-read.ts
51
+ function touchFields(model) {
52
+ const traits = model.traits;
53
+ if (traits === undefined)
54
+ return;
55
+ const names = [];
56
+ for (const trait of traits) {
57
+ const touch = trait.touch;
58
+ if (touch === undefined)
59
+ continue;
60
+ for (const field of touch)
61
+ names.push(field);
62
+ }
63
+ return names.length === 0 ? undefined : names;
64
+ }
65
+ function fieldSealed(model, field) {
66
+ const traits = model.traits;
67
+ if (traits === undefined)
68
+ return false;
69
+ for (const trait of traits) {
70
+ const sealed = trait.sealed;
71
+ if (sealed === undefined)
72
+ continue;
73
+ for (const name of sealed)
74
+ if (name === field)
75
+ return true;
76
+ }
77
+ return false;
78
+ }
79
+ function sealingTrait(model, field) {
80
+ const traits = model.traits;
81
+ if (traits === undefined)
82
+ return;
83
+ for (const trait of traits) {
84
+ const sealed = trait.sealed;
85
+ if (sealed === undefined)
86
+ continue;
87
+ for (const name of sealed)
88
+ if (name === field)
89
+ return trait.name;
90
+ }
91
+ return;
92
+ }
93
+
55
94
  // src/runtime/write.ts
56
95
  registerFailFix("OKM1102", "Pass where, or call .all(reason) to match every row.");
57
96
  registerFailFix("OKM1190", "Remove the guarded field. Input cannot set it.");
58
- var WRITE_PARAM_BUDGET = 2048;
97
+ var WRITE_PARAM_BUDGET2 = 2048;
59
98
  var REDACTED = "[redacted]";
60
99
  function encodeColumn(column, value) {
61
100
  if (!column.hidden && !column.sensitive)
@@ -102,17 +141,21 @@ var INSERT_OPTIONS = [
102
141
  "validate"
103
142
  ];
104
143
  var FILTER_OPTIONS = ["allow", "expect", "returning", "signal", "timeout", "validate"];
105
- var UPDATE_KEYS = ["set", "where"];
106
- var UPDATE_ROW_KEYS = ["id", "set", "where"];
144
+ var UPDATE_KEYS = ["set", "where", "limit", "lock", "version"];
145
+ var UPDATE_ROW_KEYS = ["id", "set", "where", "version"];
107
146
  var NO_ALLOW = new Set;
108
- async function executeWrite(host, op, table, input, options, mods, scope, plan) {
147
+ async function executeWrite2(host, op, table, input, options, mods, scope, plan) {
109
148
  const prepared = await prepareWrite(host, op, table, input, options, mods, scope);
110
149
  if (plan !== undefined)
111
150
  return prepared;
112
151
  if (prepared.statements.length === 0)
113
152
  return prepared.finish([]);
114
153
  const results = await runWrite(host, prepared.statements, prepared.options);
115
- return prepared.finish(results);
154
+ const more = prepared.follow?.(results);
155
+ if (more === undefined || more.length === 0)
156
+ return prepared.finish(results);
157
+ const extra = await runWrite(host, more, prepared.options);
158
+ return prepared.finish(results.concat(extra));
116
159
  }
117
160
  async function prepareWrite(host, op, table, input, options, mods, scope) {
118
161
  try {
@@ -121,13 +164,16 @@ async function prepareWrite(host, op, table, input, options, mods, scope) {
121
164
  statements: planned.statements,
122
165
  options: callOptions(options),
123
166
  ...planned.parentGuard === true ? { checked: "parent" } : planned.expect !== undefined ? { checked: "expect" } : {},
124
- finish: (results) => finish(results, planned, table)
167
+ ...planned.recover === undefined ? {} : {
168
+ follow: (results) => planned.recover?.(decodePlanned(withoutQuiet(results, planned.quiet), planned.outputs)) ?? []
169
+ },
170
+ finish: (results) => finish(withoutQuiet(results, planned.quiet), planned, table)
125
171
  };
126
172
  } catch (error) {
127
173
  throw scrubWrite(host.schema, table, input, error);
128
174
  }
129
175
  }
130
- async function explainWrite(schema, op, table, input, options, mods, generators, scope) {
176
+ async function explainWrite2(schema, op, table, input, options, mods, generators, scope) {
131
177
  try {
132
178
  const planned = await planWrite(schema, generators, op, table, input, options, mods, scope);
133
179
  return { statements: planned.statements };
@@ -139,10 +185,10 @@ async function planWrite(schema, generators, op, tableName, input, options, mods
139
185
  await requireValidationImport(schema, op, tableName, options);
140
186
  const table = indexes(schema).get(tableName);
141
187
  if (table === undefined) {
142
- fail("OKM1120", `Table ${tableName} is not in the schema.`);
188
+ fail2("OKM1120", `Table ${tableName} is not in the schema.`);
143
189
  }
144
190
  if (mods.uses !== undefined && op !== "insert") {
145
- const { resolve } = await import("./r6nwkxnt.js");
191
+ const { resolve } = await import("./m7n9h2a5.js");
146
192
  mods = { ...mods, presets: resolve(schema, tableName, mods.uses).wheres };
147
193
  }
148
194
  const record = isRecord(options) ? options : {};
@@ -151,6 +197,14 @@ async function planWrite(schema, generators, op, tableName, input, options, mods
151
197
  return planInsert(table, input, record, mods, generators, scope, schema.tenancy);
152
198
  }
153
199
  if (op === "update") {
200
+ if (traitNamed(table, "versioned")) {
201
+ const { planVersioned } = await import("./y6njgyda.js");
202
+ return withRowFilters(scope, mods.archive, () => {
203
+ noteArchive(table, mods.archive);
204
+ const planned = planVersioned(schema, table, input, record, mods);
205
+ return { ...planned, keys: undefined, keyColumns: undefined };
206
+ });
207
+ }
154
208
  return withRowFilters(scope, mods.archive, () => {
155
209
  noteArchive(table, mods.archive);
156
210
  return planUpdate(schema, table, input, record, mods, scope);
@@ -162,8 +216,8 @@ async function planWrite(schema, generators, op, tableName, input, options, mods
162
216
  });
163
217
  }
164
218
  async function planInsert(table, input, options, mods, generators, scope, tenancy) {
165
- const allow = allowSet(table, options.allow);
166
- noteGuarded(table, allow, "insert");
219
+ const allow = allowSet2(table, options.allow);
220
+ noteGuarded2(table, allow, "insert");
167
221
  const many = Array.isArray(input);
168
222
  const rows = (many ? input : [input]).map((row) => insertRow(table, row, allow, tenancy));
169
223
  tenancy?.stamp(table.model.name, rows, scope);
@@ -172,13 +226,14 @@ async function planInsert(table, input, options, mods, generators, scope, tenanc
172
226
  const returning = selectedColumns(table, options.returning);
173
227
  const outputs = outputsOf(returning);
174
228
  const clause = returningClause(returning, undefined);
175
- const expect = expectOf(options, mods);
229
+ const expect = expectOf2(options, mods);
176
230
  const shape = many ? "rows" : "row";
177
231
  const path = tenancy?.isPath?.(table.model.name) === true;
178
232
  if (path && options.onConflict !== undefined && options.onConflict !== "error") {
179
- fail("OKM1120", `onConflict on ${table.model.name} is not available. The insert checks the parent.`);
233
+ fail2("OKM1120", `onConflict on ${table.model.name} is not available. The insert checks the parent.`);
180
234
  }
181
- if (columns.length === 0) {
235
+ const sorting = traitNamed(table, "sortable");
236
+ if (columns.length === 0 && !sorting) {
182
237
  const text = `insert into ${quote(table.model.sql)} default values${clause}`;
183
238
  return {
184
239
  statements: rows.map(() => ({ text })),
@@ -186,51 +241,74 @@ async function planInsert(table, input, options, mods, generators, scope, tenanc
186
241
  shape,
187
242
  expect,
188
243
  keys: undefined,
189
- keyFields: undefined
244
+ keyColumns: undefined
190
245
  };
191
246
  }
192
- const conflictMod = options.onConflict === undefined || options.onConflict === "error" ? undefined : await import("./vmfkhszw.js");
247
+ const conflictMod = options.onConflict === undefined || options.onConflict === "error" ? undefined : await import("./bg3rym3g.js");
193
248
  const conflict = conflictMod?.readConflict(table, options.onConflict, allow, tenantKeyList(tenancy));
249
+ if (conflict?.kind === "update" && traitNamed(table, "versioned")) {
250
+ fail2("OKM1120", `onConflict update on ${table.model.name} does not check the version you read.`, "Read the row and update. ignore and return do not change the existing row.");
251
+ }
252
+ const sortMod = sorting ? await import("./9y3mmmh1.js") : undefined;
194
253
  const chunks = chunkRows(rows.map((row) => cellsFor(columns, row)));
195
254
  const statements = [];
255
+ const quiet = [];
196
256
  const keys = [];
197
257
  let offset = 0;
198
258
  for (const chunk of chunks) {
199
259
  const slice = rows.slice(offset, offset + chunk.length);
200
260
  offset += chunk.length;
201
- const built = path ? pathInsert(table, columns, chunk, scope, tenancy) : insertHead(table, columns, chunk);
261
+ const head = path || sortMod === undefined ? undefined : sortMod.insertAtEnd(table, columns, slice, traitGroup(table), tenantKeyList(tenancy));
262
+ const built = head ?? (path ? pathInsert(table, columns, chunk, scope, tenancy) : insertHead(table, columns, chunk));
263
+ if (head !== undefined) {
264
+ quiet.push(statements.length);
265
+ statements.push(head.lock);
266
+ }
202
267
  if (conflict === undefined || conflictMod === undefined) {
203
268
  statements.push(withParams(`${built.text}${clause}`, built.params));
204
269
  continue;
205
270
  }
206
- const keyRows = conflict.kind === "return" ? slice.map((row) => keyWire(conflict.columns, row)) : [];
207
- for (const row of keyRows)
208
- keys.push([...row]);
209
- statements.push(conflictMod.finishInsert(built.text, clause, built.params ?? [], conflict, table, returning, keyRows));
271
+ if (conflict.kind === "return") {
272
+ for (const row of slice)
273
+ keys.push(keyWire(conflict.columns, row));
274
+ }
275
+ statements.push(conflictMod.finishInsert(built.text, clause, built.params ?? [], conflict, table));
210
276
  }
277
+ const conflictColumns = conflict?.kind === "return" ? conflict.columns : undefined;
211
278
  return {
212
279
  statements,
213
280
  outputs,
214
281
  shape,
215
282
  expect,
216
- keys: conflict?.kind === "return" ? keys : undefined,
217
- keyFields: conflict?.kind === "return" ? conflict.columns.map((column) => column.field) : undefined,
218
- ...path ? { parent: rows.length } : {}
283
+ keys: conflictColumns === undefined ? undefined : keys,
284
+ keyColumns: conflictColumns,
285
+ ...path ? { parent: rows.length } : {},
286
+ ...quiet.length > 0 ? { quiet } : {},
287
+ ...conflictColumns === undefined || conflictMod === undefined ? {} : {
288
+ recover: (found) => lookupStatements(conflictMod, table, conflictColumns, returning, missingKeys(found, conflictColumns, keys))
289
+ }
219
290
  };
220
291
  }
221
292
  function planUpdate(schema, table, input, options, mods, scope) {
222
- const allow = allowSet(table, options.allow);
223
- noteGuarded(table, allow, "update");
224
- const returning = returningOption(table, options.returning);
293
+ if (mentionsVersion(input)) {
294
+ fail2("OKM1120", `version on ${table.model.name} needs versioned().`, "Add versioned() from okmodel/traits, or remove version.");
295
+ }
296
+ const allow = allowSet2(table, options.allow);
297
+ noteGuarded2(table, allow, "update");
298
+ const returning = returningOption2(table, options.returning);
225
299
  const outputs = outputsOf(returning);
226
- const expect = expectOf(options, mods);
300
+ const expect = expectOf2(options, mods);
227
301
  if (Array.isArray(input)) {
228
302
  const rows = input.map((item) => {
229
303
  const row = updateItem(table, item, allow, schema.tenancy);
230
- requireFilter(table, row.where, mods, "update");
304
+ requireFilter2(table, row.where, mods, "update");
231
305
  pathAssignment(schema.tenancy, table.model.name, row.set, scope);
232
- return { ...row, where: stack(row.where, mods.presets) };
306
+ return { ...row, where: stack2(row.where, mods.presets) };
233
307
  });
308
+ rejectDuplicateTargets2(table, rows);
309
+ rejectMixedSets2(rows);
310
+ for (const row of rows)
311
+ refuseTreeCycle(table, row.where, row.set, schema.tenancy?.key);
234
312
  const moves = rows.some((row) => pathField(schema.tenancy, table.model.name, row.set) !== undefined);
235
313
  if (moves) {
236
314
  return {
@@ -239,7 +317,7 @@ function planUpdate(schema, table, input, options, mods, scope) {
239
317
  shape: returning === undefined ? "count" : "rows",
240
318
  expect,
241
319
  keys: undefined,
242
- keyFields: undefined,
320
+ keyColumns: undefined,
243
321
  parentGuard: true
244
322
  };
245
323
  }
@@ -249,33 +327,49 @@ function planUpdate(schema, table, input, options, mods, scope) {
249
327
  shape: returning === undefined ? "count" : "rows",
250
328
  expect,
251
329
  keys: undefined,
252
- keyFields: undefined
330
+ keyColumns: undefined
253
331
  };
254
332
  }
255
333
  if (!isRecord(input))
256
- fail("OKM1121", "update expects { where, set } or a list of rows.");
257
- rejectUpdateKeys(input, false);
258
- const set = writeSet(table, input.set, allow, schema.tenancy);
259
- requireFilter(table, input.where, mods, "update");
334
+ fail2("OKM1121", "update expects { where, set } or a list of rows.");
335
+ const claim = readClaim2(input);
336
+ rejectUpdateKeys2(input, false);
337
+ const set = writeSet2(table, input.set, allow, schema.tenancy);
338
+ requireFilter2(table, input.where, mods, "update");
260
339
  if (Object.keys(set).length === 0)
261
- fail("OKM1120", "update set is empty.");
262
- const where = stack(input.where, mods.presets);
263
- if (pathAssignment(schema.tenancy, table.model.name, set, scope) !== undefined) {
340
+ fail2("OKM1120", "update set is empty.");
341
+ refuseTreeCycle(table, input.where, set, schema.tenancy?.key);
342
+ const where = stack2(input.where, mods.presets);
343
+ const moved = pathAssignment(schema.tenancy, table.model.name, set, scope);
344
+ if (moved !== undefined && claim !== undefined) {
345
+ fail2("OKM1120", `lock: "skip" on ${table.model.name} cannot change ${moved} in the same claim.`, "Claim the row first. Point it at a parent in a later update.");
346
+ }
347
+ if (moved !== undefined) {
264
348
  return {
265
349
  statements: [pathSetUpdate(schema, table, where, set, scope, returning)],
266
350
  outputs: parentOutputs(returning),
267
351
  shape: returning === undefined ? "count" : "rows",
268
352
  expect,
269
353
  keys: undefined,
270
- keyFields: undefined,
354
+ keyColumns: undefined,
271
355
  parentGuard: true
272
356
  };
273
357
  }
274
- const sql = new Sql;
358
+ if (claim !== undefined) {
359
+ return {
360
+ statements: [emitClaim(schema, table, where, set, claim, returning)],
361
+ outputs,
362
+ shape: returning === undefined ? "count" : "rows",
363
+ expect,
364
+ keys: undefined,
365
+ keyColumns: undefined
366
+ };
367
+ }
368
+ const sql = new Sql2;
275
369
  sql.text("update ");
276
370
  sql.text(quote(table.model.sql));
277
371
  sql.text(" as t set ");
278
- emitSet(sql, table, set, "t");
372
+ emitSet2(sql, table, set, "t");
279
373
  emitFilter(schema, table, where, sql, "t");
280
374
  schema.tenancy?.noteParent?.(table.model.name, set, sql.sink(), scope, (field) => table.columns.get(field)?.sql);
281
375
  sql.text(returningClause(returning, "t"));
@@ -285,31 +379,36 @@ function planUpdate(schema, table, input, options, mods, scope) {
285
379
  shape: returning === undefined ? "count" : "rows",
286
380
  expect,
287
381
  keys: undefined,
288
- keyFields: undefined
382
+ keyColumns: undefined
289
383
  };
290
384
  }
291
385
  function planDelete(schema, table, input, options, mods) {
292
386
  if (!isRecord(input))
293
- fail("OKM1121", "delete expects { where }.");
387
+ fail2("OKM1121", "delete expects { where }.");
294
388
  rejectKeys(input, ["where"], "delete");
295
- requireFilter(table, input.where, mods, "delete");
296
- const returning = returningOption(table, options.returning);
297
- const sql = new Sql;
389
+ requireFilter2(table, input.where, mods, "delete");
390
+ if (options.returning === true) {
391
+ fail2("OKM1120", "delete does not accept returning: true.", "Pass the field names in returning, or omit returning for a count.");
392
+ }
393
+ const returning = returningOption2(table, options.returning);
394
+ const sql = new Sql2;
298
395
  sql.text("delete from ");
299
396
  sql.text(quote(table.model.sql));
300
397
  sql.text(" as t");
301
- emitFilter(schema, table, stack(input.where, mods.presets), sql, "t");
398
+ emitFilter(schema, table, stack2(input.where, mods.presets), sql, "t");
302
399
  sql.text(returningClause(returning, "t"));
303
400
  return {
304
401
  statements: [sql.statement()],
305
402
  outputs: outputsOf(returning),
306
403
  shape: returning === undefined ? "count" : "rows",
307
- expect: expectOf(options, mods),
404
+ expect: expectOf2(options, mods),
308
405
  keys: undefined,
309
- keyFields: undefined
406
+ keyColumns: undefined
310
407
  };
311
408
  }
312
409
  function finish(results, planned, table) {
410
+ if (planned.versioned !== undefined)
411
+ return finishVersioned(results, planned, table);
313
412
  if (planned.parentGuard === true)
314
413
  return finishParent(results, planned, table);
315
414
  if (planned.shape === "count") {
@@ -319,12 +418,8 @@ function finish(results, planned, table) {
319
418
  checkExpect(count, planned.expect, table, "write");
320
419
  return { count };
321
420
  }
322
- const decoded = [];
323
- for (const result of results) {
324
- for (const row of result.rows)
325
- decoded.push(decodeRow(planned.outputs, row));
326
- }
327
- const rows = planned.keys === undefined || planned.keyFields === undefined ? decoded : orderByKeys(decoded, planned.keyFields, planned.keys);
421
+ const decoded = decodePlanned(results, planned.outputs);
422
+ const rows = planned.keys === undefined || planned.keyColumns === undefined ? decoded : orderByKeys(decoded, planned.keyColumns, planned.keys);
328
423
  checkExpect(rows.length, planned.expect, table, "insert");
329
424
  if (planned.parent !== undefined && rows.length < planned.parent) {
330
425
  throw new OkmError2("OKM1705", `Insert on ${table} did not find a parent in this tenant.`, {
@@ -360,6 +455,36 @@ function finishParent(results, planned, table) {
360
455
  return decoded[0] ?? null;
361
456
  return decoded;
362
457
  }
458
+ function finishVersioned(results, planned, table) {
459
+ const versioned = planned.versioned;
460
+ const first = results[0]?.rows[0];
461
+ if (versioned !== undefined && first !== undefined && countCell(first[0]) > 0) {
462
+ const fields = versioned.fields.length === 0 ? "the row" : versioned.fields.join(", ");
463
+ const sent = versioned.sent.join(", ");
464
+ throw new OkmError2("conflict", `update on ${table} tried to write ${fields} at version ${sent}, and the row changed. Reread the row and retry with the version you read.`, {
465
+ kind: "conflict",
466
+ columns: ["version"],
467
+ fix: { summary: "Reread the row and retry with the version you read." }
468
+ });
469
+ }
470
+ if (planned.shape === "count") {
471
+ const updated = first === undefined ? 0 : countCell(first[1]);
472
+ checkExpect(updated, planned.expect, table, "write");
473
+ return { count: updated };
474
+ }
475
+ const decoded = [];
476
+ for (const result of results) {
477
+ for (const item of result.rows) {
478
+ if (countCell(item[1]) === 0)
479
+ continue;
480
+ decoded.push(decodeRow(planned.outputs, item));
481
+ }
482
+ }
483
+ checkExpect(decoded.length, planned.expect, table, "write");
484
+ if (planned.shape === "row")
485
+ return decoded[0] ?? null;
486
+ return decoded;
487
+ }
363
488
  function countCell(value) {
364
489
  if (typeof value === "number")
365
490
  return value;
@@ -401,23 +526,30 @@ function parentOutputs(returning) {
401
526
  includes: outputs.includes
402
527
  };
403
528
  }
529
+ function rowId(alias) {
530
+ return `${alias}.tableoid, ${alias}.ctid`;
531
+ }
404
532
  function pathSetUpdate(schema, table, where, set, scope, returning) {
405
- const sql = new Sql;
406
- sql.text("with matched as (select t.ctid as tid from ");
533
+ const sql = new Sql2;
534
+ sql.text("with matched as (select ");
535
+ sql.text(rowId("t"));
536
+ sql.text(" from ");
407
537
  sql.text(quote(table.model.sql));
408
538
  sql.text(" as t");
409
539
  emitFilter(schema, table, where, sql, "t");
410
540
  sql.text("), updated as (update ");
411
541
  sql.text(quote(table.model.sql));
412
542
  sql.text(" as t set ");
413
- emitSet(sql, table, set, "t");
414
- sql.text(" where t.ctid in (select tid from matched)");
543
+ emitSet2(sql, table, set, "t");
544
+ sql.text(" where (");
545
+ sql.text(rowId("t"));
546
+ sql.text(") in (select tableoid, ctid from matched)");
415
547
  schema.tenancy?.noteParent?.(table.model.name, set, sql.sink(), scope, (field) => table.columns.get(field)?.sql);
416
548
  endParent(sql, returning);
417
549
  return sql.statement();
418
550
  }
419
551
  function pathListUpdate(schema, table, rows, scope, returning) {
420
- const sql = new Sql;
552
+ const sql = new Sql2;
421
553
  sql.text("with matched as (");
422
554
  for (let index = 0;index < rows.length; index += 1) {
423
555
  const row = rows[index];
@@ -453,7 +585,7 @@ function pathListUpdate(schema, table, rows, scope, returning) {
453
585
  if (!rejected)
454
586
  sql.text("select 0 as ord where false");
455
587
  sql.text("), updated as (");
456
- updateSliceBody(schema, table, rows, sql, true);
588
+ updateSliceBody2(schema, table, rows, sql, true);
457
589
  sql.text(" and not exists (select 1 from rejected)");
458
590
  endParent(sql, returning);
459
591
  return sql.statement();
@@ -483,7 +615,7 @@ function endParent(sql, returning) {
483
615
  sql.text(returningClause(returning, "t"));
484
616
  sql.text(") select c.matched, c.updated, u.* from (select (select count(*)::int from matched) as matched, (select count(*)::int from updated) as updated) c left join updated u on true");
485
617
  }
486
- function updateSliceBody(schema, table, rows, sql, wrapWhere = false) {
618
+ function updateSliceBody2(schema, table, rows, sql, wrapWhere = false, hooks) {
487
619
  const fields = [];
488
620
  const seen = new Set;
489
621
  for (const row of rows) {
@@ -497,7 +629,7 @@ function updateSliceBody(schema, table, rows, sql, wrapWhere = false) {
497
629
  }
498
630
  }
499
631
  if (fields.length === 0)
500
- fail("OKM1120", "update set is empty.");
632
+ fail2("OKM1120", "update set is empty.");
501
633
  sql.text("update ");
502
634
  sql.text(quote(table.model.sql));
503
635
  sql.text(" as t set ");
@@ -509,9 +641,13 @@ function updateSliceBody(schema, table, rows, sql, wrapWhere = false) {
509
641
  sql.text(", ");
510
642
  sql.text(quote(column.sql));
511
643
  sql.text(" = case");
512
- for (const row of rows) {
644
+ for (let index = 0;index < rows.length; index += 1) {
645
+ const row = rows[index];
646
+ if (row === undefined)
647
+ continue;
513
648
  sql.text(" when ");
514
649
  emitBare(schema, table, row.where, sql, "t");
650
+ hooks?.afterWhere?.(sql, index);
515
651
  sql.text(" then ");
516
652
  const value = row.set[column.field];
517
653
  if (value === undefined)
@@ -524,6 +660,7 @@ function updateSliceBody(schema, table, rows, sql, wrapWhere = false) {
524
660
  sql.text(" end");
525
661
  }
526
662
  emitTouch(sql, table, fields.length > 0);
663
+ hooks?.afterSet?.(sql);
527
664
  sql.text(wrapWhere ? " where (" : " where ");
528
665
  for (let index = 0;index < rows.length; index += 1) {
529
666
  const row = rows[index];
@@ -533,15 +670,132 @@ function updateSliceBody(schema, table, rows, sql, wrapWhere = false) {
533
670
  sql.text(" or ");
534
671
  sql.text("(");
535
672
  emitBare(schema, table, row.where, sql, "t");
673
+ hooks?.afterWhere?.(sql, index);
536
674
  sql.text(")");
537
675
  }
538
676
  if (wrapWhere)
539
677
  sql.text(")");
540
678
  }
541
- function orderByKeys(rows, fields, keys) {
679
+ function decodePlanned(results, outputs) {
680
+ const decoded = [];
681
+ for (const result of results) {
682
+ for (const row of result.rows)
683
+ decoded.push(decodeRow(outputs, row));
684
+ }
685
+ return decoded;
686
+ }
687
+ function withoutQuiet(results, quiet) {
688
+ if (quiet === undefined || quiet.length === 0)
689
+ return results;
690
+ const hidden = new Set(quiet);
691
+ const kept = [];
692
+ for (let index = 0;index < results.length; index += 1) {
693
+ if (hidden.has(index))
694
+ continue;
695
+ const result = results[index];
696
+ if (result !== undefined)
697
+ kept.push(result);
698
+ }
699
+ return kept;
700
+ }
701
+ function lookupStatements(mod, table, columns, returning, keyRows) {
702
+ if (keyRows.length === 0 || returning.length === 0)
703
+ return [];
704
+ const width = Math.max(1, columns.length);
705
+ const size = Math.max(1, Math.floor(WRITE_PARAM_BUDGET2 / width));
706
+ const statements = [];
707
+ for (let index = 0;index < keyRows.length; index += size) {
708
+ statements.push(mod.lookupExisting(table, columns, returning, keyRows.slice(index, index + size)));
709
+ }
710
+ return statements;
711
+ }
712
+ function missingKeys(rows, columns, keys) {
713
+ const have = new Set(rows.map((row) => rowKey(columns, row)));
714
+ const missing = [];
715
+ const seen = new Set;
716
+ for (const key of keys) {
717
+ const id = key.map((value) => value ?? "").join("\x00");
718
+ if (have.has(id) || seen.has(id))
719
+ continue;
720
+ seen.add(id);
721
+ missing.push(key);
722
+ }
723
+ return missing;
724
+ }
725
+ function rejectDuplicateTargets2(table, rows) {
726
+ if (rows.length < 2)
727
+ return;
728
+ const seen = new Set;
729
+ for (const row of rows) {
730
+ const key = canonical(row.where);
731
+ if (seen.has(key)) {
732
+ fail2("OKM1120", `update on ${table.model.name} lists the same target twice.`, "List each row once.");
733
+ }
734
+ seen.add(key);
735
+ }
736
+ }
737
+ var unresolved = 0;
738
+ function canonical(value) {
739
+ if (isOperator(value))
740
+ return `op:${operatorName(value)}:${canonical(operatorValue(value))}`;
741
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
742
+ return JSON.stringify(value);
743
+ }
744
+ if (value === null)
745
+ return "null";
746
+ if (value === undefined)
747
+ return "undefined";
748
+ if (typeof value === "bigint")
749
+ return JSON.stringify(value.toString());
750
+ if (Array.isArray(value))
751
+ return `[${value.map((item) => canonical(item)).join(",")}]`;
752
+ if (isRecord(value)) {
753
+ if (plainRecord(value)) {
754
+ const keys = Object.keys(value).sort();
755
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
756
+ }
757
+ return boxed(value);
758
+ }
759
+ return fresh();
760
+ }
761
+ function plainRecord(value) {
762
+ const proto = Object.getPrototypeOf(value);
763
+ return proto === Object.prototype || proto === null;
764
+ }
765
+ function boxed(value) {
766
+ if (value instanceof Uint8Array)
767
+ return `bytes:${hexBytes(value)}`;
768
+ if (value instanceof Date) {
769
+ const time = value.getTime();
770
+ if (Number.isNaN(time))
771
+ return fresh();
772
+ return `date:${value.toISOString()}`;
773
+ }
774
+ const read = value.toString;
775
+ if (read === Object.prototype.toString)
776
+ return fresh();
777
+ const text = read.call(value);
778
+ if (text.length > 0 && text !== "[object Object]")
779
+ return `text:${text}`;
780
+ return fresh();
781
+ }
782
+ function hexBytes(value) {
783
+ let hex = "";
784
+ for (const byte of value)
785
+ hex += byte.toString(16).padStart(2, "0");
786
+ return hex;
787
+ }
788
+ function fresh() {
789
+ unresolved += 1;
790
+ return `~${String(unresolved)}`;
791
+ }
792
+ function orderByKeys(rows, columns, keys) {
542
793
  const found = new Map;
543
- for (const row of rows)
544
- found.set(fields.map((field) => wireKey(row[field])).join("\x00"), row);
794
+ for (const row of rows) {
795
+ const id = rowKey(columns, row);
796
+ if (!found.has(id))
797
+ found.set(id, row);
798
+ }
545
799
  const ordered = [];
546
800
  for (const key of keys) {
547
801
  const row = found.get(key.map((value) => value ?? "").join("\x00"));
@@ -550,13 +804,30 @@ function orderByKeys(rows, fields, keys) {
550
804
  }
551
805
  return ordered;
552
806
  }
807
+ function rowKey(columns, row) {
808
+ let id = "";
809
+ for (let index = 0;index < columns.length; index += 1) {
810
+ const column = columns[index];
811
+ if (column === undefined)
812
+ continue;
813
+ if (index > 0)
814
+ id += "\x00";
815
+ id += valueKey(column, row[column.field]);
816
+ }
817
+ return id;
818
+ }
819
+ function valueKey(column, value) {
820
+ if (value === undefined || value === null)
821
+ return "";
822
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
823
+ return wireKey(value);
824
+ }
825
+ return column.encode(value);
826
+ }
553
827
  function wireKey(value) {
554
828
  if (typeof value === "string")
555
829
  return value;
556
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
557
- return String(value);
558
- }
559
- return "";
830
+ return String(value);
560
831
  }
561
832
  function insertHead(table, columns, rows) {
562
833
  const params = [];
@@ -583,7 +854,7 @@ function updateList(schema, table, rows, returning) {
583
854
  if (rows.length === 0)
584
855
  return [];
585
856
  const width = Math.max(1, Object.keys(rows[0]?.set ?? {}).length + 2);
586
- const size = Math.max(1, Math.floor(WRITE_PARAM_BUDGET / (width * 2)));
857
+ const size = Math.max(1, Math.floor(WRITE_PARAM_BUDGET2 / (width * 2)));
587
858
  const statements = [];
588
859
  for (let index = 0;index < rows.length; index += size) {
589
860
  statements.push(updateSlice(schema, table, rows.slice(index, index + size), returning));
@@ -591,12 +862,32 @@ function updateList(schema, table, rows, returning) {
591
862
  return statements;
592
863
  }
593
864
  function updateSlice(schema, table, rows, returning) {
594
- const sql = new Sql;
595
- updateSliceBody(schema, table, rows, sql);
865
+ const sql = new Sql2;
866
+ updateSliceBody2(schema, table, rows, sql);
596
867
  sql.text(returningClause(returning, "t"));
597
868
  return sql.statement();
598
869
  }
599
- function emitSet(sql, table, set, alias) {
870
+ function emitClaim(schema, table, where, set, limit, returning) {
871
+ const sql = new Sql2;
872
+ sql.text("update ");
873
+ sql.text(quote(table.model.sql));
874
+ sql.text(" as t set ");
875
+ emitSet2(sql, table, set, "t");
876
+ sql.text(" where (");
877
+ sql.text(rowId("t"));
878
+ sql.text(") in (select ");
879
+ sql.text(rowId("s"));
880
+ sql.text(" from ");
881
+ sql.text(quote(table.model.sql));
882
+ sql.text(" as s");
883
+ emitFilter(schema, table, where, sql, "s");
884
+ sql.text(" for update of s skip locked limit ");
885
+ sql.param(String(limit), "integer");
886
+ sql.text(")");
887
+ sql.text(returningClause(returning, "t"));
888
+ return sql.statement();
889
+ }
890
+ function emitSet2(sql, table, set, alias) {
600
891
  const keys = Object.keys(set);
601
892
  let wrote = false;
602
893
  for (const key of keys) {
@@ -632,12 +923,7 @@ function emitValue(sql, column, value, alias) {
632
923
  if (isOperator(value)) {
633
924
  const name = operatorName(value);
634
925
  if (name === "inc") {
635
- const amount = operatorValue(value);
636
- if (typeof amount === "object" && amount !== null) {
637
- fail("OKM1121", `inc on ${column.field} needs a value.`);
638
- }
639
- sql.text(`${alias}.${quote(column.sql)} + `);
640
- sql.param(encodeColumn(column, amount), column.dataType);
926
+ emitIncrement(sql, column, value, alias);
641
927
  return;
642
928
  }
643
929
  if (name === "json.set") {
@@ -648,7 +934,7 @@ function emitValue(sql, column, value, alias) {
648
934
  emitArrayWrite(sql, column, name, value, alias);
649
935
  return;
650
936
  }
651
- fail("OKM1121", `Field ${column.field} received an operator. set accepts a value, inc, set, or arr.`);
937
+ fail2("OKM1121", `Field ${column.field} received an operator. set accepts a value, inc, json.set, arr.append, or arr.remove.`);
652
938
  }
653
939
  if (value === null) {
654
940
  sql.text(`null::${column.dataType}`);
@@ -658,17 +944,31 @@ function emitValue(sql, column, value, alias) {
658
944
  takeObject(column, value);
659
945
  sql.param(encodeColumn(column, value), column.dataType);
660
946
  }
947
+ var NUMBER_TYPE = /^(?:smallint|integer|bigint|real|double precision|numeric|decimal)(?:\(\d+(?:,\d+)?\))?$/;
948
+ function emitIncrement(sql, column, value, alias) {
949
+ if (!NUMBER_TYPE.test(column.dataType)) {
950
+ fail2("OKM1124", `inc adds to a number. ${column.field} is a ${column.dataType} column.`, "Use inc on an integer, bigint, numeric, or float column.");
951
+ }
952
+ if (!isOperator(value))
953
+ fail2("OKM1121", `inc on ${column.field} needs a number.`);
954
+ const amount = operatorValue(value);
955
+ if (typeof amount !== "number" && typeof amount !== "bigint" && typeof amount !== "string") {
956
+ fail2("OKM1121", `inc on ${column.field} needs a number.`, "Pass the amount to add. null stores null; it is not an amount.");
957
+ }
958
+ sql.text(`${alias}.${quote(column.sql)} + `);
959
+ sql.param(encodeColumn(column, amount), column.dataType);
960
+ }
661
961
  function emitJsonSet(sql, column, value, alias) {
662
962
  assertOperatorFits(column, "json.set");
663
963
  if (!isOperator(value))
664
- fail("OKM1121", `json.set on ${column.field} needs a path and a value.`);
964
+ fail2("OKM1121", `json.set on ${column.field} needs a path and a value.`);
665
965
  const stored = operatorValue(value);
666
966
  if (!isRecord(stored) || !Array.isArray(stored.path)) {
667
- fail("OKM1121", `json.set on ${column.field} needs a path and a value.`);
967
+ fail2("OKM1121", `json.set on ${column.field} needs a path and a value.`);
668
968
  }
669
969
  const path = stored.path;
670
970
  if (path.length === 0 || path.some((item) => typeof item !== "string")) {
671
- fail("OKM1121", `json.set on ${column.field} needs a list of segments.`);
971
+ fail2("OKM1121", `json.set on ${column.field} needs a list of segments.`);
672
972
  }
673
973
  const encoded = encodeJson(stored.value);
674
974
  const ref = `${alias}.${quote(column.sql)}`;
@@ -685,10 +985,10 @@ function emitArrayWrite(sql, column, name, value, alias) {
685
985
  assertOperatorFits(column, name);
686
986
  const encode = column.elementEncode;
687
987
  if (encode === undefined) {
688
- fail("OKM1124", `Operator ${name} does not apply to ${column.dataType} column ${column.field}. Accepted operators: eq, not, lt, lte, gt, gte, between, inList, notIn, inc.`);
988
+ fail2("OKM1124", `Operator ${name} does not apply to ${column.dataType} column ${column.field}. Accepted operators: eq, not, lt, lte, gt, gte, between, inList, notIn, inc.`);
689
989
  }
690
990
  if (!isOperator(value)) {
691
- fail("OKM1121", `${name} on ${column.field} needs an element.`);
991
+ fail2("OKM1121", `${name} on ${column.field} needs an element.`);
692
992
  }
693
993
  const fn = name === "arr.append" ? "array_append" : "array_remove";
694
994
  sql.text(`${fn}(${alias}.${quote(column.sql)}, `);
@@ -716,11 +1016,42 @@ function emitBare(schema, table, where, sql, alias) {
716
1016
  };
717
1017
  emitWhere(schema, table, where, sink, alias, 0);
718
1018
  if (!dropped) {
719
- fail("OKM1102", `update on ${table.model.name} needs a where for each row.`);
1019
+ fail2("OKM1102", `update on ${table.model.name} needs a where for each row.`);
1020
+ }
1021
+ }
1022
+ function traitNamed(table, name) {
1023
+ const traits = table.model.traits;
1024
+ if (traits === undefined)
1025
+ return false;
1026
+ for (const trait of traits)
1027
+ if (trait.name === name)
1028
+ return true;
1029
+ return false;
1030
+ }
1031
+ function traitGroup(table) {
1032
+ const traits = table.model.traits;
1033
+ if (traits === undefined)
1034
+ return;
1035
+ for (const trait of traits) {
1036
+ if (trait.name !== "sortable")
1037
+ continue;
1038
+ const group = trait.group;
1039
+ return typeof group === "string" ? group : undefined;
720
1040
  }
1041
+ return;
1042
+ }
1043
+ function mentionsVersion(input) {
1044
+ if (Array.isArray(input)) {
1045
+ for (const item of input) {
1046
+ if (isRecord(item) && Object.hasOwn(item, "version"))
1047
+ return true;
1048
+ }
1049
+ return false;
1050
+ }
1051
+ return isRecord(input) && Object.hasOwn(input, "version");
721
1052
  }
722
1053
 
723
- class Sql {
1054
+ class Sql2 {
724
1055
  parts = [];
725
1056
  params = [];
726
1057
  text(value) {
@@ -748,7 +1079,7 @@ class Sql {
748
1079
  }
749
1080
  function insertRow(table, value, allow, tenancy) {
750
1081
  if (!isRecord(value))
751
- fail("OKM1121", "insert expects an object or a list of objects.");
1082
+ fail2("OKM1121", "insert expects an object or a list of objects.");
752
1083
  const row = {};
753
1084
  for (const key of Object.keys(value)) {
754
1085
  const column = table.columns.get(key);
@@ -760,6 +1091,7 @@ function insertRow(table, value, allow, tenancy) {
760
1091
  if (value[key] !== undefined)
761
1092
  row[key] = value[key];
762
1093
  }
1094
+ refuseHalfMorph(table, row);
763
1095
  return row;
764
1096
  }
765
1097
  function cellsFor(columns, row) {
@@ -824,7 +1156,7 @@ function tenantKeyList(tenancy) {
824
1156
  return [tenancy.key];
825
1157
  }
826
1158
  function pathInsert(table, columns, rows, scope, tenancy) {
827
- const sql = new Sql;
1159
+ const sql = new Sql2;
828
1160
  const names = columns.map((column) => quote(column.sql)).join(", ");
829
1161
  const selected = columns.map((column) => `v.${quote(column.sql)}`).join(", ");
830
1162
  sql.text(`insert into ${quote(table.model.sql)} (${names}) select ${selected} from (values `);
@@ -859,9 +1191,13 @@ function pathInsert(table, columns, rows, scope, tenancy) {
859
1191
  });
860
1192
  return sql.statement();
861
1193
  }
862
- function writeSet(table, value, allow, tenancy) {
1194
+ function writeSet2(table, value, allow, tenancy) {
863
1195
  if (!isRecord(value))
864
- fail("OKM1121", "set must be an object of fields.");
1196
+ fail2("OKM1121", "set must be an object of fields.");
1197
+ const group = traitGroup(table);
1198
+ if (group !== undefined && Object.hasOwn(value, group) && value[group] !== undefined) {
1199
+ fail2("OKM1120", `update on ${table.model.name} cannot change ${group}. It groups position.`, "Move stays in one group.");
1200
+ }
865
1201
  const set = {};
866
1202
  for (const key of Object.keys(value)) {
867
1203
  if (value[key] === undefined)
@@ -874,20 +1210,116 @@ function writeSet(table, value, allow, tenancy) {
874
1210
  refuse(table, column);
875
1211
  set[key] = value[key];
876
1212
  }
1213
+ refuseHalfMorph(table, set);
877
1214
  return set;
878
1215
  }
1216
+ function refuseTreeCycle(table, where, set, scopeKey) {
1217
+ const own = keyedId(table, where, scopeKey);
1218
+ if (own === undefined)
1219
+ return;
1220
+ for (const relation of table.model.relations) {
1221
+ if (relation.kind !== "many" || relation.through !== undefined || relation.morph !== undefined) {
1222
+ continue;
1223
+ }
1224
+ if (relation.table !== table.model.name)
1225
+ continue;
1226
+ for (const sql of relation.remote) {
1227
+ const column = columnBySql(table, sql);
1228
+ if (column === undefined)
1229
+ continue;
1230
+ const value = set[column.field];
1231
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint") {
1232
+ continue;
1233
+ }
1234
+ if ((typeof value === "string" ? value : value.toString()) !== own)
1235
+ continue;
1236
+ fail2("OKM1120", `update on ${table.model.name}.${column.field} cannot point a tree row at itself.`, "Set the parent to another row. A filter that is not the primary key is not checked.");
1237
+ }
1238
+ }
1239
+ }
1240
+ function keyedId(table, where, scopeKey) {
1241
+ if (!isRecord(where))
1242
+ return;
1243
+ const primary = table.model.primary;
1244
+ const required = primary.filter((field) => field !== scopeKey);
1245
+ const field = required.length === 1 ? required[0] : undefined;
1246
+ if (field === undefined || !Object.hasOwn(where, field))
1247
+ return;
1248
+ for (const key of Object.keys(where)) {
1249
+ if (!primary.includes(key))
1250
+ return;
1251
+ }
1252
+ const value = where[field];
1253
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint") {
1254
+ return;
1255
+ }
1256
+ return String(value);
1257
+ }
1258
+ function columnBySql(table, sql) {
1259
+ for (const column of table.model.columns) {
1260
+ if (column.sql === sql)
1261
+ return column;
1262
+ }
1263
+ return;
1264
+ }
879
1265
  function updateItem(table, value, allow, tenancy) {
880
1266
  if (!isRecord(value))
881
- fail("OKM1121", "update expects { where, set } or a list of rows.");
882
- rejectUpdateKeys(value, true);
883
- const set = writeSet(table, value.set, allow, tenancy);
1267
+ fail2("OKM1121", "update expects { where, set } or a list of rows.");
1268
+ rejectUpdateKeys2(value, true);
1269
+ const set = writeSet2(table, value.set, allow, tenancy);
1270
+ if (Object.keys(set).length === 0)
1271
+ fail2("OKM1120", "update set is empty.");
884
1272
  if (value.where !== undefined)
885
1273
  return { where: value.where, set };
886
1274
  if (value.id !== undefined)
887
1275
  return { where: { id: value.id }, set };
888
- fail("OKM1102", `update on ${table.model.name} needs id or where for each row.`);
1276
+ fail2("OKM1102", `update on ${table.model.name} needs id or where for each row.`);
889
1277
  }
890
- function rejectUpdateKeys(input, row) {
1278
+ function rejectMixedSets2(rows) {
1279
+ const kind = new Map;
1280
+ for (const row of rows) {
1281
+ for (const key of Object.keys(row.set)) {
1282
+ const value = row.set[key];
1283
+ if (value === undefined)
1284
+ continue;
1285
+ const next = isOperator(value) ? operatorName(value) : "value";
1286
+ const previous = kind.get(key);
1287
+ if (previous === undefined) {
1288
+ kind.set(key, next);
1289
+ continue;
1290
+ }
1291
+ if (previous === next)
1292
+ continue;
1293
+ const left = previous === "value" ? "a value" : previous;
1294
+ const right = next === "value" ? "a value" : next;
1295
+ fail2("OKM1120", `${key} is both ${left} and ${right} in this update.`, `Use one value or one operator for ${key} in every row.`);
1296
+ }
1297
+ }
1298
+ }
1299
+ function readClaim2(input) {
1300
+ if (Object.hasOwn(input, "wait")) {
1301
+ fail2("OKM1120", 'wait belongs on a read. An update claims rows with lock: "skip".', 'Use lock: "skip" and limit. A read uses wait: "nowait" or "skip" inside tx().');
1302
+ }
1303
+ const lock = input.lock;
1304
+ const limit = input.limit;
1305
+ if (lock === undefined && limit === undefined)
1306
+ return;
1307
+ if (lock === undefined) {
1308
+ fail2("OKM1120", 'limit on an update needs lock: "skip".', 'Pass lock: "skip" to claim up to limit rows, or remove limit.');
1309
+ }
1310
+ if (lock !== "skip") {
1311
+ const shown = typeof lock === "string" ? lock : JSON.stringify(lock) ?? "undefined";
1312
+ fail2("OKM1120", `lock on an update must be "skip". ${shown} is not a claim.`, 'Pass lock: "skip". A read uses lock: "update" or "share", and wait: "nowait" or "skip", inside tx().');
1313
+ }
1314
+ if (limit === undefined) {
1315
+ fail2("OKM1120", 'lock: "skip" needs a limit.', "Pass limit so the claim takes a fixed number of rows.");
1316
+ }
1317
+ if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > 2147483647) {
1318
+ fail2("OKM1121", "limit must be a whole number from 1 to 2147483647.");
1319
+ }
1320
+ return limit;
1321
+ }
1322
+ function rejectUpdateKeys2(input, row) {
891
1323
  const accepted = row ? UPDATE_ROW_KEYS : UPDATE_KEYS;
892
1324
  for (const key of Object.keys(input)) {
893
1325
  if (accepted.includes(key))
@@ -895,15 +1327,15 @@ function rejectUpdateKeys(input, row) {
895
1327
  throwNamed2("OKM1120", key, [...accepted, ...FILTER_OPTIONS], `Option ${key} is not accepted by update. Accepted options: ${accepted.join(", ")}. timeout and signal go in the second argument.`);
896
1328
  }
897
1329
  }
898
- function requireFilter(table, where, mods, op) {
1330
+ function requireFilter2(table, where, mods, op) {
899
1331
  if (mods.all !== undefined) {
900
1332
  if (mods.all.trim().length === 0) {
901
- fail("OKM1102", `${op} on ${table.model.name} needs a where. .all needs a reason.`);
1333
+ fail2("OKM1102", `${op} on ${table.model.name} needs a where. .all needs a reason.`);
902
1334
  }
903
1335
  return;
904
1336
  }
905
1337
  if (!effectivePredicate(where)) {
906
- fail("OKM1102", `${op} on ${table.model.name} needs a where. Pass a filter, or .all(reason).`);
1338
+ fail2("OKM1102", `${op} on ${table.model.name} needs a where. Pass a filter, or .all(reason).`);
907
1339
  }
908
1340
  }
909
1341
  function chunkRows(rows) {
@@ -915,7 +1347,7 @@ function chunkRows(rows) {
915
1347
  for (const cell of row)
916
1348
  if (cell.kind !== "default")
917
1349
  cost += 1;
918
- if (current.length > 0 && count + cost > WRITE_PARAM_BUDGET) {
1350
+ if (current.length > 0 && count + cost > WRITE_PARAM_BUDGET2) {
919
1351
  chunks.push(current);
920
1352
  current = [];
921
1353
  count = 0;
@@ -940,9 +1372,11 @@ function keyWire(columns, row) {
940
1372
  return encodeColumn(column, value);
941
1373
  });
942
1374
  }
943
- function returningOption(table, value) {
1375
+ function returningOption2(table, value) {
944
1376
  if (value === undefined)
945
1377
  return;
1378
+ if (value === true)
1379
+ return selectedColumns(table, undefined);
946
1380
  return selectedColumns(table, value);
947
1381
  }
948
1382
  function returningClause(columns, alias) {
@@ -965,7 +1399,7 @@ function outputsOf(columns) {
965
1399
  includes: []
966
1400
  };
967
1401
  }
968
- function expectOf(options, mods) {
1402
+ function expectOf2(options, mods) {
969
1403
  const value = mods.expect ?? options.expect;
970
1404
  if (value === undefined)
971
1405
  return;
@@ -997,17 +1431,49 @@ function callOptions(options) {
997
1431
  ...hasTimeout ? { timeout } : {}
998
1432
  };
999
1433
  }
1434
+ function refuseHalfMorph(table, row) {
1435
+ const relations = table.model.relations;
1436
+ for (let index = 0;index < relations.length; index += 1) {
1437
+ const relation = relations[index];
1438
+ const morph = relation?.morph;
1439
+ if (relation === undefined || morph === undefined || morph.direction !== "to")
1440
+ continue;
1441
+ const typeSet = Object.hasOwn(row, morph.typeField);
1442
+ const idSet = Object.hasOwn(row, morph.idField);
1443
+ const type = typeSet ? row[morph.typeField] : undefined;
1444
+ const id = idSet ? row[morph.idField] : undefined;
1445
+ const typeClear = type === null;
1446
+ const idClear = id === null;
1447
+ if (!typeSet && !idSet)
1448
+ continue;
1449
+ if (typeSet !== idSet || typeClear !== idClear) {
1450
+ fail2("OKM1121", `Set ${table.model.name}.${morph.typeField} and ${morph.idField} together, or clear both. Use morphTo("${morph.name}", type, id).`);
1451
+ }
1452
+ if (typeClear)
1453
+ continue;
1454
+ let accepted = false;
1455
+ const names = [];
1456
+ for (const target of morph.targets) {
1457
+ names.push(target.type);
1458
+ if (target.type === type)
1459
+ accepted = true;
1460
+ }
1461
+ if (!accepted) {
1462
+ fail2("OKM1120", `Relation ${table.model.name}.${relation.name} type ${morph.typeField} is ${typeof type === "string" ? type : typeof type}. Accepted tables: ${names.join(", ")}.`);
1463
+ }
1464
+ }
1465
+ }
1000
1466
  function refuse(table, column) {
1001
1467
  if (fieldSealed(table.model, column.field)) {
1002
- fail("OKM1190", `Field ${table.model.name}.${column.field} is set by a trait. Input cannot set it.`);
1468
+ fail2("OKM1190", `Field ${table.model.name}.${column.field} is set by a trait. Input cannot set it.`);
1003
1469
  }
1004
1470
  if (column.guardUpdate) {
1005
- fail("OKM1190", `Field ${table.model.name}.${column.field} is a primary key. Input cannot change it.`);
1471
+ fail2("OKM1190", `Field ${table.model.name}.${column.field} is a primary key. Input cannot change it.`);
1006
1472
  }
1007
1473
  if (column.guarded) {
1008
- fail("OKM1190", `Field ${table.model.name}.${column.field} is guarded. Input cannot set it.`);
1474
+ fail2("OKM1190", `Field ${table.model.name}.${column.field} is guarded. Input cannot set it.`);
1009
1475
  }
1010
- fail("OKM1120", `Field ${table.model.name}.${column.field} cannot be written.`);
1476
+ fail2("OKM1120", `Field ${table.model.name}.${column.field} cannot be written.`);
1011
1477
  }
1012
1478
  function writable(table, column, allow) {
1013
1479
  if (fieldSealed(table.model, column.field))
@@ -1016,15 +1482,15 @@ function writable(table, column, allow) {
1016
1482
  return true;
1017
1483
  return column.guarded && allow.has(column.field);
1018
1484
  }
1019
- function allowSet(table, value) {
1485
+ function allowSet2(table, value) {
1020
1486
  if (value === undefined)
1021
1487
  return NO_ALLOW;
1022
1488
  if (!Array.isArray(value))
1023
- fail("OKM1120", "allow must be a list of field names.");
1489
+ fail2("OKM1120", "allow must be a list of field names.");
1024
1490
  const set = new Set;
1025
1491
  for (const name of value) {
1026
1492
  if (typeof name !== "string" || !table.columns.has(name)) {
1027
- fail("OKM1120", "allow must name fields on the table.");
1493
+ fail2("OKM1120", "allow must name fields on the table.");
1028
1494
  }
1029
1495
  set.add(name);
1030
1496
  }
@@ -1038,7 +1504,7 @@ function noteArchive(table, view) {
1038
1504
  return;
1039
1505
  runSafety(rules, undefined);
1040
1506
  }
1041
- function noteGuarded(table, allow, op) {
1507
+ function noteGuarded2(table, allow, op) {
1042
1508
  if (!safetyInstalled())
1043
1509
  return;
1044
1510
  const contributions = [];
@@ -1141,8 +1607,5 @@ function collectSecrets(value, names, found, key) {
1141
1607
  for (const child of Object.keys(value))
1142
1608
  collectSecrets(value[child], names, found, child);
1143
1609
  }
1144
- export {
1145
- WRITE_PARAM_BUDGET,
1146
- executeWrite,
1147
- explainWrite
1148
- };
1610
+
1611
+ export { touchFields, fieldSealed, WRITE_PARAM_BUDGET2, executeWrite2, explainWrite2, updateSliceBody2, rejectDuplicateTargets2, emitSet2, Sql2, writeSet2, rejectMixedSets2, readClaim2, rejectUpdateKeys2, requireFilter2, returningOption2, expectOf2, allowSet2, noteGuarded2 };