@jarenjs/db 0.49.2 → 0.66.1

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 (110) hide show
  1. package/ARCHITECTURE.md +420 -71
  2. package/README.md +711 -79
  3. package/docs/HOSTS.md +269 -0
  4. package/docs/JOBS-FORMAT.md +309 -45
  5. package/docs/LIVE-FORMAT.md +156 -19
  6. package/docs/MIGRATION-FORMAT.md +247 -40
  7. package/docs/MODEL-FORMAT.md +968 -86
  8. package/package.json +21 -8
  9. package/schemas/jaren-migration.draft-07.schema.json +73 -0
  10. package/schemas/jaren-migration.schema.json +73 -0
  11. package/schemas/jaren-model.draft-07.schema.json +224 -162
  12. package/schemas/jaren-model.schema.json +224 -162
  13. package/src/algebra.js +227 -9
  14. package/src/backup.js +161 -0
  15. package/src/cancellation.js +48 -0
  16. package/src/capture.js +255 -44
  17. package/src/cli.js +337 -50
  18. package/src/cursor.js +411 -0
  19. package/src/dag-job.js +154 -21
  20. package/src/ddl.js +125 -11
  21. package/src/dialect.js +267 -112
  22. package/src/dialects/expression-read.js +158 -0
  23. package/src/dialects/postgres.js +618 -0
  24. package/src/dialects/rtree-ddl.js +129 -0
  25. package/src/dialects/sqlite.js +245 -12
  26. package/src/document-files.js +311 -0
  27. package/src/document-steps.js +422 -0
  28. package/src/documents.js +335 -0
  29. package/src/driver.js +503 -69
  30. package/src/drivers/bun.js +37 -1
  31. package/src/drivers/indexeddb-snapshot.js +149 -0
  32. package/src/drivers/node-pool.js +11 -0
  33. package/src/drivers/node-worker-endpoint.js +105 -0
  34. package/src/drivers/node-worker.js +204 -0
  35. package/src/drivers/node.js +41 -7
  36. package/src/drivers/postgres.js +331 -0
  37. package/src/drivers/wasm-oo1.js +97 -0
  38. package/src/drivers/wasm-session.js +67 -0
  39. package/src/drivers/wasm.js +18 -83
  40. package/src/drivers/worker-pool.js +183 -0
  41. package/src/drivers/worker-protocol.js +79 -0
  42. package/src/drivers/worker-queue.js +60 -0
  43. package/src/emit-model.js +14 -0
  44. package/src/emit.js +349 -51
  45. package/src/entity.js +102 -59
  46. package/src/errors.js +430 -2
  47. package/src/expression.js +284 -0
  48. package/src/graph.js +64 -8
  49. package/src/index.js +48 -19
  50. package/src/introspect.js +583 -0
  51. package/src/jobs.js +870 -99
  52. package/src/json-bytes.js +58 -0
  53. package/src/live-time.js +12 -3
  54. package/src/live.js +11 -1
  55. package/src/maintenance.js +175 -0
  56. package/src/migrate.js +606 -333
  57. package/src/model.js +241 -8
  58. package/src/plan.js +1238 -160
  59. package/src/pragmas.js +314 -0
  60. package/src/profile.js +151 -3
  61. package/src/query.js +1748 -312
  62. package/src/residual.js +17 -0
  63. package/src/series.js +12 -4
  64. package/src/store.js +1672 -276
  65. package/src/tracker.js +367 -68
  66. package/src/udf.js +88 -7
  67. package/types/index.d.ts +1246 -32
  68. package/types/node-pool.d.ts +28 -0
  69. package/types/node-worker.d.ts +54 -0
  70. package/types/node.d.ts +72 -3
  71. package/types/postgres.d.ts +46 -0
  72. package/types/typed.d.ts +81 -3
  73. package/types/wasm.d.ts +21 -0
  74. package/dist/types/algebra.d.ts +0 -230
  75. package/dist/types/app.d.ts +0 -49
  76. package/dist/types/capture.d.ts +0 -85
  77. package/dist/types/cli.d.ts +0 -2
  78. package/dist/types/dag-job.d.ts +0 -40
  79. package/dist/types/ddl.d.ts +0 -229
  80. package/dist/types/derive.d.ts +0 -250
  81. package/dist/types/dialect.d.ts +0 -154
  82. package/dist/types/dialects/sqlite.d.ts +0 -9
  83. package/dist/types/driver.d.ts +0 -110
  84. package/dist/types/drivers/bun.d.ts +0 -47
  85. package/dist/types/drivers/node.d.ts +0 -37
  86. package/dist/types/drivers/wasm.d.ts +0 -65
  87. package/dist/types/emit-model.d.ts +0 -44
  88. package/dist/types/emit.d.ts +0 -75
  89. package/dist/types/entity.d.ts +0 -23
  90. package/dist/types/errors.d.ts +0 -170
  91. package/dist/types/graph.d.ts +0 -28
  92. package/dist/types/index.d.ts +0 -37
  93. package/dist/types/jobs.d.ts +0 -140
  94. package/dist/types/knn.d.ts +0 -69
  95. package/dist/types/live-time.d.ts +0 -141
  96. package/dist/types/live.d.ts +0 -64
  97. package/dist/types/migrate.d.ts +0 -170
  98. package/dist/types/model.d.ts +0 -36
  99. package/dist/types/patch-sql.d.ts +0 -37
  100. package/dist/types/plan.d.ts +0 -142
  101. package/dist/types/profile.d.ts +0 -80
  102. package/dist/types/query.d.ts +0 -112
  103. package/dist/types/residual.d.ts +0 -64
  104. package/dist/types/series.d.ts +0 -227
  105. package/dist/types/store.d.ts +0 -60
  106. package/dist/types/tracker.d.ts +0 -43
  107. package/dist/types/typed.d.ts +0 -15
  108. package/dist/types/types.d.ts +0 -26
  109. package/dist/types/udf.d.ts +0 -75
  110. package/dist/types/window.d.ts +0 -52
