@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.
- package/ARCHITECTURE.md +420 -71
- package/README.md +711 -79
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +309 -45
- package/docs/LIVE-FORMAT.md +156 -19
- package/docs/MIGRATION-FORMAT.md +247 -40
- package/docs/MODEL-FORMAT.md +968 -86
- package/package.json +21 -8
- package/schemas/jaren-migration.draft-07.schema.json +73 -0
- package/schemas/jaren-migration.schema.json +73 -0
- package/schemas/jaren-model.draft-07.schema.json +224 -162
- package/schemas/jaren-model.schema.json +224 -162
- package/src/algebra.js +227 -9
- package/src/backup.js +161 -0
- package/src/cancellation.js +48 -0
- package/src/capture.js +255 -44
- package/src/cli.js +337 -50
- package/src/cursor.js +411 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +125 -11
- package/src/dialect.js +267 -112
- package/src/dialects/expression-read.js +158 -0
- package/src/dialects/postgres.js +618 -0
- package/src/dialects/rtree-ddl.js +129 -0
- package/src/dialects/sqlite.js +245 -12
- package/src/document-files.js +311 -0
- package/src/document-steps.js +422 -0
- package/src/documents.js +335 -0
- package/src/driver.js +503 -69
- package/src/drivers/bun.js +37 -1
- package/src/drivers/indexeddb-snapshot.js +149 -0
- package/src/drivers/node-pool.js +11 -0
- package/src/drivers/node-worker-endpoint.js +105 -0
- package/src/drivers/node-worker.js +204 -0
- package/src/drivers/node.js +41 -7
- package/src/drivers/postgres.js +331 -0
- package/src/drivers/wasm-oo1.js +97 -0
- package/src/drivers/wasm-session.js +67 -0
- package/src/drivers/wasm.js +18 -83
- package/src/drivers/worker-pool.js +183 -0
- package/src/drivers/worker-protocol.js +79 -0
- package/src/drivers/worker-queue.js +60 -0
- package/src/emit-model.js +14 -0
- package/src/emit.js +349 -51
- package/src/entity.js +102 -59
- package/src/errors.js +430 -2
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +48 -19
- package/src/introspect.js +583 -0
- package/src/jobs.js +870 -99
- package/src/json-bytes.js +58 -0
- package/src/live-time.js +12 -3
- package/src/live.js +11 -1
- package/src/maintenance.js +175 -0
- package/src/migrate.js +606 -333
- package/src/model.js +241 -8
- package/src/plan.js +1238 -160
- package/src/pragmas.js +314 -0
- package/src/profile.js +151 -3
- package/src/query.js +1748 -312
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1672 -276
- package/src/tracker.js +367 -68
- package/src/udf.js +88 -7
- package/types/index.d.ts +1246 -32
- package/types/node-pool.d.ts +28 -0
- package/types/node-worker.d.ts +54 -0
- package/types/node.d.ts +72 -3
- package/types/postgres.d.ts +46 -0
- package/types/typed.d.ts +81 -3
- package/types/wasm.d.ts +21 -0
- package/dist/types/algebra.d.ts +0 -230
- package/dist/types/app.d.ts +0 -49
- package/dist/types/capture.d.ts +0 -85
- package/dist/types/cli.d.ts +0 -2
- package/dist/types/dag-job.d.ts +0 -40
- package/dist/types/ddl.d.ts +0 -229
- package/dist/types/derive.d.ts +0 -250
- package/dist/types/dialect.d.ts +0 -154
- package/dist/types/dialects/sqlite.d.ts +0 -9
- package/dist/types/driver.d.ts +0 -110
- package/dist/types/drivers/bun.d.ts +0 -47
- package/dist/types/drivers/node.d.ts +0 -37
- package/dist/types/drivers/wasm.d.ts +0 -65
- package/dist/types/emit-model.d.ts +0 -44
- package/dist/types/emit.d.ts +0 -75
- package/dist/types/entity.d.ts +0 -23
- package/dist/types/errors.d.ts +0 -170
- package/dist/types/graph.d.ts +0 -28
- package/dist/types/index.d.ts +0 -37
- package/dist/types/jobs.d.ts +0 -140
- package/dist/types/knn.d.ts +0 -69
- package/dist/types/live-time.d.ts +0 -141
- package/dist/types/live.d.ts +0 -64
- package/dist/types/migrate.d.ts +0 -170
- package/dist/types/model.d.ts +0 -36
- package/dist/types/patch-sql.d.ts +0 -37
- package/dist/types/plan.d.ts +0 -142
- package/dist/types/profile.d.ts +0 -80
- package/dist/types/query.d.ts +0 -112
- package/dist/types/residual.d.ts +0 -64
- package/dist/types/series.d.ts +0 -227
- package/dist/types/store.d.ts +0 -60
- package/dist/types/tracker.d.ts +0 -43
- package/dist/types/typed.d.ts +0 -15
- package/dist/types/types.d.ts +0 -26
- package/dist/types/udf.d.ts +0 -75
- 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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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: () =>
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
357
|
-
|
|
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
|
}
|