@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/typed.js ADDED
@@ -0,0 +1,19 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The typed-store binding: a zero-cost identity whose ONLY job
4
+ * is carrying the generated `EntityMetaMap` into the type system —
5
+ * `typedStore<EntityMetaMap>(store)` narrows `entity(name)` to the
6
+ * generated document/input shapes and widens `load` results by their
7
+ * include specification. The runtime is the store it was given; every
8
+ * guarantee lives in `types/typed.d.ts` and the generated artifact.
9
+ */
10
+
11
+ /**
12
+ * Bind a store to its generated entity metadata. Identity at runtime.
13
+ * @template E
14
+ * @param {any} store - an opened store whose model generated `E`
15
+ * @returns {any}
16
+ */
17
+ export function typedStore(store) {
18
+ return store;
19
+ }
package/src/types.js ADDED
@@ -0,0 +1,36 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Schema-derived path typing: the collection's JSON Schema is the
4
+ * planner's type source (there is no engine-side inference in this
5
+ * phase — the store's own contract carries the types). Where the
6
+ * schema says nothing, the answer is `'unknown'` and the planner emits
7
+ * the defensive form; the guards in the truth table make `'unknown'`
8
+ * safe, so typing here only ever IMPROVES a plan (column choice), it
9
+ * never weakens one.
10
+ */
11
+
12
+ import { schemaTypeAt } from './ddl.js';
13
+
14
+ const KNOWN = new Set(['string', 'integer', 'number', 'boolean']);
15
+
16
+ /**
17
+ * The planner-facing type of a member path.
18
+ * @param {any} schema - The collection's document schema
19
+ * @param {({ name: string } | { index: number })[]} segments
20
+ * @returns {'string' | 'integer' | 'number' | 'boolean' | 'unknown'}
21
+ */
22
+ export function typeOfPath(schema, segments) {
23
+ const declared = schemaTypeAt(schema, segments);
24
+ return declared !== undefined && KNOWN.has(declared)
25
+ ? /** @type {any} */ (declared)
26
+ : 'unknown';
27
+ }
28
+
29
+ /**
30
+ * Whether a planner type is numeric (SQL comparison family).
31
+ * @param {string} type
32
+ * @returns {boolean}
33
+ */
34
+ export function isNumericType(type) {
35
+ return type === 'integer' || type === 'number';
36
+ }
package/src/udf.js ADDED
@@ -0,0 +1,132 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The UDF escape hatch (capability-gated): between native SQL
4
+ * and pulling rows sits registering a compiled predicate conjunct as a
5
+ * DETERMINISTIC function used in the WHERE clause. Only fragments the
6
+ * analysis proves deterministic and side-effect-free qualify — no
7
+ * externals (their values change per call, and a deterministic
8
+ * function must not close over changing state), no host functions, no
9
+ * collations. Registration is keyed by the fragment's COLLISION-FREE
10
+ * structural identity, so identical fragments share one registration
11
+ * per store and different fragments never do. A fingerprint could not
12
+ * decide this: two colliding fragments would claim one SQL function
13
+ * name, the second would silently reuse the first's predicate, and the
14
+ * WHERE clause would filter on the wrong condition. The SQL identifier
15
+ * still derives from a short fingerprint — names must be short — but a
16
+ * fingerprint clash between DIFFERENT identities is disambiguated with
17
+ * a suffix rather than collapsed.
18
+ *
19
+ * WHERE-clause use only: an INDEX over a registered function would
20
+ * make the database unwritable from any connection that has not
21
+ * registered the identical function — that schema-dependency hazard is
22
+ * why the model format declares no UDF-expression indexes.
23
+ *
24
+ * Ring 3 extends the hatch to registry `pushable:'scalar'`
25
+ * operators: a predicate fragment that uses a registered scalar operator
26
+ * (`$sqrt`, `$pow`, …) compiles WITH the store's `{ functions,
27
+ * extensions }` and pushes as the same deterministic UDF — SQLite drives
28
+ * the row iteration and the operator runs inside the callback, instead
29
+ * of every candidate crossing into the residual. The determinism rule is
30
+ * unchanged (no externals, no collations); a registered operator the
31
+ * pack did NOT mark `pushable:'scalar'` (a whole-series `$npv`, an
32
+ * aggregate) stays the residual — Ring 2 keeps it correct.
33
+ */
34
+
35
+ import { semanticKey } from '@jarenjs/core/object';
36
+ import { hashContent } from '@jarenjs/core/string';
37
+ import { compileJsonQuery, analyzeQuery } from '@jarenjs/json/query';
38
+
39
+ /**
40
+ * The SQL identifier for one fragment identity: a short fingerprint of
41
+ * the identity, not the identity itself (SQLite names are not the place
42
+ * for a whole serialized document). Distinct identities may collide
43
+ * here; {@link registerFragment} disambiguates.
44
+ * @param {string} identity
45
+ * @returns {string}
46
+ */
47
+ const functionNameFor = (identity) => `jaren_p_${hashContent(identity)}`;
48
+
49
+ /**
50
+ * Decide whether a raw predicate fragment qualifies for the hatch, and
51
+ * build its registration if so.
52
+ * @param {any} fragment - The raw conjunct (a JSON query expression
53
+ * over `$it`)
54
+ * @param {{ functions?: any, extensions?: any, pushableScalar?: Set<string> } | null}
55
+ * [operators] - the store's registered operators (Ring 3); only its
56
+ * `pushable:'scalar'` subset is admitted. `null`/absent keeps the
57
+ * original engine-internal-only rule (no host function pushes).
58
+ * @returns {{ key: string, name: string,
59
+ * compile: () => (docText: string) => number } | null}
60
+ */
61
+ export function deterministicFragment(fragment, operators = null) {
62
+ const analyzeOpts = operators === null
63
+ ? undefined
64
+ : { functions: operators.functions, extensions: operators.extensions };
65
+ let dependencies;
66
+ try {
67
+ dependencies = analyzeQuery({ $let: { it: '$' }, $return: fragment }, analyzeOpts).dependencies;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ // determinism: a UDF registered `deterministic:true` must not close
73
+ // over changing state — no externals, no collations, ever
74
+ if (dependencies.externals.length > 0
75
+ || dependencies.collations.length > 0) return null;
76
+ // pushability: core operators are always fine; a REGISTERED operator or
77
+ // host function is admitted only where the pack marked it
78
+ // `pushable:'scalar'`. With no registry, any host function still
79
+ // disqualifies (the original hatch was engine-internal fragments only).
80
+ const pushable = operators === null ? null : operators.pushableScalar ?? new Set();
81
+ const registered = operators === null ? null : operators.extensions;
82
+ for (const name of dependencies.functions) {
83
+ if (pushable === null || !pushable.has(name)) return null;
84
+ }
85
+ for (const name of dependencies.operators) {
86
+ if (registered !== null && Object.prototype.hasOwnProperty.call(registered, name)
87
+ && !pushable.has(name)) return null;
88
+ }
89
+ /** @type {string} */
90
+ let key;
91
+ try {
92
+ key = semanticKey(fragment);
93
+ }
94
+ catch {
95
+ // a fragment that cannot be keyed injectively must not be shared
96
+ // under some other fragment's registration; the residual is always
97
+ // correct, so it does not qualify for the hatch
98
+ return null;
99
+ }
100
+ return {
101
+ key,
102
+ name: functionNameFor(key),
103
+ compile: () => {
104
+ const compiled = compileJsonQuery({ $let: { it: '$' }, $return: fragment }, analyzeOpts);
105
+ return (docText) => (compiled.ebv(JSON.parse(docText)) ? 1 : 0);
106
+ },
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Register a qualified fragment once per store, and answer the SQL name
112
+ * to call. Identity is the fragment's structural key, so the same
113
+ * fragment registers once and two different fragments always get two
114
+ * different functions — even when their short names fingerprint alike,
115
+ * which the suffix resolves.
116
+ * @param {any} connection
117
+ * @param {Map<string, string>} registered - The store's registrations,
118
+ * fragment identity → the SQL function name it owns
119
+ * @param {{ key: string, name: string, compile: () => Function }} fragment
120
+ * @returns {string} the function name to call in the WHERE clause
121
+ */
122
+ export function registerFragment(connection, registered, fragment) {
123
+ const owned = registered.get(fragment.key);
124
+ if (owned !== undefined) return owned;
125
+ const stem = functionNameFor(fragment.key);
126
+ const taken = new Set(registered.values());
127
+ let name = stem;
128
+ for (let n = 2; taken.has(name); n++) name = `${stem}_${n}`;
129
+ connection.registerFunction(name, { deterministic: true }, fragment.compile());
130
+ registered.set(fragment.key, name);
131
+ return name;
132
+ }
package/src/window.js ADDED
@@ -0,0 +1,125 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The maintained sorted window for `orderBy` (+ optional
4
+ * `limit`) live queries — LIVE-FORMAT §7's window row. The structure
5
+ * keeps EVERY matching row in sorted order (which is what makes a
6
+ * delete inside the visible window answerable without a re-query: the
7
+ * successor is already here), and the visible result is the first
8
+ * `limit` entries. Ties are broken by the collection key token,
9
+ * always ascending — the total order LIVE-FORMAT §9 promises.
10
+ *
11
+ * String comparison is CODEPOINT order (SQLite BINARY over UTF-8 —
12
+ * probed, never assumed), which `<` on JS strings gets wrong for
13
+ * astral-vs-BMP pairs; `compareCodepoint` walks code points.
14
+ */
15
+
16
+ /**
17
+ * Compare two strings in Unicode code-point order.
18
+ * @param {string} a
19
+ * @param {string} b
20
+ * @returns {number}
21
+ */
22
+ export function compareCodepoint(a, b) {
23
+ if (a === b) return 0;
24
+ const length = Math.min(a.length, b.length);
25
+ for (let i = 0; i < length;) {
26
+ const ca = /** @type {number} */ (a.codePointAt(i));
27
+ const cb = /** @type {number} */ (b.codePointAt(i));
28
+ if (ca !== cb) return ca < cb ? -1 : 1;
29
+ i += ca > 0xffff ? 2 : 1;
30
+ }
31
+ return a.length < b.length ? -1 : 1;
32
+ }
33
+
34
+ /**
35
+ * Compare two sort VALUES of one order term. Absent (`undefined`)
36
+ * ranks per `emptyGreatest` as the greatest or least value; direction
37
+ * is applied by the caller. Mixed types rank number-before-string —
38
+ * deterministic on hostile data, exact on schema-conformant data (the
39
+ * same precondition the pushdown ordering documents).
40
+ * @param {any} a
41
+ * @param {any} b
42
+ * @param {boolean} emptyGreatest
43
+ * @returns {number}
44
+ */
45
+ function compareValues(a, b, emptyGreatest) {
46
+ const aEmpty = a === undefined;
47
+ const bEmpty = b === undefined;
48
+ if (aEmpty || bEmpty) {
49
+ if (aEmpty && bEmpty) return 0;
50
+ return (aEmpty ? 1 : -1) * (emptyGreatest ? 1 : -1);
51
+ }
52
+ const aNumber = typeof a === 'number';
53
+ const bNumber = typeof b === 'number';
54
+ if (aNumber && bNumber) return a < b ? -1 : a > b ? 1 : 0;
55
+ if (aNumber !== bNumber) return aNumber ? -1 : 1;
56
+ return compareCodepoint(String(a), String(b));
57
+ }
58
+
59
+ /**
60
+ * @typedef {{ token: string, sortValues: any[], item: any }} WindowEntry
61
+ */
62
+
63
+ /**
64
+ * A maintained sorted set of rows under the declared order terms plus
65
+ * the key-token tiebreaker.
66
+ * @param {{ desc: boolean, emptyGreatest: boolean }[]} terms
67
+ * @param {number | null} limit - visible size; `null` = everything
68
+ */
69
+ export function createSortedWindow(terms, limit) {
70
+ /** @type {WindowEntry[]} */
71
+ const entries = [];
72
+ /** @type {Map<string, WindowEntry>} */
73
+ const byToken = new Map();
74
+
75
+ /**
76
+ * @param {WindowEntry} a
77
+ * @param {WindowEntry} b
78
+ */
79
+ const compare = (a, b) => {
80
+ for (let i = 0; i < terms.length; i++) {
81
+ const term = terms[i];
82
+ const cmp = compareValues(a.sortValues[i], b.sortValues[i], term.emptyGreatest);
83
+ if (cmp !== 0) return term.desc ? -cmp : cmp;
84
+ }
85
+ return compareCodepoint(a.token, b.token);
86
+ };
87
+
88
+ /** Binary search for the insertion index of `entry`. */
89
+ const positionOf = (entry) => {
90
+ let low = 0;
91
+ let high = entries.length;
92
+ while (low < high) {
93
+ const mid = (low + high) >>> 1;
94
+ if (compare(entries[mid], entry) < 0) low = mid + 1;
95
+ else high = mid;
96
+ }
97
+ return low;
98
+ };
99
+
100
+ return {
101
+ compare,
102
+ size: () => entries.length,
103
+ /** The visible slice: the first `limit` entries (all, unbounded). */
104
+ visible: () => (limit === null ? [...entries] : entries.slice(0, limit)),
105
+ /**
106
+ * Insert a row; the token MUST not be present.
107
+ * @param {string} token
108
+ * @param {any[]} sortValues
109
+ * @param {any} item
110
+ */
111
+ insert(token, sortValues, item) {
112
+ const entry = { token, sortValues, item };
113
+ entries.splice(positionOf(entry), 0, entry);
114
+ byToken.set(token, entry);
115
+ },
116
+ /** Remove a row by token; absent tokens are a no-op. */
117
+ remove(token) {
118
+ const entry = byToken.get(token);
119
+ if (entry === undefined) return false;
120
+ entries.splice(entries.indexOf(entry), 1);
121
+ byToken.delete(token);
122
+ return true;
123
+ },
124
+ };
125
+ }
package/types/app.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The app binding for live queries (LIVE-FORMAT §10): generated
3
+ * documents plus the subscription handler factory — the db package
4
+ * never imports `@jarenjs/app`.
5
+ */
6
+ import type { Store } from './index.js';
7
+
8
+ export interface LiveAppBindingOptions {
9
+ /** The registered subscription handler name (default 'db/live'). */
10
+ run?: string;
11
+ /** The patch-forwarding action name (default 'db/liveChanged'). */
12
+ action?: string;
13
+ /** JSON Pointer to the state slot holding `{ rows }`. */
14
+ statePath: string;
15
+ /** Collection name; omit for an entity-root document. */
16
+ collection?: string;
17
+ query: unknown;
18
+ externals?: Record<string, unknown>;
19
+ mode?: 'auto' | 'incremental' | 'rerun';
20
+ /** An APP-FORMAT `when` query gating the subscription. */
21
+ when?: unknown;
22
+ }
23
+
24
+ export interface LiveAppBinding {
25
+ /** The APP-FORMAT §5.3 subscription entry (plain data). */
26
+ subscription: unknown;
27
+ /** `{ [action]: { patch: '$payload' } }` — merge into `actions`. */
28
+ actions: Record<string, unknown>;
29
+ }
30
+
31
+ export declare function liveAppBinding(options: LiveAppBindingOptions): LiveAppBinding;
32
+ export declare function prefixLivePatch(
33
+ patch: ReadonlyArray<{ op: string; path: string }>, statePath: string):
34
+ Array<{ op: string; path: string }>;
35
+ export declare function createLiveSubscription(store: Store):
36
+ (props: unknown, dispatch: (action: string, payload: unknown) => void) => () => void;
package/types/bun.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /** Hand-authored declarations for @jarenjs/db/bun (strategy 1). */
2
+ import type { Driver } from '@jarenjs/db';
3
+
4
+ /** The `bun:sqlite` binding; the builtin loads lazily inside open(). */
5
+ export declare function bunDriver(): Driver;
6
+ /** Adapt an already-constructed bun Database-shaped database. */
7
+ export declare function adaptBunDatabase(db: unknown): unknown;
8
+ /** Construct and adapt from a loaded `bun:sqlite`-shaped module. */
9
+ export declare function fromBunModule(mod: unknown, path: string, options?: unknown): unknown;