package/src/entity.js CHANGED
@@ -18,9 +18,10 @@ import { compileJsonQuery } from '@jarenjs/json/query';
18
18
  import {
19
19
  getEpochOfDateTimeRFC3339, getEpochOfDateOnlyRFC3339,
20
20
  } from '@jarenjs/core/dates/rfc3339';
21
+ import { resolveRuntime } from '@jarenjs/core/runtime';
21
22
 
22
- import { DbRuntimeError } from './errors.js';
23
- import { chain } from './driver.js';
23
+ import { DbRuntimeError, wrapDriverError } from './errors.js';
24
+ import { chain, attempt } from './driver.js';
24
25
 
25
26
  /**
26
27
  * The write/read machinery for one entity, prepared once.
@@ -28,9 +29,13 @@ import { chain } from './driver.js';
28
29
  * @param {any} entity - the normalized entity (model.js)
29
30
  * @param {any} entityMapping - `explainMapping(...).entities[name]`
30
31
  * @param {((doc: any) => any) | null} validate
32
+ * @param {Partial<import('@jarenjs/core/runtime').Runtime>} [runtime] - the
33
+ * host's runtime record: the clock a `default: 'now'` stamps and the
34
+ * identifier a `default: 'uuid'` allocates; the platform's own when absent
31
35
  * @returns {any}
32
36
  */
