@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
@@ -0,0 +1,178 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The wasm binding: an INJECTED handle behind the driver
4
+ * contract. This module imports no runtime builtin at all — the host
5
+ * (a browser, a worker) supplies the SQLite build, and this driver
6
+ * only adapts it. Every handle method may return a value or a promise;
7
+ * a main-thread OPFS-backed build is asynchronous and that is exactly
8
+ * why the public store surface is.
9
+ *
10
+ * The injected contract:
11
+ *
12
+ * handle = {
13
+ * open(path, options) -> raw | Promise<raw>,
14
+ * synchronous?: boolean, // default false
15
+ * declares?: { userFunctions?, deterministicIndexableFunctions?,
16
+ * sessions? } // default all false
17
+ * }
18
+ * raw = { exec(sql), prepare(sql) -> { run, get, all, iterate? },
19
+ * close(), registerFunction?, registerAggregate?, session? }
20
+ *
21
+ * Capability truth still comes from the probe: whatever the handle
22
+ * declares is intersected with what the loaded library actually
23
+ * compiled in.
24
+ */
25
+
26
+ import { chain, openConnection } from '../driver.js';
27
+ import { sqliteDialect } from '../dialects/sqlite.js';
28
+ import { DbCompileError } from '../errors.js';
29
+
30
+ /**
31
+ * Adapt an already-constructed `sqlite3.oo1` database (the official
32
+ * SQLite wasm build's object API) into the raw contract. The oo1 API
33
+ * is SYNCHRONOUS — wasm SQLite computes in place and the SAH-pool
34
+ * OPFS VFS does synchronous I/O inside a dedicated worker — which is
35
+ * exactly what keeps journal capture, live queries and the job queue
36
+ * working unchanged in a browser.
37
+ *
38
+ * Statements are REUSED by the store's prepared caches: every
39
+ * operation ends in `reset()`, never `finalize()`. oo1 user functions
40
+ * receive a context pointer first — stripped here — and register
41
+ * variadic (`arity: -1`), matching the engine's fragment shapes.
42
+ * @param {any} sqlite3 - the loaded sqlite3 module (for `capi`)
43
+ * @param {any} db - an `sqlite3.oo1.DB`-shaped database
44
+ * @returns {any} the raw binding for {@link openConnection}
45
+ */
46
+ export function adaptOo1Database(sqlite3, db) {
47
+ return {
48
+ /** @param {string} sql */
49
+ exec: (sql) => {
50
+ db.exec(sql);
51
+ },
52
+ /** @param {string} sql */
53
+ prepare: (sql) => {
54
+ const statement = db.prepare(sql);
55
+ const bind = (params) => {
56
+ statement.reset();
57
+ if (params.length > 0) statement.bind(params);
58
+ };
59
+ return {
60
+ run: (params = []) => {
61
+ bind(params);
62
+ statement.step();
63
+ statement.reset();
64
+ return {
65
+ changes: db.changes(),
66
+ lastInsertRowid: Number(
67
+ sqlite3.capi.sqlite3_last_insert_rowid(db)),
68
+ };
69
+ },
70
+ get: (params = []) => {
71
+ bind(params);
72
+ const row = statement.step() ? statement.get({}) : undefined;
73
+ statement.reset();
74
+ return row;
75
+ },
76
+ all: (params = []) => {
77
+ bind(params);
78
+ const rows = [];
79
+ while (statement.step()) rows.push(statement.get({}));
80
+ statement.reset();
81
+ return rows;
82
+ },
83
+ iterate: (params = []) => {
84
+ bind(params);
85
+ return {
86
+ next() {
87
+ if (statement.step()) return { done: false, value: statement.get({}) };
88
+ statement.reset();
89
+ return { done: true, value: undefined };
90
+ },
91
+ return(value) {
92
+ statement.reset();
93
+ return { done: true, value };
94
+ },
95
+ [Symbol.iterator]() { return this; },
96
+ };
97
+ },
98
+ };
99
+ },
100
+ close: () => db.close(),
101
+ registerFunction: (name, options, fn) => db.createFunction(name,
102
+ (_context, ...args) => fn(...args),
103
+ { deterministic: options?.deterministic === true, arity: -1 }),
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Build the injected HANDLE from a loaded sqlite3 module — the D6
109
+ * recipe: the host loads the wasm build and picks the database class
110
+ * (`sqlite3.oo1.DB` for `:memory:`, the SAH-pool util's `OpfsSAHPoolDb`
111
+ * for OPFS persistence), and this package only adapts it.
112
+ *
113
+ * `sessions` is deliberately NOT declared even though the canonical
114
+ * wasm build compiles `ENABLE_SESSION`: this adapter does not yet map
115
+ * the session C API, so capture runs in the journal mode — stated in
116
+ * the capability matrix, adapting it is a roadmap item.
117
+ * @param {any} sqlite3 - the loaded sqlite3 module
118
+ * @param {{ DbClass?: any }} [handleOptions] - the database class to
119
+ * construct (default `sqlite3.oo1.DB`)
120
+ * @returns {any} a handle for {@link wasmDriver}
121
+ */
122
+ export function sqlite3Handle(sqlite3, handleOptions) {
123
+ if (sqlite3 === null || typeof sqlite3 !== 'object'
124
+ || typeof sqlite3?.oo1?.DB !== 'function') {
125
+ throw new DbCompileError('JD0003',
126
+ 'sqlite3Handle needs a loaded sqlite3 module exposing oo1.DB');
127
+ }
128
+ const DbClass = handleOptions?.DbClass ?? sqlite3.oo1.DB;
129
+ return {
130
+ synchronous: true,
131
+ declares: {
132
+ userFunctions: true,
133
+ deterministicIndexableFunctions: true,
134
+ sessions: false,
135
+ },
136
+ /**
137
+ * @param {string} path
138
+ * @param {{ readOnly?: boolean }} [options]
139
+ */
140
+ open: (path, options) => adaptOo1Database(sqlite3,
141
+ new DbClass(path ?? ':memory:', options?.readOnly === true ? 'r' : 'c')),
142
+ };
143
+ }
144
+
145
+ /**
146
+ * The wasm driver over an injected handle.
147
+ * @param {any} handle - The host-supplied SQLite handle (see the file
148
+ * header for the contract)
149
+ * @returns {any}
150
+ */
151
+ export function wasmDriver(handle) {
152
+ if (handle === null || typeof handle !== 'object'
153
+ || typeof handle.open !== 'function') {
154
+ throw new DbCompileError('JD0003',
155
+ 'wasmDriver needs an injected handle exposing open(path, options)');
156
+ }
157
+ return Object.freeze({
158
+ name: 'wasm-sqlite',
159
+ dialect: sqliteDialect,
160
+ /**
161
+ * @param {string} path
162
+ * @param {any} [options]
163
+ * @returns {any}
164
+ */
165
+ open: (path, options) => chain(handle.open(path, options), (raw) =>
166
+ openConnection(raw, {
167
+ dialect: sqliteDialect,
168
+ synchronous: handle.synchronous === true,
169
+ declared: {
170
+ sessions: handle.declares?.sessions === true,
171
+ userFunctions: handle.declares?.userFunctions === true,
172
+ deterministicIndexableFunctions:
173
+ handle.declares?.deterministicIndexableFunctions === true,
174
+ aggregateFunctions: handle.declares?.aggregateFunctions === true,
175
+ },
176
+ })),
177
+ });
178
+ }
@@ -0,0 +1,208 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Entity types from models (D17): build an EMIT MODEL DOCUMENT
4
+ * — the published contract from EMIT-FORMAT.md — that renders entity
5
+ * interfaces, input variants and the typed-store metadata from the
6
+ * same model document the runtime validates against.
7
+ *
8
+ * The seam (EMIT-FORMAT §4.1, recorded here as the order demanded):
9
+ * the schema compiler is INJECTED (`options.compile` is
10
+ * `compileEmitModel` from `@jarenjs/emit`), called with
11
+ * `extensions: ['x-entity']` so the vocabulary rides the member nodes
12
+ * verbatim; this module then post-processes the MODEL DOCUMENT —
13
+ * replacing relation member types with references, flipping
14
+ * optionality, adding declarations — and never renders a character of
15
+ * TypeScript itself. Emit stays database-free, db stays
16
+ * renderer-free, and the injection keeps `@jarenjs/emit` out of db's
17
+ * dependency graph (the generator script wires the two).
18
+ *
19
+ * What the artifact says, deliberately:
20
+ * - entity interfaces are CLOSED objects (excess-property checking is
21
+ * the point of generated types; the runtime validator stays
22
+ * authoritative for what a database accepts);
23
+ * - relation members are optional references — present only when a
24
+ * graph load included them; `date-time`/`date` strings carry the
25
+ * `DateTime` brand so the linq date operators light up;
26
+ * - the `<Name>Input` variant makes defaulted and generated members
27
+ * optional, drops to-one/to-many relation members (`create`/`add`
28
+ * refuse them), and types many-to-many members as key-or-document
29
+ * arrays (what `add()` accepts);
30
+ * - `EntityMetaMap` carries doc/input/key/relations per entity — the
31
+ * generic typed-store surface (`@jarenjs/db/typed`) binds to it.
32
+ */
33
+
34
+ import { normalizeEntities } from './model.js';
35
+
36
+ const primitive = (name) => ({ kind: 'primitive', primitive: name });
37
+ const ref = (name) => ({ kind: 'ref', ref: name });
38
+ const arrayOf = (items) => ({ kind: 'array', items });
39
+ const unionOf = (options) => ({ kind: 'union', options });
40
+ const literal = (value) => ({ kind: 'literal', value });
41
+ const member = (name, type, required, doc = []) =>
42
+ ({ kind: 'member', name, type, required, constraints: [], doc });
43
+ const objectOf = (members) => ({ kind: 'object', members });
44
+ const declaration = (name, type, doc = []) =>
45
+ ({ kind: 'declaration', name, type, constraints: [], doc });
46
+
47
+ /** The `DateTime` brand: structurally the linq brand, generated
48
+ * inline so the artifact stands alone. */
49
+ const DATE_TIME_DECLARATION = declaration('DateTime', {
50
+ kind: 'intersection',
51
+ parts: [primitive('string'), objectOf([
52
+ member('__jarenTag', literal('date-time'), true),
53
+ ])],
54
+ }, ['An RFC 3339 string branded for the date operators;',
55
+ 'structurally identical to the @jarenjs/linq brand.']);
56
+
57
+ const keyTypeOf = (entity) => {
58
+ const parts = entity.keys.map((key) =>
59
+ primitive(entity.properties.get(key).type === 'string' ? 'string' : 'number'));
60
+ if (parts.length === 1) return parts[0];
61
+ return objectOf(entity.keys.map((key, i) => member(key, parts[i], true)));
62
+ };
63
+
64
+ /**
65
+ * Build the emit-model document for a model's entities.
66
+ * @param {any} model - a jaren-model document with `entities`
67
+ * @param {{ compile: (schema: any, options?: any) => any,
68
+ * source?: string, reserved?: string[] }} options - `compile` is
69
+ * `compileEmitModel` (injected; see the header)
70
+ * @returns {any} an EMIT-FORMAT `0.1` model document
71
+ */
72
+ export function entityEmitModel(model, options) {
73
+ const compile = options?.compile;
74
+ if (typeof compile !== 'function')
75
+ throw new TypeError("entityEmitModel needs { compile: compileEmitModel } injected");
76
+ const entities = normalizeEntities(model);
77
+ const entityNames = [...entities.keys()];
78
+
79
+ // every entity name, input name and the brand are reserved up front
80
+ // so nested-shape hints can never steal them
81
+ const taken = [
82
+ ...(options?.reserved ?? []),
83
+ 'DateTime', 'Entities', 'EntityInputs', 'EntityMetaMap',
84
+ ...entityNames,
85
+ ...entityNames.map((name) => `${name}Input`),
86
+ ];
87
+
88
+ /** @type {any[]} */
89
+ const declarations = [];
90
+ let usesDateTime = false;
91
+
92
+ for (const entity of entities.values()) {
93
+ const compiled = compile(entity.schema, {
94
+ name: entity.name,
95
+ extensions: ['x-entity'],
96
+ openObjects: 'closed',
97
+ reserved: taken.filter((name) => name !== entity.name),
98
+ });
99
+ for (const compiledDeclaration of compiled.declarations) {
100
+ if (!taken.includes(compiledDeclaration.name))
101
+ taken.push(compiledDeclaration.name);
102
+ }
103
+
104
+ const root = compiled.declarations
105
+ .find((candidate) => candidate.name === entity.name);
106
+ const others = compiled.declarations
107
+ .filter((candidate) => candidate !== root);
108
+
109
+ // post-process the entity's own members through the vocabulary
110
+ for (const memberNode of root.type.members) {
111
+ const property = entity.properties.get(memberNode.name);
112
+ if (property === undefined) continue;
113
+ if (property.relation !== undefined) {
114
+ // the seam is load-bearing: a relation member is DETECTED by
115
+ // the preserved keyword, resolved through the normalized model
116
+ if (memberNode.extensions?.['x-entity']?.relation === undefined) continue;
117
+ const relation = property.relation;
118
+ const target = ref(relation.to);
119
+ memberNode.type = relation.kind === 'oneToOne' ? target : arrayOf(target);
120
+ memberNode.required = false;
121
+ memberNode.doc = [
122
+ `The ${relation.kind} relation to ${relation.to}; present only`,
123
+ 'when a graph load included it.'];
124
+ continue;
125
+ }
126
+ if (property.type === 'string'
127
+ && (property.format === 'date-time' || property.format === 'date')) {
128
+ memberNode.type = ref('DateTime');
129
+ usesDateTime = true;
130
+ }
131
+ if (property.version) {
132
+ memberNode.doc = ['The optimistic-concurrency token (§11.5):',
133
+ 'engine-owned, bumped on every successful write.'];
134
+ }
135
+ }
136
+
137
+ // the input variant: defaulted/generated members optional, to-one
138
+ // and to-many members gone, many-to-many as key-or-document arrays
139
+ const inputMembers = [];
140
+ for (const memberNode of root.type.members) {
141
+ const property = entity.properties.get(memberNode.name);
142
+ if (property?.relation !== undefined) {
143
+ if (property.relation.kind !== 'manyToMany') continue;
144
+ const target = entities.get(property.relation.to);
145
+ inputMembers.push(member(memberNode.name,
146
+ arrayOf(unionOf([keyTypeOf(target), ref(property.relation.to)])),
147
+ false,
148
+ ['Membership rows to attach: target keys or documents.']));
149
+ continue;
150
+ }
151
+ const inputMember = {
152
+ ...memberNode,
153
+ // a DateTime read is a plain string write: the brand
154
+ // discriminates expressions, never blocks a caller's literal
155
+ type: memberNode.type.ref === 'DateTime' ? primitive('string') : memberNode.type,
156
+ required: property?.default !== undefined
157
+ ? false
158
+ : memberNode.required,
159
+ };
160
+ if (property?.default !== undefined) {
161
+ inputMember.doc = [`Defaulted (${JSON.stringify(property.default)});`,
162
+ 'optional for a caller, present after the write.'];
163
+ }
164
+ inputMembers.push(inputMember);
165
+ }
166
+
167
+ declarations.push(...others, root,
168
+ declaration(`${entity.name}Input`, objectOf(inputMembers),
169
+ [`What create()/add() accept for ${entity.name}: defaulted and`,
170
+ 'generated members are optional; relation projections are not',
171
+ 'writable (many-to-many membership arrays are).']));
172
+ }
173
+
174
+ // the metadata the generic typed store binds to
175
+ const metaFor = (entity) => objectOf([
176
+ member('doc', ref(entity.name), true),
177
+ member('input', ref(`${entity.name}Input`), true),
178
+ member('key', keyTypeOf(entity), true),
179
+ member('relations', objectOf(
180
+ [...entity.properties.values()]
181
+ .filter((property) => property.relation !== undefined)
182
+ .map((property) => member(property.name, objectOf([
183
+ member('entity', literal(property.relation.to), true),
184
+ member('doc', ref(property.relation.to), true),
185
+ member('many', literal(property.relation.kind !== 'oneToOne'), true),
186
+ ]), true))), true),
187
+ ]);
188
+ declarations.push(
189
+ declaration('Entities',
190
+ objectOf(entityNames.map((name) => member(name, ref(name), true))),
191
+ ['Entity name to document shape.']),
192
+ declaration('EntityInputs',
193
+ objectOf(entityNames.map((name) => member(name, ref(`${name}Input`), true))),
194
+ ['Entity name to input shape.']),
195
+ declaration('EntityMetaMap',
196
+ objectOf(entityNames.map((name) => member(name, metaFor(entities.get(name)), true))),
197
+ ['The typed-store binding: pass this to TypedStore<…> from',
198
+ "'@jarenjs/db/typed'."]));
199
+
200
+ if (usesDateTime) declarations.unshift(DATE_TIME_DECLARATION);
201
+
202
+ return {
203
+ $emit: '0.1',
204
+ source: options?.source ?? null,
205
+ root: null,
206
+ declarations,
207
+ };
208
+ }