@jarenjs/db 0.34.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 (83) hide show
  1. package/ARCHITECTURE.md +397 -0
  2. package/README.md +218 -0
  3. package/dist/types/algebra.d.ts +133 -0
  4. package/dist/types/app.d.ts +49 -0
  5. package/dist/types/capture.d.ts +85 -0
  6. package/dist/types/cli.d.ts +2 -0
  7. package/dist/types/dag-job.d.ts +40 -0
  8. package/dist/types/ddl.d.ts +170 -0
  9. package/dist/types/dialect.d.ts +130 -0
  10. package/dist/types/dialects/sqlite.d.ts +9 -0
  11. package/dist/types/driver.d.ts +128 -0
  12. package/dist/types/drivers/bun.d.ts +47 -0
  13. package/dist/types/drivers/node.d.ts +37 -0
  14. package/dist/types/drivers/wasm.d.ts +65 -0
  15. package/dist/types/emit-model.d.ts +44 -0
  16. package/dist/types/emit.d.ts +72 -0
  17. package/dist/types/entity.d.ts +23 -0
  18. package/dist/types/errors.d.ts +165 -0
  19. package/dist/types/graph.d.ts +28 -0
  20. package/dist/types/index.d.ts +35 -0
  21. package/dist/types/jobs.d.ts +134 -0
  22. package/dist/types/live.d.ts +62 -0
  23. package/dist/types/migrate.d.ts +163 -0
  24. package/dist/types/model.d.ts +36 -0
  25. package/dist/types/patch-sql.d.ts +37 -0
  26. package/dist/types/plan.d.ts +119 -0
  27. package/dist/types/profile.d.ts +80 -0
  28. package/dist/types/query.d.ts +100 -0
  29. package/dist/types/residual.d.ts +50 -0
  30. package/dist/types/store.d.ts +53 -0
  31. package/dist/types/tracker.d.ts +43 -0
  32. package/dist/types/typed.d.ts +15 -0
  33. package/dist/types/types.d.ts +26 -0
  34. package/dist/types/udf.d.ts +70 -0
  35. package/dist/types/window.d.ts +52 -0
  36. package/docs/JOBS-FORMAT.md +218 -0
  37. package/docs/LIVE-FORMAT.md +348 -0
  38. package/docs/MIGRATION-FORMAT.md +302 -0
  39. package/docs/MODEL-FORMAT.md +928 -0
  40. package/package.json +81 -0
  41. package/schemas/jaren-migration.draft-07.schema.json +144 -0
  42. package/schemas/jaren-migration.schema.json +144 -0
  43. package/schemas/jaren-model.draft-07.schema.json +149 -0
  44. package/schemas/jaren-model.schema.json +149 -0
  45. package/src/algebra.js +105 -0
  46. package/src/app.js +108 -0
  47. package/src/capture.js +584 -0
  48. package/src/cli.js +264 -0
  49. package/src/dag-job.js +86 -0
  50. package/src/ddl.js +588 -0
  51. package/src/dialect.js +297 -0
  52. package/src/dialects/sqlite.js +175 -0
  53. package/src/driver.js +419 -0
  54. package/src/drivers/bun.js +101 -0
  55. package/src/drivers/node.js +93 -0
  56. package/src/drivers/wasm.js +178 -0
  57. package/src/emit-model.js +208 -0
  58. package/src/emit.js +393 -0
  59. package/src/entity.js +367 -0
  60. package/src/errors.js +173 -0
  61. package/src/graph.js +101 -0
  62. package/src/index.js +64 -0
  63. package/src/jobs.js +507 -0
  64. package/src/live.js +899 -0
  65. package/src/migrate.js +1411 -0
  66. package/src/model.js +476 -0
  67. package/src/patch-sql.js +150 -0
  68. package/src/plan.js +1038 -0
  69. package/src/profile.js +131 -0
  70. package/src/query.js +1010 -0
  71. package/src/residual.js +91 -0
  72. package/src/store.js +1422 -0
  73. package/src/tracker.js +776 -0
  74. package/src/typed.js +19 -0
  75. package/src/types.js +36 -0
  76. package/src/udf.js +132 -0
  77. package/src/window.js +125 -0
  78. package/types/app.d.ts +36 -0
  79. package/types/bun.d.ts +9 -0
  80. package/types/index.d.ts +592 -0
  81. package/types/node.d.ts +15 -0
  82. package/types/typed.d.ts +108 -0
  83. package/types/wasm.d.ts +5 -0