33
- export function entityCore(connection, entity, entityMapping, validate) {
37
+ export function entityCore(connection, entity, entityMapping, validate, runtime = undefined) {
38
+ const host = resolveRuntime(runtime);
34
39
  const dialect = connection.dialect;
35
40
  const q = dialect.quoteIdentifier;
36
41
  const table = entityMapping.table;
@@ -132,6 +137,28 @@ export function entityCore(connection, entity, entityMapping, validate) {
132
137
  return doc;
133
138
  };
134
139
 
140
+ /** The document as a read will answer it. For a column-mapped scalar,
141
+ * JSON `null` and absence both store as SQL NULL and read back ABSENT
142
+ * (§9.3) — so a value a write RETURNS drops it too, and the promise
143
+ * this file opens with holds: the value the application sees IS the
144
+ * value stored. A property that needs present-`null` declares
145
+ * `column: "json"` and stays in the document, where it survives; an
146
+ * epoch column keeps its string in the document for the same reason. */
147
+ const asStored = (doc) => {
148
+ let out = doc;
149
+ const drop = (name) => {
150
+ if (!(name in out) || (out[name] !== null && out[name] !== undefined)) return;
151
+ if (out === doc) out = { ...doc };
152
+ delete out[name];
153
+ };
154
+ for (const column of scalarColumns) {
155
+ if (column.epoch) continue; // the string is in the document, null and all
156
+ drop(column.name);
157
+ }
158
+ for (const fk of fkColumns) drop(fk);
159
+ return out;
160
+ };
161
+
135
162
  // defaults, compiled once
136
163
  const defaulters = [];
137
164
  const updateStamps = [];
@@ -139,13 +166,17 @@ export function entityCore(connection, entity, entityMapping, validate) {
139
166
  const declared = property.default;
140
167
  if (declared === undefined || property.relation !== undefined) continue;
141
168
  if (declared === 'now' || declared === 'updated') {
142
- defaulters.push({ name: property.name, fill: () => new Date().toISOString() });
143
- if (declared === 'updated')
144
- updateStamps.push({ name: property.name, fill: () => new Date().toISOString() });
169
+ // a `date` property takes the calendar date; a date-time stamp on
170
+ // it was invalid under its own format and refused by an epoch column
171
+ const stamp = property.format === 'date'
172
+ ? () => new Date(host.now()).toISOString().slice(0, 10)
173
+ : () => new Date(host.now()).toISOString();
174
+ defaulters.push({ name: property.name, fill: stamp });
175
+ if (declared === 'updated') updateStamps.push({ name: property.name, fill: stamp });
145
176
  continue;
146
177
  }
147
178
  if (declared === 'uuid') {
148
- defaulters.push({ name: property.name, fill: () => crypto.randomUUID() });
179
+ defaulters.push({ name: property.name, fill: () => host.uuid() });
149
180
  continue;
150
181
  }
151
182
  if (declared === 'auto') continue; // the database allocates
@@ -162,12 +193,42 @@ export function entityCore(connection, entity, entityMapping, validate) {
162
193
  for (const { name, fill } of defaulters) {
163
194
  if (out[name] === undefined) out[name] = fill(out);
164
195
  }
196
+ // the version token starts at 0 on insert: a row written with SQL
197
+ // NULL never matched the tracker's `WHERE ver = 0` and could not be
198
+ // saved through the unit of work at all
199
+ if (!updating && entity.version !== null && entity.version !== undefined
200
+ && out[entity.version] === undefined) {
201
+ out[entity.version] = 0;
202
+ }
165
203
  if (updating) {
166
204
  for (const { name, fill } of updateStamps) out[name] = fill(out);
167
205
  }
168
206
  return out;
169
207
  };
170
208
 
209
+ /**
210
+ * Refuse relation members a write cannot store: every relation member
211
+ * is a projection (§10.1), except a many-to-many MEMBERSHIP array,
212
+ * which `create()`/`add()` attach through the join table.
213
+ * @param {any} doc
214
+ * @param {string} verb
215
+ * @param {boolean} memberships - whether membership arrays are taken
216
+ */
217
+ const refuseProjections = (doc, verb, memberships) => {
218
+ for (const name of relationNames) {
219
+ const value = doc?.[name];
220
+ if (value === undefined || (Array.isArray(value) && value.length === 0)) continue;
221
+ const relation = entity.properties.get(name).relation;
222
+ if (memberships && relation.kind === 'manyToMany' && Array.isArray(value)) continue;
223
+ throw new DbRuntimeError('JD2003',
224
+ `'${name}' is a relation member — ${verb}() stores no projections; `
225
+ + (relation.kind === 'manyToMany'
226
+ ? 'membership changes through the unit of work (put + saveChanges)'
227
+ : 'write the related entities themselves'),
228
+ { docPath, collection: entity.name });
229
+ }
230
+ };
231
+
171
232
  const checkValid = (doc) => {
172
233
  if (validate === null) return;
173
234
  const outcome = validate(doc);
@@ -187,7 +248,7 @@ export function entityCore(connection, entity, entityMapping, validate) {
187
248
  const prepared = (name, sql) => {
188
249
  let statement = statements.get(name);
189
250
  if (statement === undefined) {
190
- statement = connection.prepare(sql);
251
+ statement = connection.prepare(sql, { readOnly: name === 'get' });
191
252
  statements.set(name, statement);
192
253
  }
193
254
  return statement;
@@ -232,14 +293,11 @@ export function entityCore(connection, entity, entityMapping, validate) {
232
293
  { docPath, collection: entity.name });
233
294
  };
234
295
 
235
- const wrapWrite = (error, key) => {
236
- if (/** @type {any} */ (error)?.code === 'JD2003') return error;
237
- return new DbRuntimeError('JD2005',
238
- `the database rejected the operation: ${/** @type {any} */ (error)?.message ?? String(error)}`,
239
- key === undefined
240
- ? { docPath, collection: entity.name, cause: error }
241
- : { docPath, collection: entity.name, key, cause: error });
242
- };
296
+ const wrapWrite = (error, key) => wrapDriverError(error, {
297
+ docPath, collection: entity.name, ...(key === undefined ? undefined : { key }),
298
+ ...(keys.length === 1 ? { unique: { table, column: keys[0] } } : undefined),
299
+ duplicateReason: `a '${entity.name}' already exists under key ${JSON.stringify(key)}`,
300
+ });
243
301
 
244
302
  const columnByName = new Map(scalarColumns.map((column) => [column.name, column]));
245
303
  /** Encode ONE column assignment the way {@link split} would. */
@@ -269,7 +327,7 @@ export function entityCore(connection, entity, entityMapping, validate) {
269
327
  complete: (doc, { updating }) => {
270
328
  const completed = applyDefaults(doc, { updating });
271
329
  checkValid(completed);
272
- return completed;
330
+ return asStored(completed);
273
331
  },
274
332
  validateOnly: (doc) => checkValid(doc),
275
333
  stampUpdated: (doc) => {
@@ -280,15 +338,7 @@ export function entityCore(connection, entity, entityMapping, validate) {
280
338
  },
281
339
  normalizeKey: (key) => normalizeKeyArg(key),
282
340
  create(doc) {
283
- for (const name of relationNames) {
284
- const value = doc?.[name];
285
- if (value !== undefined && (!Array.isArray(value) || value.length > 0)) {
286
- throw new DbRuntimeError('JD2003',
287
- `'${name}' is a relation member — create() stores no `
288
- + 'projections; use the unit of work for membership',
289
- { docPath, collection: entity.name });
290
- }
291
- }
341
+ refuseProjections(doc, 'create', true);
292
342
  const completed = applyDefaults(doc, { updating: false });
293
343
  checkValid(completed);
294
344
  const { values, rest } = split(completed);
@@ -296,33 +346,39 @@ export function entityCore(connection, entity, entityMapping, validate) {
296
346
  const sql = insertSqlFor(names);
297
347
  return chain(prepared(`insert:${names.join(',')}`, sql), (statement) => {
298
348
  const params = [...values.map((value) => value.value), JSON.stringify(rest)];
299
- let out;
300
- try {
301
- out = autoKey !== null && !names.includes(autoKey)
302
- ? statement.get(params)
303
- : (statement.run(params), null);
304
- }
305
- catch (error) {
306
- throw wrapWrite(error, completed[keys[0]]);
307
- }
308
- if (out !== null) return { ...completed, [autoKey]: out.key };
309
- return completed;
349
+ const returning = autoKey !== null && !names.includes(autoKey);
350
+ return chain(
351
+ attempt(() => (returning ? statement.get(params) : statement.run(params)),
352
+ (error) => wrapWrite(error, completed[keys[0]])),
353
+ (out) => asStored(returning ? { ...completed, [autoKey]: out.key } : completed));
310
354
  });
311
355
  },
312
356
  get(key) {
313
357
  const parts = normalizeKeyArg(key);
314
358
  const sql = `SELECT ${selectColumns} FROM ${q(table)} WHERE ${keyWhere(0)}`;
315
- return chain(prepared('get', sql), (statement) =>
316
- chain(statement.get(parts), (row) => (row === undefined ? undefined : merge(row))));
359
+ // classified like every read of the query engines, never raw
360
+ return attempt(() => chain(prepared('get', sql), (statement) =>
361
+ chain(statement.get(parts), (row) => (row === undefined ? undefined : merge(row)))),
362
+ (error) => wrapDriverError(error, { docPath, collection: entity.name, key }));
317
363
  },
318
364
  update(key, changes) {
319
365
  const parts = normalizeKeyArg(key);
366
+ refuseProjections(changes, 'update', false);
320
367
  return chain(this.get(key), (current) => {
321
368
  if (current === undefined) {
322
369
  throw new DbRuntimeError('JD2006',
323
370
  `no '${entity.name}' to update under that key`,
324
371
  { docPath, collection: entity.name });
325
372
  }
373
+ for (const keyName of keys) {
374
+ // the key identifies the row the UPDATE addresses; rewriting it
375
+ // through `changes` moved rows out from under the tracker
376
+ if (changes?.[keyName] !== undefined && changes[keyName] !== current[keyName]) {
377
+ throw new DbRuntimeError('JD2003',
378
+ `'${keyName}' is the primary key — update() cannot rewrite it; delete and create`,
379
+ { docPath, collection: entity.name, key: parts[0] });
380
+ }
381
+ }
326
382
  const next = applyDefaults({ ...current, ...changes }, { updating: true });
327
383
  // an explicit update is last-write-wins by contract (§11.2),
328
384
  // but it still moves a declared version token so optimistic
@@ -337,31 +393,18 @@ export function entityCore(connection, entity, entityMapping, validate) {
337
393
  ].join(', ');
338
394
  const sql = `UPDATE ${q(table)} SET ${assignments} `
339
395
  + `WHERE ${keyWhere(values.length + 1)}`;
340
- return chain(prepared(`update:${values.length}`, sql), (statement) => {
341
- try {
342
- statement.run([...values.map((value) => value.value),
343
- JSON.stringify(rest), ...parts]);
344
- }
345
- catch (error) {
346
- throw wrapWrite(error, parts[0]);
347
- }
348
- return next;
349
- });
396
+ return chain(prepared(`update:${values.length}`, sql), (statement) =>
397
+ chain(attempt(() => statement.run([...values.map((value) => value.value),
398
+ JSON.stringify(rest), ...parts]), (error) => wrapWrite(error, parts[0])),
399
+ () => asStored(next)));
350
400
  });
351
401
  },
352
402
  delete(key) {
353
403
  const parts = normalizeKeyArg(key);
354
404
  const sql = `DELETE FROM ${q(table)} WHERE ${keyWhere(0)}`;
355
- return chain(prepared('delete', sql), (statement) => {
356
- let out;
357
- try {
358
- out = statement.run(parts);
359
- }
360
- catch (error) {
361
- throw wrapWrite(error, parts[0]);
362
- }
363
- return chain(out, (result) => Number(result?.changes ?? 0) > 0);
364
- });
405
+ return chain(prepared('delete', sql), (statement) =>
406
+ chain(attempt(() => statement.run(parts), (error) => wrapWrite(error, parts[0])),
407
+ (result) => Number(result?.changes ?? 0) > 0));
365
408
  },
366
409
  };
367
410
  }