@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.
Files changed (72) hide show
  1. package/ARCHITECTURE.md +133 -17
  2. package/README.md +270 -36
  3. package/docs/JOBS-FORMAT.md +24 -8
  4. package/docs/LIVE-FORMAT.md +139 -7
  5. package/docs/MIGRATION-FORMAT.md +118 -36
  6. package/docs/MODEL-FORMAT.md +251 -30
  7. package/package.json +4 -5
  8. package/schemas/jaren-migration.draft-07.schema.json +73 -0
  9. package/schemas/jaren-migration.schema.json +73 -0
  10. package/src/algebra.js +22 -3
  11. package/src/capture.js +66 -28
  12. package/src/cli.js +225 -44
  13. package/src/ddl.js +23 -3
  14. package/src/dialect.js +13 -0
  15. package/src/dialects/sqlite.js +21 -1
  16. package/src/driver.js +63 -16
  17. package/src/drivers/wasm.js +1 -0
  18. package/src/emit-model.js +14 -0
  19. package/src/emit.js +42 -9
  20. package/src/entity.js +92 -47
  21. package/src/errors.js +28 -0
  22. package/src/index.js +2 -2
  23. package/src/jobs.js +40 -5
  24. package/src/live-time.js +605 -0
  25. package/src/live.js +52 -9
  26. package/src/migrate.js +397 -191
  27. package/src/model.js +173 -8
  28. package/src/plan.js +834 -47
  29. package/src/query.js +296 -22
  30. package/src/residual.js +15 -6
  31. package/src/series.js +349 -0
  32. package/src/store.js +243 -69
  33. package/src/tracker.js +173 -48
  34. package/types/index.d.ts +206 -12
  35. package/types/node.d.ts +3 -1
  36. package/types/typed.d.ts +58 -2
  37. package/types/wasm.d.ts +7 -0
  38. package/dist/types/algebra.d.ts +0 -199
  39. package/dist/types/app.d.ts +0 -49
  40. package/dist/types/capture.d.ts +0 -85
  41. package/dist/types/cli.d.ts +0 -2
  42. package/dist/types/dag-job.d.ts +0 -40
  43. package/dist/types/ddl.d.ts +0 -229
  44. package/dist/types/derive.d.ts +0 -250
  45. package/dist/types/dialect.d.ts +0 -149
  46. package/dist/types/dialects/sqlite.d.ts +0 -9
  47. package/dist/types/driver.d.ts +0 -110
  48. package/dist/types/drivers/bun.d.ts +0 -47
  49. package/dist/types/drivers/node.d.ts +0 -37
  50. package/dist/types/drivers/wasm.d.ts +0 -65
  51. package/dist/types/emit-model.d.ts +0 -44
  52. package/dist/types/emit.d.ts +0 -75
  53. package/dist/types/entity.d.ts +0 -23
  54. package/dist/types/errors.d.ts +0 -167
  55. package/dist/types/graph.d.ts +0 -28
  56. package/dist/types/index.d.ts +0 -37
  57. package/dist/types/jobs.d.ts +0 -140
  58. package/dist/types/knn.d.ts +0 -69
  59. package/dist/types/live.d.ts +0 -62
  60. package/dist/types/migrate.d.ts +0 -170
  61. package/dist/types/model.d.ts +0 -36
  62. package/dist/types/patch-sql.d.ts +0 -37
  63. package/dist/types/plan.d.ts +0 -140
  64. package/dist/types/profile.d.ts +0 -80
  65. package/dist/types/query.d.ts +0 -111
  66. package/dist/types/residual.d.ts +0 -61
  67. package/dist/types/store.d.ts +0 -53
  68. package/dist/types/tracker.d.ts +0 -43
  69. package/dist/types/typed.d.ts +0 -15
  70. package/dist/types/types.d.ts +0 -26
  71. package/dist/types/udf.d.ts +0 -75
  72. 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]] === '$[*]' ? names[0] : null;
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 strategy = classification.strategy === 'rows' ? rowsStrategy(classification, context)
870
- : classification.strategy === 'window' ? windowStrategy(classification, context)
871
- : classification.strategy === 'accumulator'
872
- ? accumulatorStrategy(classification, context)
873
- : classification.strategy === 'group' ? groupStrategy(classification, context)
874
- : rerunStrategy(classification, context);
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);