package/src/algebra.js ADDED
@@ -0,0 +1,105 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The Plan algebra: the dialect-neutral middle stage between the
4
+ * engine's AST and a dialect's SQL. A plan is a plain JSON value —
5
+ * inspectable, golden-testable without a database — and it carries NO
6
+ * SQL text: every string in a plan is a member name, a type tag, an
7
+ * external name or a reason sentence, never a fragment of any query
8
+ * language. `assertNoSqlText` is the tripwire the tests run over every
9
+ * golden.
10
+ *
11
+ * One plan shape covers this version: a guarded selection over ONE
12
+ * collection with optional ordering, window, aggregate and a
13
+ * whole-document projection. Constructs beyond it are residuals by
14
+ * design (see ARCHITECTURE.md's deliberate-residual table).
15
+ */
16
+
17
+ /** The plan format version, carried on every plan. */
18
+ export const PLAN_VERSION = 1;
19
+
20
+ /**
21
+ * @typedef {{ segments: ({ name: string } | { index: number })[],
22
+ * type: string, column: string | null }} PlanRef
23
+ * A typed reference into the stored document: `type` is the
24
+ * schema-declared type or `'unknown'`; `column` is the generated
25
+ * column name when the collection indexes this path.
26
+ *
27
+ * @typedef {{ lit: unknown } | { ext: string }} PlanOperand
28
+ *
29
+ * @typedef {(
30
+ * { p: 'and' | 'or', items: PlanPredicate[] } |
31
+ * { p: 'not', item: PlanPredicate } |
32
+ * { p: 'cmp', op: 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge',
33
+ * ref: PlanRef, operand: PlanOperand } |
34
+ * { p: 'typeIs', ref: PlanRef, types: string[], positive: boolean } |
35
+ * { p: 'strop', kind: 'starts' | 'ends' | 'contains',
36
+ * ref: PlanRef, operand: PlanOperand } |
37
+ * { p: 'const', value: boolean } |
38
+ * { p: 'udf', name: string, key: string }
39
+ * )} PlanPredicate
40
+ *
41
+ * @typedef {{ ref: PlanRef, desc: boolean, emptyGreatest: boolean }} PlanOrderTerm
42
+ *
43
+ * @typedef {{
44
+ * planVersion: number,
45
+ * alg: 'select',
46
+ * collection: string,
47
+ * filter: PlanPredicate | null,
48
+ * order: PlanOrderTerm[] | null,
49
+ * window: { offset: number, limit: number | null } | null,
50
+ * aggregate: { fn: 'count' | 'sum' | 'avg' | 'min' | 'max',
51
+ * ref: PlanRef | null } | null,
52
+ * project: 'document',
53
+ * }} Plan
54
+ */
55
+
56
+ /**
57
+ * A fresh select plan over one collection.
58
+ * @param {string} collection
59
+ * @returns {Plan}
60
+ */
61
+ export function selectPlan(collection) {
62
+ return {
63
+ planVersion: PLAN_VERSION,
64
+ alg: 'select',
65
+ collection,
66
+ filter: null,
67
+ order: null,
68
+ window: null,
69
+ aggregate: null,
70
+ project: 'document',
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Conjoin a predicate onto a plan's filter.
76
+ * @param {PlanPredicate | null} filter
77
+ * @param {PlanPredicate} predicate
78
+ * @returns {PlanPredicate}
79
+ */
80
+ export function conjoin(filter, predicate) {
81
+ if (filter === null) return predicate;
82
+ if (filter.p === 'and') return { p: 'and', items: [...filter.items, predicate] };
83
+ return { p: 'and', items: [filter, predicate] };
84
+ }
85
+
86
+ /**
87
+ * Tokens that must never appear anywhere in a plan: if one does, SQL
88
+ * text leaked out of the dialect layer into the neutral algebra.
89
+ */
90
+ const SQL_TOKENS = [
91
+ 'SELECT', 'WHERE', 'ORDER BY', 'LIMIT ', 'INSERT', 'FROM ',
92
+ 'jsonb_extract', 'json_type', 'substr(', 'instr(', '"doc"', '@p1', ' AS ',
93
+ ];
94
+
95
+ /**
96
+ * Throw when a plan value carries anything that smells like SQL.
97
+ * @param {unknown} plan
98
+ */
99
+ export function assertNoSqlText(plan) {
100
+ const text = JSON.stringify(plan);
101
+ for (const token of SQL_TOKENS) {
102
+ if (text.includes(token))
103
+ throw new Error(`plan carries SQL text: found '${token}'`);
104
+ }
105
+ }
package/src/app.js ADDED
@@ -0,0 +1,108 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The app binding for live queries (LIVE-FORMAT §10): GENERATED
4
+ * documents plus a handler factory — the `fsmToApp` precedent. The db
5
+ * package never imports `@jarenjs/app`; the app document declares a
6
+ * subscription (`APP-FORMAT §5.3`) whose registered handler is
7
+ * `createLiveSubscription(store)`, and a two-line action whose whole
8
+ * body is `{ patch: '$payload' }` — the handler prefixes every op
9
+ * with the declared state path, so the app loop applies live patches
10
+ * with the machinery it already has.
11
+ */
12
+
13
+ /**
14
+ * Prefix every op path in a live patch with the state slot.
15
+ * @param {any[]} patch
16
+ * @param {string} statePath - JSON Pointer to the slot holding the
17
+ * live result document
18
+ */
19
+ export function prefixLivePatch(patch, statePath) {
20
+ return patch.map((op) => ({
21
+ ...op,
22
+ path: statePath + op.path,
23
+ ...(op.from !== undefined ? { from: statePath + op.from } : {}),
24
+ }));
25
+ }
26
+
27
+ /**
28
+ * The generated documents (§10): a subscription entry and the
29
+ * patch-forwarding action, both plain data for the app document.
30
+ * @param {{ run?: string, action?: string, statePath: string,
31
+ * collection?: string, query: any, externals?: any, mode?: string,
32
+ * when?: any }} options
33
+ * @returns {{ subscription: any, actions: any }}
34
+ */
35
+ export function liveAppBinding(options) {
36
+ if (typeof options?.statePath !== 'string' || !options.statePath.startsWith('/')) {
37
+ throw new TypeError('liveAppBinding needs a statePath JSON Pointer');
38
+ }
39
+ if (options.query === undefined) {
40
+ throw new TypeError('liveAppBinding needs the query document');
41
+ }
42
+ const run = options.run ?? 'db/live';
43
+ const action = options.action ?? 'db/liveChanged';
44
+ return {
45
+ subscription: {
46
+ run,
47
+ with: {
48
+ action,
49
+ statePath: options.statePath,
50
+ ...(options.collection !== undefined ? { collection: options.collection } : {}),
51
+ query: options.query,
52
+ ...(options.externals !== undefined ? { externals: options.externals } : {}),
53
+ ...(options.mode !== undefined ? { mode: options.mode } : {}),
54
+ },
55
+ ...(options.when !== undefined ? { when: options.when } : {}),
56
+ },
57
+ actions: { [action]: { patch: '$payload' } },
58
+ };
59
+ }
60
+
61
+ /**
62
+ * The subscription handler factory: registers the live query when the
63
+ * subscription starts, dispatches ONE initializing patch (a `replace`
64
+ * of the whole slot), forwards each emission prefixed, and closes on
65
+ * cleanup. An emission error surfaces as a dispatch of
66
+ * `<action>/error` so the app can render it — silence is not an
67
+ * option the format allows.
68
+ * @param {any} store - an open store with capture
69
+ * @returns {(props: any, dispatch: Function) => Function}
70
+ */
71
+ export function createLiveSubscription(store) {
72
+ return (props, dispatch) => {
73
+ const { action, statePath, collection, query, externals, mode } = props;
74
+ let closed = false;
75
+ /** @type {any} */
76
+ let live = null;
77
+ const registration = collection !== undefined
78
+ ? store.collection(collection).live(query, { externals, mode })
79
+ : store.live(query, { externals, mode });
80
+ registration.then((handle) => {
81
+ if (closed) {
82
+ handle.close();
83
+ return;
84
+ }
85
+ live = handle;
86
+ dispatch(action, [{ op: 'replace', path: statePath, value: handle.result }]);
87
+ handle.subscribe((event) => {
88
+ if (event.error !== undefined) {
89
+ dispatch(`${action}/error`, {
90
+ code: event.error.code, message: String(event.error.message ?? event.error),
91
+ });
92
+ return;
93
+ }
94
+ dispatch(action, prefixLivePatch(event.patch, statePath));
95
+ });
96
+ }, (error) => {
97
+ if (!closed) {
98
+ dispatch(`${action}/error`, {
99
+ code: error.code, message: String(error.message ?? error),
100
+ });
101
+ }
102
+ });
103
+ return () => {
104
+ closed = true;
105
+ if (live !== null) live.close();
106
+ };
107
+ };
108
+ }