@jarenjs/db 0.46.5 → 0.56.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.
- package/ARCHITECTURE.md +133 -17
- package/README.md +270 -36
- package/docs/JOBS-FORMAT.md +24 -8
- package/docs/LIVE-FORMAT.md +139 -7
- package/docs/MIGRATION-FORMAT.md +118 -36
- package/docs/MODEL-FORMAT.md +251 -30
- package/package.json +4 -5
- package/schemas/jaren-migration.draft-07.schema.json +73 -0
- package/schemas/jaren-migration.schema.json +73 -0
- package/src/algebra.js +22 -3
- package/src/capture.js +66 -28
- package/src/cli.js +225 -44
- package/src/ddl.js +23 -3
- package/src/dialect.js +13 -0
- package/src/dialects/sqlite.js +21 -1
- package/src/driver.js +63 -16
- package/src/drivers/wasm.js +1 -0
- package/src/emit-model.js +14 -0
- package/src/emit.js +42 -9
- package/src/entity.js +92 -47
- package/src/errors.js +28 -0
- package/src/index.js +2 -2
- package/src/jobs.js +40 -5
- package/src/live-time.js +605 -0
- package/src/live.js +52 -9
- package/src/migrate.js +397 -191
- package/src/model.js +173 -8
- package/src/plan.js +834 -47
- package/src/query.js +296 -22
- package/src/residual.js +15 -6
- package/src/series.js +349 -0
- package/src/store.js +243 -69
- package/src/tracker.js +173 -48
- package/types/index.d.ts +206 -12
- package/types/node.d.ts +3 -1
- package/types/typed.d.ts +58 -2
- package/types/wasm.d.ts +7 -0
- package/dist/types/algebra.d.ts +0 -199
- 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 -149
- 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 -167
- 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.d.ts +0 -62
- 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 -140
- package/dist/types/profile.d.ts +0 -80
- package/dist/types/query.d.ts +0 -111
- package/dist/types/residual.d.ts +0 -61
- package/dist/types/store.d.ts +0 -53
- 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/live.js
CHANGED
|
@@ -26,6 +26,7 @@ import { DbCompileError, DbRuntimeError } from './errors.js';
|
|
|
26
26
|
import { chain } from './driver.js';
|
|
27
27
|
import { planQuery } from './plan.js';
|
|
28
28
|
import { createSortedWindow } from './window.js';
|
|
29
|
+
import { classifyEventTime, bucketStrategy, rollingStrategy } from './live-time.js';
|
|
29
30
|
|
|
30
31
|
/** The store-level live bounds and their defaults (§12: printed,
|
|
31
32
|
* never silent). */
|
|
@@ -78,12 +79,22 @@ function unwrapDocument(document) {
|
|
|
78
79
|
return { inner: doc, whole, windowed, offset, limit, aggregate };
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
/** Whether a `$for` source is the whole collection: the bare `$[*]`, or
|
|
83
|
+
* its packed spelling `["$[*]"]` — one array item a `$for` unpacks back
|
|
84
|
+
* into the rows, which is how `@jarenjs/linq` binds every source so an
|
|
85
|
+
* array-valued row stays one item. Over a collection the two are the
|
|
86
|
+
* same rows (the planner reads through the packing the same way). */
|
|
87
|
+
function isWholeCollection(source) {
|
|
88
|
+
return source === '$[*]'
|
|
89
|
+
|| (Array.isArray(source) && source.length === 1 && source[0] === '$[*]');
|
|
90
|
+
}
|
|
91
|
+
|
|
81
92
|
/** The single for-binding name of a canonical flwor, or null. */
|
|
82
93
|
function bindingNameOf(inner) {
|
|
83
94
|
if (!isJsonObject(inner) || !isJsonObject(inner.$for)) return null;
|
|
84
95
|
const names = Object.keys(inner.$for);
|
|
85
96
|
if (names.length !== 1) return null;
|
|
86
|
-
return inner.$for[names[0]]
|
|
97
|
+
return isWholeCollection(inner.$for[names[0]]) ? names[0] : null;
|
|
87
98
|
}
|
|
88
99
|
|
|
89
100
|
/** The whole-documents read behind a flwor: same binding, same
|
|
@@ -238,9 +249,11 @@ const SPATIAL_RERUN = {
|
|
|
238
249
|
* columnByCanonical)
|
|
239
250
|
* @param {boolean} keyed - whether documents carry their key (a
|
|
240
251
|
* declared key pointer); unkeyed rows cannot be tracked by key
|
|
252
|
+
* @param {any} [eventTime] - the normalized `eventTime` option
|
|
253
|
+
* (`live-time.js`), or null when the caller declared none
|
|
241
254
|
* @returns {any}
|
|
242
255
|
*/
|
|
243
|
-
export function classifyLiveQuery(document, queryShape, keyed) {
|
|
256
|
+
export function classifyLiveQuery(document, queryShape, keyed, eventTime = null) {
|
|
244
257
|
const rerun = (reason) => ({ strategy: 'rerun', reason });
|
|
245
258
|
// the reason named is the first one that is NOT a spatial refinement:
|
|
246
259
|
// a refinement narrows and never forces a re-run by itself
|
|
@@ -251,6 +264,12 @@ export function classifyLiveQuery(document, queryShape, keyed) {
|
|
|
251
264
|
};
|
|
252
265
|
const { inner, whole, windowed, offset, limit, aggregate } = unwrapDocument(document);
|
|
253
266
|
|
|
267
|
+
// §13: a document that IS a temporal operator over the collection
|
|
268
|
+
// answers to event time or re-runs, and never to the §7 table — the
|
|
269
|
+
// planner's own residual for it is a fetch, not a maintainable shape
|
|
270
|
+
const temporal = classifyEventTime(inner, windowed, keyed, eventTime);
|
|
271
|
+
if (temporal !== null) return temporal;
|
|
272
|
+
|
|
254
273
|
if (aggregate !== null) {
|
|
255
274
|
if (windowed) return rerun('a windowed aggregate maintains no accumulator');
|
|
256
275
|
const planned = planQuery({ [aggregate.name]: inner }, queryShape, {});
|
|
@@ -865,13 +884,17 @@ export function createLiveRegistry(bounds) {
|
|
|
865
884
|
execute: definition.execute,
|
|
866
885
|
readRow: definition.readRow,
|
|
867
886
|
keyOf: definition.keyOf,
|
|
887
|
+
// §8's touched-key reader, handed to the strategies rather than
|
|
888
|
+
// imported by them: `live-time.js` maintains its own state and
|
|
889
|
+
// must not become a second implementation of the pointer walk
|
|
890
|
+
touchedKeys: (record, deps) => touchedKeys(record, definition.name, deps),
|
|
868
891
|
};
|
|
869
|
-
const
|
|
870
|
-
:
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
892
|
+
const STRATEGIES = {
|
|
893
|
+
rows: rowsStrategy, window: windowStrategy, accumulator: accumulatorStrategy,
|
|
894
|
+
group: groupStrategy, bucket: bucketStrategy, rolling: rollingStrategy,
|
|
895
|
+
};
|
|
896
|
+
const strategy = (STRATEGIES[classification.strategy] ?? rerunStrategy)(
|
|
897
|
+
classification, context);
|
|
875
898
|
|
|
876
899
|
/** @type {Set<Function>} */
|
|
877
900
|
const observers = new Set();
|
|
@@ -906,6 +929,19 @@ export function createLiveRegistry(bounds) {
|
|
|
906
929
|
let outcome;
|
|
907
930
|
try {
|
|
908
931
|
outcome = strategy.apply(record, state.result.rows);
|
|
932
|
+
if (outcome !== null && outcome.ops === undefined) {
|
|
933
|
+
// §13: a reading behind the lateness boundary is never
|
|
934
|
+
// folded in silently — the view re-reads from the store,
|
|
935
|
+
// and the emission carries the reason EVEN when the rows
|
|
936
|
+
// did not move, because "nothing changed" is exactly what
|
|
937
|
+
// a reader must not conclude on its own here
|
|
938
|
+
const late = outcome.rebuild === true;
|
|
939
|
+
const fresh = late ? strategy.rebuild() : outcome.rows;
|
|
940
|
+
const rows = shareByValue(state.result.rows, fresh);
|
|
941
|
+
const ops = diffRows(state.result.rows, rows);
|
|
942
|
+
outcome = ops.length === 0 && !late ? null
|
|
943
|
+
: { ops, rows, ...(late ? { late: outcome.late } : {}) };
|
|
944
|
+
}
|
|
909
945
|
if (outcome !== null) checkBound(strategy.entries(outcome.rows));
|
|
910
946
|
}
|
|
911
947
|
catch (error) {
|
|
@@ -926,7 +962,8 @@ export function createLiveRegistry(bounds) {
|
|
|
926
962
|
state.stats.matched += 1;
|
|
927
963
|
state.stats.emissions += 1;
|
|
928
964
|
state.result = { rows: outcome.rows };
|
|
929
|
-
const event = { patch: outcome.ops, seq: record.seq
|
|
965
|
+
const event = { patch: outcome.ops, seq: record.seq,
|
|
966
|
+
...(outcome.late === undefined ? {} : { lateData: outcome.late }) };
|
|
930
967
|
for (const observer of observers) {
|
|
931
968
|
try {
|
|
932
969
|
observer(event);
|
|
@@ -956,6 +993,12 @@ export function createLiveRegistry(bounds) {
|
|
|
956
993
|
get error() { return state.error; },
|
|
957
994
|
mode,
|
|
958
995
|
stats: () => ({ ...state.stats, ...(strategy.stats?.() ?? {}) }),
|
|
996
|
+
...(strategy.advance === undefined ? {} : {
|
|
997
|
+
advance(watermark) {
|
|
998
|
+
if (state.status !== 'live') throw new TypeError('the live query is closed');
|
|
999
|
+
strategy.advance(watermark);
|
|
1000
|
+
},
|
|
1001
|
+
}),
|
|
959
1002
|
subscribe(observer) {
|
|
960
1003
|
if (state.status !== 'live') throw new TypeError('the live query is closed');
|
|
961
1004
|
observers.add(observer);
|