@jarenjs/db 0.49.2 → 0.66.1
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 +420 -71
- package/README.md +711 -79
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +309 -45
- package/docs/LIVE-FORMAT.md +156 -19
- package/docs/MIGRATION-FORMAT.md +247 -40
- package/docs/MODEL-FORMAT.md +968 -86
- package/package.json +21 -8
- package/schemas/jaren-migration.draft-07.schema.json +73 -0
- package/schemas/jaren-migration.schema.json +73 -0
- package/schemas/jaren-model.draft-07.schema.json +224 -162
- package/schemas/jaren-model.schema.json +224 -162
- package/src/algebra.js +227 -9
- package/src/backup.js +161 -0
- package/src/cancellation.js +48 -0
- package/src/capture.js +255 -44
- package/src/cli.js +337 -50
- package/src/cursor.js +411 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +125 -11
- package/src/dialect.js +267 -112
- package/src/dialects/expression-read.js +158 -0
- package/src/dialects/postgres.js +618 -0
- package/src/dialects/rtree-ddl.js +129 -0
- package/src/dialects/sqlite.js +245 -12
- package/src/document-files.js +311 -0
- package/src/document-steps.js +422 -0
- package/src/documents.js +335 -0
- package/src/driver.js +503 -69
- package/src/drivers/bun.js +37 -1
- package/src/drivers/indexeddb-snapshot.js +149 -0
- package/src/drivers/node-pool.js +11 -0
- package/src/drivers/node-worker-endpoint.js +105 -0
- package/src/drivers/node-worker.js +204 -0
- package/src/drivers/node.js +41 -7
- package/src/drivers/postgres.js +331 -0
- package/src/drivers/wasm-oo1.js +97 -0
- package/src/drivers/wasm-session.js +67 -0
- package/src/drivers/wasm.js +18 -83
- package/src/drivers/worker-pool.js +183 -0
- package/src/drivers/worker-protocol.js +79 -0
- package/src/drivers/worker-queue.js +60 -0
- package/src/emit-model.js +14 -0
- package/src/emit.js +349 -51
- package/src/entity.js +102 -59
- package/src/errors.js +430 -2
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +48 -19
- package/src/introspect.js +583 -0
- package/src/jobs.js +870 -99
- package/src/json-bytes.js +58 -0
- package/src/live-time.js +12 -3
- package/src/live.js +11 -1
- package/src/maintenance.js +175 -0
- package/src/migrate.js +606 -333
- package/src/model.js +241 -8
- package/src/plan.js +1238 -160
- package/src/pragmas.js +314 -0
- package/src/profile.js +151 -3
- package/src/query.js +1748 -312
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1672 -276
- package/src/tracker.js +367 -68
- package/src/udf.js +88 -7
- package/types/index.d.ts +1246 -32
- package/types/node-pool.d.ts +28 -0
- package/types/node-worker.d.ts +54 -0
- package/types/node.d.ts +72 -3
- package/types/postgres.d.ts +46 -0
- package/types/typed.d.ts +81 -3
- package/types/wasm.d.ts +21 -0
- package/dist/types/algebra.d.ts +0 -230
- 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 -154
- 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 -170
- 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-time.d.ts +0 -141
- package/dist/types/live.d.ts +0 -64
- 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 -142
- package/dist/types/profile.d.ts +0 -80
- package/dist/types/query.d.ts +0 -112
- package/dist/types/residual.d.ts +0 -64
- package/dist/types/series.d.ts +0 -227
- package/dist/types/store.d.ts +0 -60
- 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/ARCHITECTURE.md
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
# @jarenjs/db — architecture
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
documents the two seams that make that promise cheap to keep and cheap
|
|
5
|
-
to outgrow — an undocumented seam decays into an accident.
|
|
3
|
+
The package separates Store behavior from driver ownership and dialect spelling.
|
|
6
4
|
|
|
7
5
|
## The driver seam (`src/driver.js`, `src/drivers/*`)
|
|
8
6
|
|
|
9
|
-
A driver is `{ name, dialect, open(path, options) }` returning a
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
A driver is `{ name, dialect, open(path, options) }` returning a Connection
|
|
8
|
+
(`docs/MODEL-FORMAT.md` §4). Runtime bindings live behind public Node, Bun, wasm,
|
|
9
|
+
PostgreSQL, Node worker and Node pool subpaths. Node and Bun builtins load lazily
|
|
10
|
+
inside `open()`, so importing a subpath remains portable and an unavailable
|
|
11
|
+
binding becomes `JD0003`. The worker endpoint's static Node imports are reached
|
|
12
|
+
only when the Node worker factory launches that endpoint.
|
|
13
|
+
|
|
14
|
+
Worker protocol, scheduling and snapshot storage sit below `finishConnection`;
|
|
15
|
+
query planning, capture and transaction scopes remain in the Store. Compiled
|
|
16
|
+
operation metadata classifies reads for the pool. The wasm oo1 adapter shares one
|
|
17
|
+
session mapping across OPFS and atomic IndexedDB snapshots. [Execution hosts](docs/HOSTS.md)
|
|
18
|
+
documents bounds, native-call shutdown limits, capability probes and measurements.
|
|
19
19
|
|
|
20
20
|
Every connection and statement method may return a value or a promise.
|
|
21
21
|
The store composes through the sync-capable `chain` helper, which
|
|
@@ -46,10 +46,54 @@ test-double dialect whose quoting, parameter style and type names all
|
|
|
46
46
|
differ: the same model produces correspondingly different SQL.
|
|
47
47
|
|
|
48
48
|
Deliberately *not* dialect concerns, because they are behavioural
|
|
49
|
-
rather than syntactic, and live in the capability table
|
|
50
|
-
whether functions register per connection, whether change
|
|
51
|
-
exists and in what form, and whether tables can be
|
|
52
|
-
place.
|
|
49
|
+
rather than syntactic, and live in the CONNECTION capability table
|
|
50
|
+
instead: whether functions register per connection, whether change
|
|
51
|
+
capture exists and in what form, and whether tables can be
|
|
52
|
+
restructured in place.
|
|
53
|
+
|
|
54
|
+
The dialect has a capability table of its own, and it is a closed set
|
|
55
|
+
(`DIALECT_CAPABILITIES`): `createDialect` refuses a name outside it, so
|
|
56
|
+
a misspelled capability cannot read as a quiet `false` on the engine
|
|
57
|
+
that has the feature. Every entry answers a SYNTACTIC question — does
|
|
58
|
+
this engine have a binary JSON type, indexable generated columns, a
|
|
59
|
+
column with no scalar type, `RETURNING`, upsert, savepoints, an
|
|
60
|
+
up-front write lock, a result alias in `GROUP BY`, in-place `ALTER`,
|
|
61
|
+
virtual tables, triggers, a configuration vocabulary, a stored CREATE
|
|
62
|
+
text, always-on referential integrity, an insertion-ordered row
|
|
63
|
+
identity. What the store does where the answer is `false` is a
|
|
64
|
+
FALLBACK or a coded refusal on the common path, never a skip:
|
|
65
|
+
|
|
66
|
+
| Capability off | What the common path does instead |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `pragmas` | no configuration statement is issued; an explicitly requested pragma is `JD0007`, a defaulted one is dropped, and the effective record reads `null` throughout |
|
|
69
|
+
| `declaredSqlText` | the drift check is the structural one — columns and their types, indexes and their covered columns in order, foreign-key tuples — and an object the model never declared is no longer detectable |
|
|
70
|
+
| `foreignKeysAlwaysOn` (on) | the open path neither sets nor reads back the referential-integrity switch |
|
|
71
|
+
| `virtualTables` / `triggers` | a `physical: 'rtree'` column set maps back onto the B-tree over the four edge columns, and `explain().prefilters[].via` says so |
|
|
72
|
+
| `untypedColumns` | a comparison against a member the schema does not type reads the member out of the document instead of the column: same answer, unindexed |
|
|
73
|
+
| `indexableGeneratedColumns` | the derived-column mapping is the STORED one — the store writes the values and the columns are ordinary |
|
|
74
|
+
| `rowIdentity` (with a declared identity column) | the dialect adds that column to every table it creates, and it IS `rowIdentity()` |
|
|
75
|
+
| `immediateTransactions` | `tx.beginImmediate` is `tx.begin`, and a read-then-write body takes its lock when it writes |
|
|
76
|
+
|
|
77
|
+
Two subsystems are SQLite's own in 0.1 and say so on the connection
|
|
78
|
+
rather than in the dialect: the durable job queue (`capabilities.jobs`)
|
|
79
|
+
and the change ledger (`capabilities.changeCapture`) write their own
|
|
80
|
+
statements. Opening a store that asks for either on a connection that
|
|
81
|
+
declares it absent is a coded refusal at open — `JD0003` and `JD0051` —
|
|
82
|
+
not a failure at the first statement.
|
|
83
|
+
|
|
84
|
+
`test/db/dialect-conformance.js` is the kit every dialect passes: it
|
|
85
|
+
asserts behaviour rather than text, because a second dialect that
|
|
86
|
+
passed a golden-SQL comparison would have to spell PostgreSQL the way
|
|
87
|
+
SQLite spells it. A quoted identifier cannot close its own quoting; a
|
|
88
|
+
write statement binds through the dialect's own parameter form; a
|
|
89
|
+
generated column carries the storage word its engine has; an optional
|
|
90
|
+
feature is absent in BOTH halves or present in both.
|
|
91
|
+
`test/db/dialect-census.test.js` is the other side of the same claim: a
|
|
92
|
+
character scanner reads every string and template literal of every
|
|
93
|
+
module, keeps the ones that begin as SQL, and fails on a SQLite
|
|
94
|
+
spelling outside `src/dialects/` and `src/drivers/` unless the module
|
|
95
|
+
is named — with the capability that makes it honest — in a short
|
|
96
|
+
approved list.
|
|
53
97
|
|
|
54
98
|
The SQLite dialect stores documents as JSONB in a `BLOB` column of a
|
|
55
99
|
`STRICT` table, projects each indexed path into a virtual generated
|
|
@@ -57,6 +101,175 @@ column over `jsonb_extract`, and renders reads back to text through
|
|
|
57
101
|
`json()`. Parameters are positional because every shipped binding
|
|
58
102
|
binds arrays.
|
|
59
103
|
|
|
104
|
+
### The PostgreSQL dialect (`src/dialects/postgres.js`)
|
|
105
|
+
|
|
106
|
+
`postgresDialect()` is the second spelling, and the reason the contract
|
|
107
|
+
above exists. It imports no client and no runtime builtin, so
|
|
108
|
+
`@jarenjs/db/postgres` resolves in a browser bundle and type-checks with
|
|
109
|
+
nothing installed; transport is the injected driver's business.
|
|
110
|
+
|
|
111
|
+
Documents are `jsonb`. An indexed path becomes a **STORED** generated
|
|
112
|
+
column — PostgreSQL has no indexable virtual one — typed from the
|
|
113
|
+
schema, and its expression guards the member with `jsonb_typeof` inside
|
|
114
|
+
a `CASE`, because a generated column's expression must be IMMUTABLE and
|
|
115
|
+
a cast that can raise is not one: a document whose member is the wrong
|
|
116
|
+
JSON type stores `NULL` rather than failing its INSERT. Text columns and
|
|
117
|
+
text comparisons carry `COLLATE "C"`, which is byte order, which is what
|
|
118
|
+
SQLite's default `BINARY` collation is — without it the same half-open
|
|
119
|
+
prefix range would hold different rows on a server initialised in
|
|
120
|
+
another locale. Every table the dialect creates carries `rid bigserial`,
|
|
121
|
+
because a collection is a SEQUENCE and PostgreSQL has no per-row
|
|
122
|
+
identity that survives an UPDATE (`ctid` moves).
|
|
123
|
+
|
|
124
|
+
Three mapping decisions cost something, and each is stated rather than
|
|
125
|
+
worked around:
|
|
126
|
+
|
|
127
|
+
| Decision | Why | What it costs |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `integer` and `number` are both `numeric` | `bigint` would reject a document the model accepts — `3.5` in a member the schema types `integer` — inside a generated column's cast, and a storage decision that can refuse a valid document is not one | introspection cannot tell the two apart on the way back |
|
|
130
|
+
| a `boolean` member is `smallint` holding 1 or 0 | the shared forms compare a boolean against those integers (a type test, a projected pair's type name); a `boolean` column would make each an operator-resolution error rather than a row | introspection reads the column back as a number |
|
|
131
|
+
| a path the schema does not type is `jsonb` | there is no honest scalar type for it, and `capabilities.untypedColumns: false` says so | a comparison against such a member reads the document rather than the column: the same answer, unindexed |
|
|
132
|
+
|
|
133
|
+
Two engine differences the emitter had to be taught, rather than the
|
|
134
|
+
dialect papering over:
|
|
135
|
+
|
|
136
|
+
- **The type discriminator's vocabulary.** `jsonb_typeof` answers six
|
|
137
|
+
names; the row decoder reads SQLite's in JavaScript. The dialect maps
|
|
138
|
+
the two that differ (`string` → `text`, `boolean` → `true`/`false`)
|
|
139
|
+
and declares `numericTypeNames: ['number']`, which is what the
|
|
140
|
+
emitter's numeric guard reads instead of `('integer', 'real')`.
|
|
141
|
+
- **An external operand's parameter.** A PostgreSQL parameter's type is
|
|
142
|
+
resolved once for the whole statement, so one placeholder cannot be a
|
|
143
|
+
text member's operand in one branch and a numeric member's in
|
|
144
|
+
another — and the guard that keeps a row out of the wrong branch does
|
|
145
|
+
not stop the COERCION, because `AND` does not short-circuit. The
|
|
146
|
+
dialect declares `externalEncoding: 'json'`: the store binds the
|
|
147
|
+
value's JSON text, and the two branches compare in a space that holds
|
|
148
|
+
every scalar (text against text under `C`, number against number in
|
|
149
|
+
`jsonb`'s own numeric order).
|
|
150
|
+
|
|
151
|
+
An identifier longer than 63 bytes is REFUSED rather than emitted:
|
|
152
|
+
PostgreSQL truncates one silently, and two generated column names that
|
|
153
|
+
share a prefix would become one, which is an index quietly serving
|
|
154
|
+
another path.
|
|
155
|
+
|
|
156
|
+
### Database → model (`src/introspect.js`)
|
|
157
|
+
|
|
158
|
+
`store.introspect(options)` — and `introspectModel(connection, options)`
|
|
159
|
+
under it — reads a live database's catalog and derives the
|
|
160
|
+
`jaren-model` document that would produce it, beside a report of
|
|
161
|
+
everything it could not.
|
|
162
|
+
|
|
163
|
+
It is **read-only**, and a statement trace is what says so: every
|
|
164
|
+
statement it issues begins `SELECT` (or `PRAGMA`, on SQLite). A derived
|
|
165
|
+
model is an ANSWER; applying it is the migration planner's job and the
|
|
166
|
+
operator's decision, and the two are deliberately not one act.
|
|
167
|
+
|
|
168
|
+
It **never claims a byte-perfect round trip**. A physical shape carries
|
|
169
|
+
less than the model that made it, and each gap is a report row with a
|
|
170
|
+
stable code, an object and a detail, sorted, once:
|
|
171
|
+
|
|
172
|
+
| code | what the shape does not carry |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `document-members` | a document's unindexed members leave no trace, so the derived schema holds only what an index or the key names |
|
|
175
|
+
| `key-source` | a text key column cannot say which document member filled it; `identity: 'uuid'` is derived and `options.keys` supplies the pointer |
|
|
176
|
+
| `unmapped-view` | a view is not a shape a model can declare |
|
|
177
|
+
| `unmapped-table` | a table with neither a document column nor a key |
|
|
178
|
+
| `unmapped-column` | a generated column whose expression is not a member path this dialect wrote |
|
|
179
|
+
| `unmapped-index` | an index over an expression, a predicate, or a column no member path explains |
|
|
180
|
+
| `unmapped-type` | a column type no schema type maps back from — the member is derived untyped |
|
|
181
|
+
| `ambiguous-relation` | a foreign key says which entity it points at; which side declared the edge, and whether the other holds many, is not in the shape |
|
|
182
|
+
|
|
183
|
+
`strict: true` refuses instead of returning a partial model, because a
|
|
184
|
+
caller about to diff the result against a declared model needs to know
|
|
185
|
+
the difference is real.
|
|
186
|
+
|
|
187
|
+
What IS exact: every index, by name and by covered member paths in
|
|
188
|
+
order; the type of every member an index covers, placed at the DEPTH
|
|
189
|
+
the path names it at (so `$.nested.deep` rebuilds
|
|
190
|
+
`nested.properties.deep`, and the column type round-trips); a
|
|
191
|
+
database-allocated integer key; an entity's primary key, its unique and
|
|
192
|
+
indexed columns, and its foreign keys with their on-delete behaviour.
|
|
193
|
+
|
|
194
|
+
Two things make the two engines answer the same question. The dialect's
|
|
195
|
+
catalog statements hand back tables, columns, indexes and foreign keys
|
|
196
|
+
in ONE row shape — so the IR in the middle is engine-neutral — and only
|
|
197
|
+
the dialect reads its own generated-column expression back into a member
|
|
198
|
+
path (`memberPathOf`), because only the dialect wrote it. Indexes come
|
|
199
|
+
back sorted by name on both, because a physical shape carries no record
|
|
200
|
+
of the order a model declared them in and a derivation that inherited
|
|
201
|
+
one engine's listing order would not be a derivation.
|
|
202
|
+
|
|
203
|
+
### The portability matrix
|
|
204
|
+
|
|
205
|
+
Everything a dialect may decline, and what the store does instead. A
|
|
206
|
+
`—` is a feature the engine does not have; the store's behaviour is the
|
|
207
|
+
common fallback, never a skip.
|
|
208
|
+
|
|
209
|
+
| Capability | SQLite | PostgreSQL |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `jsonb` | yes (`BLOB` + the JSONB family) | yes (`jsonb`) |
|
|
212
|
+
| `generatedColumns` / `indexableGeneratedColumns` | yes, `VIRTUAL` | yes, `STORED` |
|
|
213
|
+
| `untypedColumns` | yes (`ANY`) | — (the member is read from the document) |
|
|
214
|
+
| `returning`, `upsert`, `savepoints` | yes | yes |
|
|
215
|
+
| `immediateTransactions` | yes (`BEGIN IMMEDIATE`) | — (`tx.beginImmediate` is `BEGIN`) |
|
|
216
|
+
| `groupByAlias` | yes | yes |
|
|
217
|
+
| `alterTableFull` | — | yes |
|
|
218
|
+
| `virtualTables` / `triggers` | yes (the R\*Tree mapping) | — (`physical: 'rtree'` maps back onto the four edge columns) |
|
|
219
|
+
| `pragmas` | yes (the closed set) | — (a server is configured by its operator) |
|
|
220
|
+
| `declaredSqlText` | yes | — (the drift check is the structural one) |
|
|
221
|
+
| `foreignKeysAlwaysOn` | — (set and verified per connection) | yes |
|
|
222
|
+
| `rowIdentity` | yes (`rowid`) | yes (the declared `rid` column) |
|
|
223
|
+
| `savepointStartsTransaction` | yes | — (a top-level transaction is `BEGIN`/`COMMIT`) |
|
|
224
|
+
|
|
225
|
+
And the two subsystems that are SQLite's own, declared on the
|
|
226
|
+
CONNECTION rather than the dialect because they are a library's
|
|
227
|
+
capability and not a syntax: the durable job queue (`jobs`) and the
|
|
228
|
+
change ledger (`changeCapture`). A store asked for either on a
|
|
229
|
+
connection that declares it absent is refused at open by name.
|
|
230
|
+
|
|
231
|
+
One behavioural difference is neither a capability nor a fallback,
|
|
232
|
+
because nothing can paper over it: **PostgreSQL aborts a transaction a
|
|
233
|
+
statement failed in**, and every later statement in it is `25P02` until
|
|
234
|
+
it ends. SQLite carries on. A body that means to catch a write's failure
|
|
235
|
+
and continue has to say so — `scope.transaction()` opens a savepoint the
|
|
236
|
+
failure rolls back to — and where it does not, the store reports the
|
|
237
|
+
engine's own condition under `JD2088` (`class: 'aborted'`, not
|
|
238
|
+
retryable) rather than a confusing failure of the next statement.
|
|
239
|
+
|
|
240
|
+
### The injected driver (`src/drivers/postgres.js`)
|
|
241
|
+
|
|
242
|
+
The driver imports nothing either. It takes a connection SOURCE —
|
|
243
|
+
`{ connect() }` answering `{ query(text, values), release?() }`, which a
|
|
244
|
+
`pg.Pool` is verbatim — acquires ONE client at open, holds it for the
|
|
245
|
+
store's life and releases it exactly once at `close()`. Holding one is
|
|
246
|
+
not a simplification: a connection owns one savepoint stack, and a store
|
|
247
|
+
that took a different pooled client per statement would have `BEGIN` on
|
|
248
|
+
one and `COMMIT` on another.
|
|
249
|
+
|
|
250
|
+
Statements are prepared by name, from a counter that is monotonic across
|
|
251
|
+
the PROCESS rather than across one adapter — a prepared statement
|
|
252
|
+
belongs to the session, and a pooled client's session outlives the store
|
|
253
|
+
that borrowed it, so a per-adapter counter would re-use a name for
|
|
254
|
+
different SQL and the server refuses that. The names are deallocated at
|
|
255
|
+
`close()`, so the session goes back to the pool clean. A cached plan
|
|
256
|
+
whose result type changed under it (`0A000`, after a migration) is
|
|
257
|
+
re-run unnamed rather than raised.
|
|
258
|
+
|
|
259
|
+
Three normalizations, each forced by the wire rather than chosen:
|
|
260
|
+
`int8` and `numeric` arrive as strings (they can exceed a double) and
|
|
261
|
+
become JavaScript numbers, which is the same ceiling SQLite's INTEGER
|
|
262
|
+
has; `json`/`jsonb` arrive parsed, because the client's type parsers are
|
|
263
|
+
the host's configuration, and the row decoder reads text; and a
|
|
264
|
+
JavaScript boolean is bound as 1 or 0, because a boolean member is 1 or
|
|
265
|
+
0 in this mapping.
|
|
266
|
+
|
|
267
|
+
`schema` is where the store lives. It is set on the acquired connection
|
|
268
|
+
AND given to the dialect, so the DDL and the catalog reads agree — one
|
|
269
|
+
value, two consumers. A migration's SHADOW replay is a second database,
|
|
270
|
+
which on a file engine is `:memory:` and on a server has to be another
|
|
271
|
+
schema: `migrate(..., { shadowDriver })` is where the host names one.
|
|
272
|
+
|
|
60
273
|
## What sits on top
|
|
61
274
|
|
|
62
275
|
- `src/ddl.js` — a normalized collection to its physical plan. Index
|
|
@@ -141,9 +354,13 @@ composition; `$exists`/`$empty`; `$starts-with`/`$ends-with`/
|
|
|
141
354
|
`$contains` on schema-typed string paths with literal patterns;
|
|
142
355
|
`$orderby` over singular schema-typed paths (`$dir`, `$empty`, no
|
|
143
356
|
collation); a top-level `$subsequence` window with literal bounds; the
|
|
144
|
-
top-level aggregates `$count`
|
|
145
|
-
|
|
146
|
-
|
|
357
|
+
top-level aggregates `$count` and `$sum`/`$avg`/`$min`/`$max` over a
|
|
358
|
+
singular schema-typed path; the whole-document projection that returns
|
|
359
|
+
the bare binding; ONE member path, projected as its value beside its
|
|
360
|
+
JSON type; and a nested SHAPE of objects, arrays, literals and member
|
|
361
|
+
paths, projected as one value/type pair per distinct leaf and rebuilt
|
|
362
|
+
by the decoder — never by parsing a JSON text the database assembled,
|
|
363
|
+
which could not tell an absent member from a present `null`.
|
|
147
364
|
|
|
148
365
|
Plus the spatial predicates a **derived** index makes decidable:
|
|
149
366
|
`$bbox-intersects` against a literal or external region, a geohash
|
|
@@ -176,11 +393,14 @@ mode, `knn`, beside native, row and set.
|
|
|
176
393
|
|
|
177
394
|
| construct | reason |
|
|
178
395
|
|---|---|
|
|
179
|
-
| `$let` bindings, `$fold`,
|
|
180
|
-
| a
|
|
396
|
+
| `$let` bindings, `$fold`, positional/window bindings | no equivalence proof exists yet; residual by default |
|
|
397
|
+
| a `$groupby` whose key is untyped or nullable, whose `$return` reads the binding, or whose `$orderby` names anything but a key | SQL's grouping and the engine's need not agree on an untyped key; after a grouping the binding holds the group's ROWS, which an object member cannot take |
|
|
398
|
+
| a window over the GROUPS, or an aggregate of them | the plan groups whole; a `LIMIT` over the groups would cut a different set |
|
|
399
|
+
| a `$for` binding nothing joins to — a cartesian product | the engine builds the product; a plan that emitted one by accident is the thing an equi-join graph exists to prevent |
|
|
400
|
+
| non-singular path expansion | one relation per binding in this version |
|
|
181
401
|
| `$match` and other unlisted operators, `$call` | no native spelling proven equivalent |
|
|
182
402
|
| `$orderby` with a `$collation` | a collation the dialect cannot reproduce is refused, not approximated |
|
|
183
|
-
|
|
|
403
|
+
| a projection the tree cannot rebuild: an operator over a member, a reference to the binding itself, a non-singular path, a projection with no member path at all | the WHOLE projection runs per row (the row residual) — pushed, ordered and windowed rows, projected by the engine; promoting the part that composes would answer a shape nobody asked for |
|
|
184
404
|
| string operators with an external pattern | the pattern's type is unknowable at plan time and the engine ERRORS on non-string patterns |
|
|
185
405
|
| comparisons where both sides are paths | join territory |
|
|
186
406
|
| array/object literals in comparisons | deep-equality has no guarded native form |
|
|
@@ -247,14 +467,21 @@ index-usable spelling and read every row of the collection.
|
|
|
247
467
|
|
|
248
468
|
The guards make the forms sound for typed AND untyped paths alike —
|
|
249
469
|
the schema type's job is choosing the generated COLUMN (the index),
|
|
250
|
-
never weakening the guard. Two documented preconditions
|
|
251
|
-
operators, aggregates
|
|
252
|
-
paths
|
|
253
|
-
|
|
254
|
-
non-string under a string operator —
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
470
|
+
never weakening the guard. Two documented preconditions. String
|
|
471
|
+
operators, aggregates and ordering are only promoted on schema-typed
|
|
472
|
+
paths that cannot hold `null` and are not boolean, because the engine
|
|
473
|
+
ERRORS on non-conforming operands where SQL would coerce or sort — a
|
|
474
|
+
`null` under an ordered key, a non-string under a string operator —
|
|
475
|
+
so a path whose schema admits `null` is a named residual for those
|
|
476
|
+
forms and the two engines keep answering alike over conforming data.
|
|
477
|
+
And the guards protect the JSON TYPE, not the VALUE: a typed
|
|
478
|
+
generated column carries SQLite affinity, so on an unvalidated store a
|
|
479
|
+
row that violates the schema — the string `'020'` under an `integer`
|
|
480
|
+
path — reads as the integer `20` in the column while `json_type` still
|
|
481
|
+
says text, and a pushed comparison answers rows the engine (comparing
|
|
482
|
+
the JSON value) does not. Keep `compileSchema` injected: with the hook
|
|
483
|
+
no such row is ever stored, and without it the pushdown is exact only
|
|
484
|
+
over documents that happen to conform.
|
|
258
485
|
|
|
259
486
|
**Bind-time diversion.** SQLite cannot bind a boolean, and a
|
|
260
487
|
`null`-valued external needs Jaren's null semantics, not SQL's. At
|
|
@@ -400,15 +627,19 @@ wants the engine's refusal for every row keeps `compileSchema` injected
|
|
|
400
627
|
and nothing else, which is what makes the two agree on every row it
|
|
401
628
|
does fetch.
|
|
402
629
|
|
|
403
|
-
**The probe.** A literal vector must be
|
|
630
|
+
**The probe.** A literal vector must be a DECLARED width at plan time
|
|
404
631
|
(another width is not recognized, and the reason says both widths). An
|
|
405
|
-
external probe is
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
632
|
+
external probe's width is only knowable when it is bound, so the plan
|
|
633
|
+
carries one ALTERNATIVE per declared width over the member — one emitted
|
|
634
|
+
statement each, prepared once and kept with the plan — and the bind
|
|
635
|
+
picks the alternative whose width the probe has. A `CASE` across the
|
|
636
|
+
columns would read every one of them per row, and a statement per call
|
|
637
|
+
would give up the prepared cache; neither is emitted. A bound value no
|
|
638
|
+
declared width takes — another width, a non-finite component, not an
|
|
639
|
+
array at all — DIVERTS the call to the full-collection residual, where
|
|
640
|
+
the engine answers what it answers everywhere (empty keys, so the
|
|
641
|
+
secondary keys order every row; or its own `JQ2001` for a non-array).
|
|
642
|
+
The plan never raises on the engine's behalf.
|
|
412
643
|
|
|
413
644
|
That diversion is correct and it is expensive: the residual reads every
|
|
414
645
|
document, and `explain()` still reports `knn`, because the plan is the
|
|
@@ -467,17 +698,66 @@ is the ENGINE running the caller's own document, decides:
|
|
|
467
698
|
|
|
468
699
|
| operator | what bounds the fetch |
|
|
469
700
|
|---|---|
|
|
701
|
+
| `$overlaps` over a declared interval | the half-open conjunction over the two bound columns, plus every row the operator RAISES on (below) |
|
|
470
702
|
| `$resample` (fill, calendar, `first`/`last`) | the spec's own `start`/`end`, plus the operand's `$where` |
|
|
471
703
|
| `$rolling` | the operand's `$where` alone — a window measured in time answers once per input instant |
|
|
472
|
-
| `$asof` with the collection on the RIGHT | the probes' own span (`at <= max` backward, `at >= min` forward, both sides under a `tolerance`) and a membership test over the probes' `by` keys |
|
|
704
|
+
| `$asof` with the collection on the RIGHT | the probes' own span (`at <= max` backward, `at >= min` forward, both sides under a `tolerance`) and a membership test over the probes' `by` keys, plus the ANCHOR below |
|
|
473
705
|
| `$asof` with the collection on the LEFT | nothing — a join answers once per LEFT row, so every left row is needed |
|
|
474
706
|
|
|
475
|
-
The as-of bound is
|
|
476
|
-
what `test/db/statement-count.test.js` pins: the
|
|
477
|
-
exists to refuse is one seek per left row.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
707
|
+
The as-of bound is a FIXED number of statements, whatever the probes
|
|
708
|
+
number, which is what `test/db/statement-count.test.js` pins: the
|
|
709
|
+
failure mode a batch exists to refuse is one seek per left row.
|
|
710
|
+
|
|
711
|
+
**The anchor.** Without a `tolerance` the open side has no bound the
|
|
712
|
+
probes imply: the row that answers the earliest probe is the last one at
|
|
713
|
+
or before it, however far back that lies. The tight bound is the data's
|
|
714
|
+
own, so the plan ASKS for it — `plan.seeks` carries one aggregate read
|
|
715
|
+
through the same declared index, and the statement binds its answer
|
|
716
|
+
through the typed slot (`emit.js`'s fourth `ParamSlot` kind), which
|
|
717
|
+
needs no bind-time type branch because the column's type is declared.
|
|
718
|
+
|
|
719
|
+
Per group the anchor is `MAX(at) WHERE at <= min(probes)`; the scalar is
|
|
720
|
+
the LEAST of those over the groups. A single global `MAX` would be
|
|
721
|
+
unsound — it can come from one group and drop another group's only
|
|
722
|
+
candidate — and matches only move forward as the probe does, so no row
|
|
723
|
+
below the fold can answer any probe. The comparison stays inclusive, so
|
|
724
|
+
rows sharing the anchor instant reach the kernel and its duplicate rule
|
|
725
|
+
decides among them. A seek that finds nothing binds its own probe: it
|
|
726
|
+
proved there is no row on that side. `forward` anchors the other end,
|
|
727
|
+
`nearest` both, and a `tolerance` asks for none — arithmetic already
|
|
728
|
+
closed both sides. What the anchor saves depends on where the probes
|
|
729
|
+
sit; `benchmark/series.js` publishes the candidate count beside the
|
|
730
|
+
timing rather than netting it out, over probes spread evenly across the
|
|
731
|
+
whole span, which is the anchor at its worst.
|
|
732
|
+
|
|
733
|
+
**`$overlaps` and the row that raises.** Two half-open spans share an
|
|
734
|
+
instant when each starts before the other ends, which over declared
|
|
735
|
+
bound columns is two comparisons the emitter already spells. What makes
|
|
736
|
+
it a pre-filter rather than an exact translation is the kernel's other
|
|
737
|
+
half: a span that is empty or reversed is not `false`, it RAISES
|
|
738
|
+
(`JQ2001`). A conjunction alone would drop `[500, 100)` for a probe of
|
|
739
|
+
`[100, 200)` — answering where the engine errors, which no pushdown may
|
|
740
|
+
do — so the statement keeps every row whose own span is inverted
|
|
741
|
+
(`start >= end`) and the operator decides over the candidates. That
|
|
742
|
+
disjunct compares two COLUMNS, which no index bounds, so the fetch
|
|
743
|
+
scans; what it still buys is that the pruned rows never reach the
|
|
744
|
+
engine at all. The other malformed cases are SCHEMA ones, exactly as the
|
|
745
|
+
spatial promotions' precondition is: the member must be typed as an
|
|
746
|
+
object whose `start` and `end` are both REQUIRED and both numeric, and a
|
|
747
|
+
write is validated against that schema, so a bound that is absent,
|
|
748
|
+
textual or null is a row the collection cannot hold. An unmapped pair,
|
|
749
|
+
an untyped one, or a probe that is not itself a half-open span pushes
|
|
750
|
+
nothing and names why. ROADMAP carries the declared-interval `CHECK`
|
|
751
|
+
that would make the conjunction exact and the fetch a seek.
|
|
752
|
+
|
|
753
|
+
**Reading a declared column without its guard.** A temporal refinement's
|
|
754
|
+
own bounds (`p: 'colCmp'`) carry no `json_type` beside them: the model
|
|
755
|
+
declared the column, the comparison only NARROWS, and the kernel decides
|
|
756
|
+
over what comes back. Without that, the statement parsed every scanned
|
|
757
|
+
row's document to discriminate a member the column already carried — at
|
|
758
|
+
the benchmark's largest shape that guard was most of the fetch's cost.
|
|
759
|
+
Narrowing is what makes it safe: SQL's own ordering keeps a row the
|
|
760
|
+
guard would have dropped, never the other way about.
|
|
481
761
|
|
|
482
762
|
**A group with no instant.** A row whose instant member is missing or
|
|
483
763
|
is not a number groups under SQL `NULL`; the kernel REFUSES such a row
|
|
@@ -571,8 +851,9 @@ collection binding, so the planner passes that binding's **name** in:
|
|
|
571
851
|
wrapped under any other name every reference would read as an external,
|
|
572
852
|
the determinism rule would reject the fragment, and the hatch would
|
|
573
853
|
silently not engage — no error and no reason in `explain()`.
|
|
574
|
-
Registration is keyed by `
|
|
575
|
-
|
|
854
|
+
Registration is keyed by `semanticKey(fragment)` (the
|
|
855
|
+
order-insensitive identity from `@jarenjs/core/object`) so identical
|
|
856
|
+
fragments share one registration, and the planner MUST
|
|
576
857
|
produce a correct plan with the capability disabled (tested that way).
|
|
577
858
|
Preference order: native SQL → deterministic function → residual, and
|
|
578
859
|
`explain()` names the choice. **Index-form UDFs are deliberately not
|
|
@@ -585,11 +866,15 @@ UDF-expression indexes.
|
|
|
585
866
|
### The statement cache
|
|
586
867
|
|
|
587
868
|
A caller of the core primitives, not an eighth implementation:
|
|
588
|
-
`
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
869
|
+
`createSemanticCache` keyed by the whole discriminating tuple — the
|
|
870
|
+
document plus collection, dialect, strictness, the pushdown switch and
|
|
871
|
+
the profile — where the identity is the tuple's COMPLETE
|
|
872
|
+
serialization, never a fingerprint of it: a 32-bit content hash
|
|
873
|
+
collides after tens of thousands of documents, and a collision here
|
|
874
|
+
answers one query with another query's plan and rows (the
|
|
875
|
+
cache-identity test exists to keep that key abolished).
|
|
876
|
+
`store.stats()` exposes hits, misses and evictions so the cache is
|
|
877
|
+
proven rather than assumed.
|
|
593
878
|
`store.stats()` exposes hits, misses and evictions, so the cache is
|
|
594
879
|
proven rather than assumed.
|
|
595
880
|
|
|
@@ -621,8 +906,12 @@ diff engine and maps operations to minimal statements (column writes,
|
|
|
621
906
|
fallback); inserts batch parent-first, deletes run child-first,
|
|
622
907
|
foreign-key cycles among the changed set are `JD0040`, and a declared
|
|
623
908
|
`version` property turns every update into an optimistic
|
|
624
|
-
`WHERE version = ?` with `JD2040` on conflict.
|
|
625
|
-
|
|
909
|
+
`WHERE version = ?` with `JD2040` on conflict. A save that FAILS leaves
|
|
910
|
+
the tracker exactly as it was, so it retries; a save whose statements
|
|
911
|
+
all succeed advances at once — inside an enclosing transaction too,
|
|
912
|
+
where the database already holds the rows — and registers the exact
|
|
913
|
+
withdrawal of that advance on the owning scope, whose rollback takes it
|
|
914
|
+
back so the retry plans the same statements again.
|
|
626
915
|
|
|
627
916
|
## The migration engine, relationally (`src/migrate.js`, `src/cli.js`)
|
|
628
917
|
|
|
@@ -711,22 +1000,40 @@ re-query to keep a promise rather than a number.
|
|
|
711
1000
|
|
|
712
1001
|
## Durable runs and the job queue (`src/jobs.js`, `src/dag-job.js`)
|
|
713
1002
|
|
|
714
|
-
The queue's
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1003
|
+
The queue's correctness story has two halves. The claim UPDATE selects
|
|
1004
|
+
the earliest eligible row (pending, failed, or an expired lease — so
|
|
1005
|
+
recovery IS the next claim, not a sweeper) whose kind the worker
|
|
1006
|
+
registered, MINTS the fence — a fresh opaque `lease_token` and the next
|
|
1007
|
+
`lease_generation` — sets the row leased with a deadline, and RETURNs
|
|
1008
|
+
it. One statement is one transaction, so no two workers claim the same
|
|
1009
|
+
job without any distributed lock. Every later settling transition —
|
|
1010
|
+
renew, checkpoint save, complete, fail — then wears the fence:
|
|
1011
|
+
`state='leased' AND lease_token=? AND lease_until > ?`, the token plus
|
|
1012
|
+
CURRENT validity, never the owner (one worker reuses one owner for
|
|
1013
|
+
every attempt it ever makes, so an owner cannot say which attempt is
|
|
1014
|
+
speaking). A lease is immutable — `renew` answers a replacement and the
|
|
1015
|
+
superseded token settles nothing — and a guard that matches nothing is
|
|
1016
|
+
a coded refusal naming which of the three reasons applied (`JD2065`
|
|
1017
|
+
settled/unknown, `JD2066` superseded, `JD2067` expired), never a silent
|
|
1018
|
+
`false`. A renewal that fails for any OTHER reason is a storage problem
|
|
1019
|
+
the next renewal may survive; only the three fence codes prove loss.
|
|
1020
|
+
Checkpoint rows are stamped with the attempt's generation: `load` reads
|
|
1021
|
+
up to it, and a settlement prunes no further, so a stale attempt cannot
|
|
1022
|
+
erase a live one's work. Execution stays at-least-once; settlement is
|
|
1023
|
+
exactly-once AGAINST THE STORE (proven by four workers on four
|
|
1024
|
+
connections over one WAL file) — an external effect a handler already
|
|
1025
|
+
made is still the handler's to make idempotent. Ownership follows the
|
|
1026
|
+
handle: root `store.jobs.*` and all worker control I/O take the store
|
|
1027
|
+
gate, while `tx.jobs` runs as its exact transaction scope — the
|
|
1028
|
+
transactional outbox. The `@jarenjs/flow` composition injects
|
|
1029
|
+
`compileDag` (db never imports flow — an import-graph test enforces it)
|
|
1030
|
+
and binds a per-ATTEMPT checkpoint store that follows the current
|
|
1031
|
+
lease; a DAG run's completion records the result, marks the job done
|
|
1032
|
+
and prunes the checkpoint rows in ONE transaction, so a crash resumes
|
|
1033
|
+
from its checkpointed nodes rather than restarting, and a resume must
|
|
1034
|
+
match the persisted workflow revision and input hash (`JD2069`).
|
|
1035
|
+
Non-goals stated plainly: a shared SQLite file over a network
|
|
1036
|
+
filesystem is not a safe coordination substrate.
|
|
730
1037
|
|
|
731
1038
|
## The wasm driver (`src/drivers/wasm.js`)
|
|
732
1039
|
|
|
@@ -746,3 +1053,45 @@ studio's throwaway-store operation, holds the wasm build to the
|
|
|
746
1053
|
JavaScript engine's recorded answers in every one of those engines,
|
|
747
1054
|
OPFS or not, because an entry seeds its own store and needs execution,
|
|
748
1055
|
not persistence.
|
|
1056
|
+
|
|
1057
|
+
## Operating the store (`src/pragmas.js`, `src/maintenance.js`, `src/backup.js`, `src/cancellation.js`, `src/errors.js`)
|
|
1058
|
+
|
|
1059
|
+
The operability surface is five small modules and one rule each,
|
|
1060
|
+
consumed by `src/store.js` and never reached by a query:
|
|
1061
|
+
|
|
1062
|
+
- `src/pragmas.js` — the closed table of configurable connection
|
|
1063
|
+
pragmas (option name, SQL spelling, validator, read-back mapping,
|
|
1064
|
+
default, the two store-kind flags). The open sequence applies the
|
|
1065
|
+
requests in table order and reads every declared pragma back; an
|
|
1066
|
+
explicit request the engine did not take refuses the open, a default
|
|
1067
|
+
it could not take is reported as read. The dialect spells exactly one
|
|
1068
|
+
set statement and one read statement for it.
|
|
1069
|
+
- `src/maintenance.js` — `checkpoint`, `integrityCheck`,
|
|
1070
|
+
`foreignKeyCheck`, `optimize` over a connection, answering the engine's
|
|
1071
|
+
own row as typed data; availability per operation is the binding's
|
|
1072
|
+
declaration and, for the two that write, the store's read-only flag —
|
|
1073
|
+
refused by code exactly where the report says `false`.
|
|
1074
|
+
- `src/backup.js` — the online backup over the driver's primitive triple
|
|
1075
|
+
(`copy`, `rename`, `remove`; the Node binding's), written to a
|
|
1076
|
+
temporary sibling and renamed only at verified completion, cleaned up
|
|
1077
|
+
on every other branch. The root closure imports no builtin: the
|
|
1078
|
+
primitives are the driver's.
|
|
1079
|
+
- `src/cancellation.js` — the one check every lifecycle runs between its
|
|
1080
|
+
units of work (a statement, a row, a step, a batch, a page), on the
|
|
1081
|
+
clock the caller was given; each lifecycle owns its abort code, the
|
|
1082
|
+
deadline code is one. `capabilities.cancellation` states the
|
|
1083
|
+
granularity per lifecycle and that nothing interrupts mid-statement.
|
|
1084
|
+
- `src/errors.js` — `classifyDriverError`, the one table every path
|
|
1085
|
+
consults (writes, the query engines through their boundary and the
|
|
1086
|
+
cursor's `wrap`, maintenance and backup under their own code, the open
|
|
1087
|
+
sequence): class, retryability, code. The query path's only special
|
|
1088
|
+
case is the int64 overflow of a pushed aggregate, answered by the
|
|
1089
|
+
engine as a coded residual.
|
|
1090
|
+
|
|
1091
|
+
The open path itself creates or verifies the shape inside an IMMEDIATE
|
|
1092
|
+
transaction with idempotent DDL (one dialect spelling, applied on the
|
|
1093
|
+
open path only) and retries once on a classed busy failure, so two
|
|
1094
|
+
processes creating one fresh file never meet the deferred-upgrade
|
|
1095
|
+
`SQLITE_BUSY` the busy handler cannot retry. The job queue's
|
|
1096
|
+
administration (`page`, `cancel`, `requeue`, `sweep`) lives in
|
|
1097
|
+
`src/jobs.js` beside the fence it authorises through (JOBS-FORMAT §10).
|