@lunora/sql-store 1.0.0-alpha.16 → 1.0.0-alpha.161
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/LICENSE.md +6 -0
- package/README.md +10 -8
- package/dist/dialect.d.mts +161 -41
- package/dist/dialect.d.ts +161 -41
- package/dist/dialect.mjs +0 -1
- package/dist/index.d.mts +374 -129
- package/dist/index.d.ts +374 -129
- package/dist/index.mjs +1 -2
- package/dist/packem_shared/SEARCH_STATE_TABLE-CTjm9HUb.mjs +1 -0
- package/dist/packem_shared/backfillSqlSearchIndexes-BWtqC8SB.mjs +1 -0
- package/dist/packem_shared/createSqlCtxDb-LjE6a4t0.mjs +7 -0
- package/dist/packem_shared/ctx-db-search-CpGgDAhl.mjs +1 -0
- package/dist/packem_shared/ctx-db-search-state-TMs6Zlae.mjs +1 -0
- package/dist/packem_shared/decodeBigint-BQtyR2O3.mjs +1 -0
- package/dist/packem_shared/decodeGlobalRow-P2Z1dMjU.mjs +1 -0
- package/dist/packem_shared/runSqlAggregateMigrations-ByqTfw-y.mjs +1 -0
- package/dist/packem_shared/value-codec-CBr0_L1N.mjs +1 -0
- package/package.json +3 -2
- package/dist/packem_shared/createSqlCtxDb-BlGq23Wp.mjs +0 -1784
- package/dist/packem_shared/decodeBigint-Dedu92k4.mjs +0 -69
package/dist/index.d.ts
CHANGED
|
@@ -1,139 +1,185 @@
|
|
|
1
|
-
import { ServerDefaultContextLike, DatabaseWriterLike, SchedulerLike, SchemaLike,
|
|
2
|
-
import {
|
|
1
|
+
import { TableDefinitionLike, ServerDefaultContextLike, DatabaseWriterLike, CrossShardReadArgs, QueryPage, SchedulerLike, SchemaLike, CdcChange, ValidatorLike } from '@lunora/shard-engine';
|
|
2
|
+
import { SqlRunResult, SqlDialect } from "./dialect.js";
|
|
3
3
|
export type { SqlExec } from "./dialect.js";
|
|
4
4
|
import 'drizzle-orm';
|
|
5
5
|
/**
|
|
6
|
-
* Async SQL surface the D1 ORM needs: `all` for reads, `run` for writes.
|
|
7
|
-
* Satisfied by a `D1Session`/`D1Client` in production and a `node:sqlite`
|
|
8
|
-
* adapter in tests, so the query logic runs against a real SQLite engine.
|
|
9
|
-
*/
|
|
6
|
+
* Async SQL surface the D1 ORM needs: `all` for reads, `run` for writes.
|
|
7
|
+
* Satisfied by a `D1Session`/`D1Client` in production and a `node:sqlite`
|
|
8
|
+
* adapter in tests, so the query logic runs against a real SQLite engine.
|
|
9
|
+
*/
|
|
10
10
|
interface SqlCtxExec {
|
|
11
11
|
all: (sql: string, parameters: ReadonlyArray<unknown>) => Promise<Record<string, unknown>[]>;
|
|
12
|
+
batch?: (statements: ReadonlyArray<{
|
|
13
|
+
params: ReadonlyArray<unknown>;
|
|
14
|
+
sql: string;
|
|
15
|
+
}>) => Promise<void>;
|
|
12
16
|
run: (sql: string, parameters: ReadonlyArray<unknown>) => Promise<SqlRunResult | void>;
|
|
13
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Decode a SELECTed row back into a document: `id` → `_id`, `_creationTime`
|
|
20
|
+
* preserved, and every column run through the shared {@link sqliteDecode} so the
|
|
21
|
+
* stored form is reversed back into its JS shape. Exported so the data-browser
|
|
22
|
+
* (`introspect.ts`) and admin export/import paths share the exact same decode.
|
|
23
|
+
*
|
|
24
|
+
* The decode is engine-agnostic: every backend stores SQLite-shaped values
|
|
25
|
+
* (boolean → 1/0, JSON → text, bigint → an order-preserving text key), and `sqliteDecode` is
|
|
26
|
+
* robust to a driver returning either the stored string OR a natively-parsed
|
|
27
|
+
* value (e.g. mysql2 returns JSON columns pre-parsed) — so the same decoder is
|
|
28
|
+
* correct on SQLite, Postgres and MySQL.
|
|
29
|
+
*/
|
|
30
|
+
declare const decodeGlobalRow: (definition: TableDefinitionLike, row: Record<string, unknown>) => Record<string, unknown>;
|
|
14
31
|
interface SqlCtxDbOptions {
|
|
15
32
|
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
* Resolved request auth handed to `.serverDefault(fn)` column factories so
|
|
34
|
+
* server-trusted columns (owner/tenant ids) stamp from the verified caller,
|
|
35
|
+
* never the client. The generated worker passes the per-request identity;
|
|
36
|
+
* absent it, server-trusted columns stamp the anonymous slice (`userId: null`).
|
|
37
|
+
*/
|
|
21
38
|
auth?: ServerDefaultContextLike["auth"];
|
|
22
39
|
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
* Opt into change-data-capture: when `true`, every committed write appends a
|
|
41
|
+
* post-image to the `__cdc_log` table (created lazily alongside the other
|
|
42
|
+
* companion tables). Backs CDC streaming export for `.global()` tables — the
|
|
43
|
+
* log is for export/CDC consumers, NOT point-in-time recovery: D1's PITR is
|
|
44
|
+
* the platform's own Time Travel (`wrangler d1 time-travel restore`), an
|
|
45
|
+
* atomic restore, not a changelog replay. Leave undefined for zero-cost
|
|
46
|
+
* legacy behaviour.
|
|
47
|
+
*/
|
|
31
48
|
cdc?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* How long `.global()` changelog entries are kept, in milliseconds. Absent
|
|
51
|
+
* (the default) means forever — the log grows for the life of the database.
|
|
52
|
+
*
|
|
53
|
+
* Opt-in for the same reason the shard-local windows are, and the reason is
|
|
54
|
+
* not caution: this log's streaming-export consumers hold opaque cursors
|
|
55
|
+
* issued outside this deployment, and nothing here can see where they sit.
|
|
56
|
+
* A default window would be a guess whose failure mode is silent data loss
|
|
57
|
+
* in someone's warehouse. A deployment that wants the log bounded states a
|
|
58
|
+
* window it knows covers its consumers; `.global()` shape pollers are then
|
|
59
|
+
* protected exactly, by the floor this read path reports rather than by the
|
|
60
|
+
* window. See `sweepSqlCdcRetention`.
|
|
61
|
+
*
|
|
62
|
+
* Time rather than rows because the log is SHARED: every shard in every
|
|
63
|
+
* region writes it, so a row count is not a bound any single consumer can
|
|
64
|
+
* reason about, while "older than N" is exactly what they compare their own
|
|
65
|
+
* lag against.
|
|
66
|
+
*/
|
|
67
|
+
cdcRetentionMs?: number;
|
|
32
68
|
clock?: () => number;
|
|
33
69
|
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
70
|
+
* Cross-shard counter for **reverse cross-backend relations** — the `_count`
|
|
71
|
+
* mirror of the `crossShardReader` option below.
|
|
72
|
+
*/
|
|
37
73
|
crossShardCounter?: DatabaseWriterLike["count"];
|
|
38
74
|
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
75
|
+
* Optional cross-shard reader for **reverse cross-backend relations**: a
|
|
76
|
+
* `.global()` (D1) parent loading a shard-local (`.shardBy()`/root) child.
|
|
77
|
+
* Such a child's rows are partitioned across every shard DO, so the local D1
|
|
78
|
+
* writer can't resolve it. When provided, the relation loader routes the
|
|
79
|
+
* child's read through this (the host wires it to the Query Coordinator's
|
|
80
|
+
* RLS-correct `fanOut`, with identity forwarded so each shard applies its own
|
|
81
|
+
* RLS). Absent it, loading such a relation throws a clear "not supported"
|
|
82
|
+
* error (legacy behaviour). The forward direction (shard-local parent →
|
|
83
|
+
* global child) and same-backend relations never touch this.
|
|
84
|
+
*
|
|
85
|
+
* Takes {@link CrossShardReadArgs}, not `QueryArgs`: the hop is JSON, so the
|
|
86
|
+
* RLS filters are handed over as data (see that type's docblock).
|
|
87
|
+
*/
|
|
88
|
+
crossShardReader?: (table: string, args: CrossShardReadArgs) => Promise<QueryPage>;
|
|
50
89
|
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
90
|
+
* The SQL dialect that shapes every statement (identifier quoting, value
|
|
91
|
+
* encode/decode, column types, upserts, RETURNING vs affected-rows).
|
|
92
|
+
* `@lunora/d1` passes its `sqliteDialect`; the PlanetScale/Hyperdrive backend
|
|
93
|
+
* passes its Postgres/MySQL dialect. Required — the core is engine-blind.
|
|
94
|
+
*/
|
|
56
95
|
dialect: SqlDialect;
|
|
57
96
|
exec: SqlCtxExec;
|
|
58
97
|
idGenerator?: () => string;
|
|
59
98
|
/**
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
99
|
+
* Ceiling on the number of child join keys a relation-crossing `where`
|
|
100
|
+
* predicate may materialize before the semijoin pre-resolver fails closed
|
|
101
|
+
* (`relation predicate … exceeding the N-key limit`). D1 has no EXISTS
|
|
102
|
+
* push-down, so an overflow here can only fail closed — never truncate the
|
|
103
|
+
* `IN (...)` and silently mis-match. Defaults to the pre-resolver's shared
|
|
104
|
+
* key cap when omitted.
|
|
105
|
+
*/
|
|
67
106
|
maxRelationKeys?: number;
|
|
68
107
|
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
108
|
+
* Object identifying the database this store provisions, so the one-shot
|
|
109
|
+
* `CREATE TABLE/INDEX IF NOT EXISTS` sweep is shared by every ctx-db built
|
|
110
|
+
* against it instead of repeating per instance.
|
|
111
|
+
*
|
|
112
|
+
* Hosts build a ctx-db **per request** (the writer captures the caller's
|
|
113
|
+
* identity and D1 bookmark), so without this the sweep — one round trip per
|
|
114
|
+
* global table plus one per index — runs again on the first `.global()`
|
|
115
|
+
* access of every request. On a 50-table schema that is ~1s in `lunora dev`
|
|
116
|
+
* and a real latency floor in production.
|
|
117
|
+
*
|
|
118
|
+
* WHAT TO PASS: something that outlives the request AND names one database
|
|
119
|
+
* (and one configuration). The D1 binding off `env` and the declaration
|
|
120
|
+
* object a host builds once both qualify; the result of a per-request
|
|
121
|
+
* factory does not — it keys the memo on a fresh object every time and
|
|
122
|
+
* shares nothing (which is silent, and looks exactly like not passing it).
|
|
123
|
+
*
|
|
124
|
+
* INVARIANT, unenforced: one scope ⇒ one `schema`, `dialect` and `cdc`. The
|
|
125
|
+
* memoised run closes over the FIRST writer's, so a later writer sharing a
|
|
126
|
+
* scope with `cdc: true` behind one with `cdc: false` never gets
|
|
127
|
+
* `__cdc_log`. Hosts pass app-level constants for all three, which is why
|
|
128
|
+
* this is a note rather than a composite key.
|
|
129
|
+
*
|
|
130
|
+
* Two behaviours change with a shared memo, both benign and both worth
|
|
131
|
+
* knowing: the DDL runs in the FIRST writer's D1 session, so a later
|
|
132
|
+
* session's first read is no longer implicitly pinned behind a write of its
|
|
133
|
+
* own (on a replicated binding that can mean one transient stale read on a
|
|
134
|
+
* freshly-provisioned database); and a database reset out from under a live
|
|
135
|
+
* isolate is no longer healed by the next request, since the entry lives as
|
|
136
|
+
* long as the scope does.
|
|
137
|
+
*
|
|
138
|
+
* Omit it and the memo stays per ctx-db — slower, never wrong, and what the
|
|
139
|
+
* tests want (they pair a fresh database with a reused schema object).
|
|
140
|
+
*/
|
|
141
|
+
provisionScope?: object;
|
|
142
|
+
/**
|
|
143
|
+
* Scheduler exposed to global-table trigger handlers as `ctx.scheduler`.
|
|
144
|
+
* Absent it, `ctx.scheduler` is a stub that throws on use — pass one when
|
|
145
|
+
* triggers on `.global()` tables need to enqueue follow-up work.
|
|
146
|
+
*/
|
|
73
147
|
scheduler?: SchedulerLike;
|
|
74
148
|
schema: SchemaLike;
|
|
75
149
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Decode a SELECTed row back into a document: `id` → `_id`, `_creationTime`
|
|
78
|
-
* preserved, and every column run through the shared {@link sqliteDecode} so the
|
|
79
|
-
* stored form is reversed back into its JS shape. Exported so the data-browser
|
|
80
|
-
* (`introspect.ts`) and admin export/import paths share the exact same decode.
|
|
81
|
-
*
|
|
82
|
-
* The decode is engine-agnostic: every backend stores SQLite-shaped values
|
|
83
|
-
* (boolean → 1/0, JSON → text, bigint → decimal string), and `sqliteDecode` is
|
|
84
|
-
* robust to a driver returning either the stored string OR a natively-parsed
|
|
85
|
-
* value (e.g. mysql2 returns JSON columns pre-parsed) — so the same decoder is
|
|
86
|
-
* correct on SQLite, Postgres and MySQL.
|
|
87
|
-
*/
|
|
88
|
-
declare const decodeGlobalRow: (definition: TableDefinitionLike, row: Record<string, unknown>) => Record<string, unknown>;
|
|
89
|
-
declare const runSqlGlobalTableMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
|
|
90
|
-
/**
|
|
91
|
-
* Materialize the `__agg_<index>` companion tables for every declared
|
|
92
|
-
* `aggregateIndex` on a global table. Global tables in Lunora ship their own
|
|
93
|
-
* DDL — counter tables are opt-in so production hosts can decide where they
|
|
94
|
-
* live. Tests and dev hosts can call this once after their schema migration to
|
|
95
|
-
* unlock O(1) counts.
|
|
96
|
-
*
|
|
97
|
-
* Idempotent (`CREATE TABLE IF NOT EXISTS`).
|
|
98
|
-
*/
|
|
99
|
-
declare const runSqlAggregateMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
|
|
100
|
-
/**
|
|
101
|
-
* Materialize the `__rank_<index>` companion tables for every declared
|
|
102
|
-
* `rankIndex` on a global table. Mirrors `runSqlAggregateMigrations` — same
|
|
103
|
-
* opt-in pattern so production hosts decide whether to spend the DDL.
|
|
104
|
-
*
|
|
105
|
-
* Idempotent (`CREATE TABLE IF NOT EXISTS` + `createIndexIfNotExists`).
|
|
106
|
-
*/
|
|
107
|
-
declare const runSqlRankMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
|
|
108
|
-
/**
|
|
109
|
-
* Materialize the `__fts_<index>` FTS5 shadow tables for every declared
|
|
110
|
-
* `.searchIndex()` on a global table. Mirrors `runSqlAggregateMigrations` — same
|
|
111
|
-
* opt-in pattern so production hosts decide whether to spend the DDL. Only runs
|
|
112
|
-
* on engines that ship FTS5 (D1 does; the `node:sqlite` test runner doesn't,
|
|
113
|
-
* where `.search()` transparently falls back to a scan). `__text__` holds the
|
|
114
|
-
* indexed field; `__id__` (UNINDEXED) joins back to the row.
|
|
115
|
-
*
|
|
116
|
-
* Idempotent (`CREATE VIRTUAL TABLE IF NOT EXISTS`).
|
|
117
|
-
*/
|
|
118
|
-
declare const runSqlSearchMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
|
|
119
|
-
/** One change-data-capture entry: a committed mutation, in monotonic `seq` order. Mirrors the DO twin. */
|
|
120
|
-
interface CdcChange {
|
|
121
|
-
/** Post-image document for insert/update; absent for delete (the `id` identifies the removed row). */
|
|
122
|
-
doc?: Record<string, unknown>;
|
|
123
|
-
id: string;
|
|
124
|
-
op: "delete" | "insert" | "update";
|
|
125
|
-
/** Monotonic per-database cursor — strictly increasing, never reused. */
|
|
126
|
-
seq: number;
|
|
127
|
-
table: string;
|
|
128
|
-
/** Wall-clock millis when the change committed (the ctx-db `clock`). */
|
|
129
|
-
ts: number;
|
|
130
|
-
}
|
|
131
150
|
/** Create the `__cdc_log` table. Idempotent; only run when CDC is enabled. */
|
|
132
151
|
declare const runSqlCdcMigration: (exec: SqlCtxExec, dialect: SqlDialect) => Promise<void>;
|
|
133
152
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
|
|
153
|
+
* Delete `.global()` changelog entries older than `retentionMs`, at most
|
|
154
|
+
* {@link GLOBAL_CDC_SWEEP_MAX_ROWS} per pass, and only if this writer wins the lease.
|
|
155
|
+
*
|
|
156
|
+
* **Time, not rows.** The shard-local twin bounds its log by row count because
|
|
157
|
+
* one shard owns it and a row count is a memory bound on that one object. This
|
|
158
|
+
* log is shared, so a row count means nothing to any individual consumer — but
|
|
159
|
+
* "older than N" is directly what every consumer needs to reason about, and it
|
|
160
|
+
* is the unit an operator can compare against their own connector's lag.
|
|
161
|
+
*
|
|
162
|
+
* **Which consumers this can strand, and what protects each:**
|
|
163
|
+
*
|
|
164
|
+
* - `.global()` shape pollers hold in-memory cursors this store cannot see. They
|
|
165
|
+
* are protected EXACTLY rather than approximately: {@link readSqlCdcChangedTables}
|
|
166
|
+
* reports the retained floor, and a poller below it treats the tick as "no
|
|
167
|
+
* visibility" and re-reads every shape. That is the same self-healing path a
|
|
168
|
+
* changelog error already takes, so a trimmed poller is slow for one tick, not
|
|
169
|
+
* wrong. No cursor registry, no assumption about how far behind a shard can be.
|
|
170
|
+
* - Streaming-export / warehouse consumers hold opaque cursors issued outside
|
|
171
|
+
* this deployment. Nothing here can see them, so nothing here guesses:
|
|
172
|
+
* {@link readSqlCdcChanges} refuses a page below the floor rather than serving
|
|
173
|
+
* the surviving tail, and retention stays OFF unless an operator states a
|
|
174
|
+
* window they know covers their connector.
|
|
175
|
+
*/
|
|
176
|
+
declare const sweepSqlCdcRetention: (exec: SqlCtxExec, dialect: SqlDialect, retentionMs: number, now: number) => Promise<void>;
|
|
177
|
+
/** Oldest `seq` still retained in the `.global()` changelog, or `undefined` when it is empty. */
|
|
178
|
+
declare const readSqlCdcFloor: (exec: SqlCtxExec, dialect: SqlDialect) => Promise<number | undefined>;
|
|
179
|
+
/**
|
|
180
|
+
* Read changelog entries newer than `sinceSeq` in commit order, up to `limit`
|
|
181
|
+
* (clamped to [1, 10000]); plus the cursor to resume from.
|
|
182
|
+
*/
|
|
137
183
|
declare const readSqlCdcChanges: (exec: SqlCtxExec, options: {
|
|
138
184
|
limit?: number;
|
|
139
185
|
sinceSeq?: number;
|
|
@@ -141,34 +187,233 @@ declare const readSqlCdcChanges: (exec: SqlCtxExec, options: {
|
|
|
141
187
|
changes: CdcChange[];
|
|
142
188
|
cursor: number;
|
|
143
189
|
}>;
|
|
144
|
-
/**
|
|
145
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Which tables the changelog recorded a write to after `sinceSeq`, plus the
|
|
192
|
+
* cursor to resume from. Metadata only — it reads no `doc`, so its cost is a
|
|
193
|
+
* grouped scan over an index range rather than the size of the documents in it.
|
|
194
|
+
*
|
|
195
|
+
* The `.global()` shape poll asks this once per tick for the whole shard, and a
|
|
196
|
+
* shape whose table is absent from the answer skips its membership read
|
|
197
|
+
* entirely.
|
|
198
|
+
*
|
|
199
|
+
* **One statement, and the cursor comes out of the same rows as the tables.**
|
|
200
|
+
* Reading the head separately — in either order — opens a window where a write
|
|
201
|
+
* commits between the two round trips and ends up absent from `tables` while
|
|
202
|
+
* sitting at or below the adopted `cursor`, which loses it for good. Deriving
|
|
203
|
+
* the cursor as the max `seq` actually returned closes that window by
|
|
204
|
+
* construction: the caller can never advance past a row this scan did not see.
|
|
205
|
+
*
|
|
206
|
+
* **Late commits.** Postgres and MySQL allocate `seq` at INSERT, not at commit,
|
|
207
|
+
* so an append holding `N` can commit after one holding `N + 1`. A poll that
|
|
208
|
+
* adopts `N + 1` while `N` is still in flight skips `N` until the next resync
|
|
209
|
+
* pass. So on those engines the cursor stops below the first HOLE in `seq`
|
|
210
|
+
* rather than at the highest row seen: a hole is an allocated `seq` that is not
|
|
211
|
+
* (yet) visible, which is exactly what an in-flight append looks like. Tables
|
|
212
|
+
* above the hole are still reported, so nothing is delayed — rows past the
|
|
213
|
+
* cursor are just reported again next tick.
|
|
214
|
+
*
|
|
215
|
+
* A hole can also be permanent (a failed append burns its `seq`), and a cursor
|
|
216
|
+
* pinned under one forever would re-report everything above it on every tick.
|
|
217
|
+
* So a hole only holds the cursor while the first row after it is younger than
|
|
218
|
+
* {@link CDC_LATE_COMMIT_WINDOW_MS}: that row's append started before its own
|
|
219
|
+
* `seq` was allocated, which is after the hole's, so once it is that old the
|
|
220
|
+
* hole's owner has either committed (and is visible) or never will. An append
|
|
221
|
+
* slower than the window falls back to the resync interval, as before. A MySQL
|
|
222
|
+
* `auto_increment_increment` above 1 makes every step a hole, which holds the
|
|
223
|
+
* cursor one window behind the head: correct, just less narrowing.
|
|
224
|
+
*
|
|
225
|
+
* D1 skips all of this: single writer, `AUTOINCREMENT`, and `withSession` gives
|
|
226
|
+
* both reads one snapshot, so there is no hole to wait for — and D1 bills rows
|
|
227
|
+
* read.
|
|
228
|
+
*/
|
|
229
|
+
declare const readSqlCdcChangedTables: (exec: SqlCtxExec, sinceSeq: number, dialect: SqlDialect, options?: {
|
|
230
|
+
cursorOnly?: boolean;
|
|
231
|
+
now?: number;
|
|
232
|
+
retained?: boolean;
|
|
233
|
+
}) => Promise<{
|
|
234
|
+
cursor: number;
|
|
235
|
+
floor?: number;
|
|
236
|
+
tables: string[];
|
|
237
|
+
}>;
|
|
146
238
|
declare const createSqlCtxDb: (options: SqlCtxDbOptions) => DatabaseWriterLike;
|
|
239
|
+
/**
|
|
240
|
+
* Auto-provision every `.global()` table from the schema: `CREATE TABLE IF NOT
|
|
241
|
+
* EXISTS` with the physical `id`/`_creationTime` columns plus a typed column per
|
|
242
|
+
* declared field, then its secondary and `.unique()` indexes. This is the D1
|
|
243
|
+
* twin of `@lunora/do`'s `runShardMigrations` (which self-creates shard-local
|
|
244
|
+
* tables) — it makes the schema the single source of truth for global tables
|
|
245
|
+
* too, so a fresh database serves them without a hand-applied migration. The
|
|
246
|
+
* column set and dialect match exactly what this module reads and writes
|
|
247
|
+
* (`columnRef`, `serializeColumnValue`, `decodeGlobalRow`).
|
|
248
|
+
*
|
|
249
|
+
* Idempotent (`CREATE TABLE/INDEX IF NOT EXISTS`); additive only — it never
|
|
250
|
+
* drops or retypes an existing column, so destructive schema changes still need
|
|
251
|
+
* an explicit migration.
|
|
252
|
+
*/
|
|
253
|
+
declare const runSqlGlobalTableMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
|
|
254
|
+
/**
|
|
255
|
+
* Materialize the `__agg_<index>` companion tables for every declared
|
|
256
|
+
* `aggregateIndex` on a global table. Global tables in Lunora ship their own
|
|
257
|
+
* DDL — counter tables are opt-in so production hosts can decide where they
|
|
258
|
+
* live. Tests and dev hosts can call this once after their schema migration to
|
|
259
|
+
* unlock O(1) counts.
|
|
260
|
+
*
|
|
261
|
+
* Idempotent (`CREATE TABLE IF NOT EXISTS`).
|
|
262
|
+
*/
|
|
263
|
+
declare const runSqlAggregateMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
|
|
264
|
+
/**
|
|
265
|
+
* Materialize the `__rank_<index>` companion tables for every declared
|
|
266
|
+
* `rankIndex` on a global table. Mirrors `runSqlAggregateMigrations` — same
|
|
267
|
+
* opt-in pattern so production hosts decide whether to spend the DDL.
|
|
268
|
+
*
|
|
269
|
+
* Idempotent (`CREATE TABLE IF NOT EXISTS` + `createIndexIfNotExists`).
|
|
270
|
+
*/
|
|
271
|
+
declare const runSqlRankMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
|
|
272
|
+
/** What {@link backfillSqlSearchIndexes} could not finish. */
|
|
273
|
+
interface SqlSearchBackfillResult {
|
|
274
|
+
/**
|
|
275
|
+
* Inverted companions that still lack their unique `(token, id)` key. Writes
|
|
276
|
+
* keep working without it, but two backfills racing over one row can index
|
|
277
|
+
* it twice; the warning logged for each says why the key was refused.
|
|
278
|
+
*/
|
|
279
|
+
uniqueKeyMissing: string[];
|
|
280
|
+
/** A previous build's rows left unmigrated because every attempt at them lost to a concurrent write. */
|
|
281
|
+
unmappedSkipped: number;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Provision the search companions, then index one bounded page of the rows that
|
|
285
|
+
* predate each index — unless it is declared `staged: true`, which leaves the
|
|
286
|
+
* whole backfill to {@link backfillSqlSearchIndexes}.
|
|
287
|
+
*
|
|
288
|
+
* Idempotent (`CREATE … IF NOT EXISTS` throughout, and the backfill resumes
|
|
289
|
+
* from recorded progress).
|
|
290
|
+
*/
|
|
291
|
+
declare const runSqlSearchMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
|
|
292
|
+
/**
|
|
293
|
+
* Run every declared search index — including the `staged: true` ones the
|
|
294
|
+
* migration pass skips — through to completion. The entry point a host calls
|
|
295
|
+
* out-of-band after deploying a search index over a table too large to index a
|
|
296
|
+
* page at a time.
|
|
297
|
+
*
|
|
298
|
+
* Idempotent and resumable: an index recorded as complete is skipped, and an
|
|
299
|
+
* interrupted run picks up from its cursor.
|
|
300
|
+
*/
|
|
301
|
+
declare const backfillSqlSearchIndexes: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<SqlSearchBackfillResult>;
|
|
302
|
+
/** Reserved table holding one backfill-progress row per search companion. */
|
|
303
|
+
declare const SEARCH_STATE_TABLE = "__lunora_search_state";
|
|
304
|
+
/** Create the progress table. Idempotent; runs alongside the companion DDL. */
|
|
305
|
+
declare const migrateSearchState: (exec: SqlCtxExec, dialect: SqlDialect) => Promise<void>;
|
|
147
306
|
/** Map a JS value onto its SQLite storage form — SQLite has no boolean, so true/false → 1/0. */
|
|
148
|
-
declare const sqliteEncode: (value: unknown) => unknown;
|
|
307
|
+
declare const sqliteEncode: (value: unknown, kind?: string) => unknown;
|
|
149
308
|
/** Parse `raw` as JSON, returning `raw` unchanged when it is not valid JSON. */
|
|
150
309
|
declare const tryJsonParse: (raw: string) => unknown;
|
|
151
|
-
/**
|
|
310
|
+
/**
|
|
311
|
+
* Decode a `bigint` column: the order-preserving key {@link bigintSqlKey} writes,
|
|
312
|
+
* or — for a row stored by a build that wrote plain decimal text — the decimal
|
|
313
|
+
* string, else verbatim.
|
|
314
|
+
*/
|
|
152
315
|
declare const decodeBigint: (raw: unknown) => unknown;
|
|
153
316
|
/**
|
|
154
|
-
* Resolve the *effective* storage kind of a column validator.
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
|
|
317
|
+
* Resolve the *effective* storage kind of a column validator: `v.optional(inner)`
|
|
318
|
+
* unwrapped to `inner`'s kind, since encoding keys off the runtime value's JS
|
|
319
|
+
* type and the validator's own `kind` of `"optional"` hides that.
|
|
320
|
+
*
|
|
321
|
+
* A thin alias over `shared/effective-kind`, which is where the rule lives so
|
|
322
|
+
* the DO row store applies the identical one — it reads the same validators and
|
|
323
|
+
* has the same failure mode, and two copies drifted the last time.
|
|
324
|
+
*/
|
|
161
325
|
declare const effectiveColumnKind: (validator: ValidatorLike) => string | undefined;
|
|
162
326
|
/**
|
|
163
|
-
* Inverse of {@link sqliteEncode}: map a SQLite storage value back onto its JS
|
|
164
|
-
* form, driven by the field's effective validator `kind`:
|
|
165
|
-
*
|
|
166
|
-
* - `boolean`: 1/0 → true/false (SQLite has no boolean type).
|
|
167
|
-
* - `bigint`: decimal string → `BigInt`.
|
|
168
|
-
* - `
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
|
|
327
|
+
* Inverse of {@link sqliteEncode}: map a SQLite storage value back onto its JS
|
|
328
|
+
* form, driven by the field's effective validator `kind`:
|
|
329
|
+
*
|
|
330
|
+
* - `boolean`: 1/0 → true/false (SQLite has no boolean type).
|
|
331
|
+
* - `bigint`: decimal string → `BigInt`.
|
|
332
|
+
* - `bytes`: normalizes any driver return shape to a genuine `ArrayBuffer` — a
|
|
333
|
+
* view (`Uint8Array`/`Buffer`/…) is sliced to its own byte window, a plain
|
|
334
|
+
* `ArrayBuffer` passes through, and an array of byte values is packed into a
|
|
335
|
+
* fresh buffer. Required because `v.bytes()` validates
|
|
336
|
+
* `value instanceof ArrayBuffer` and different backends return different BLOB
|
|
337
|
+
* shapes: the D1 binding `Array<number>`, workerd's raw SQLite `ArrayBuffer`,
|
|
338
|
+
* node:sqlite `Uint8Array`, pg/mysql2 `Buffer`. The D1 one is the shape only a
|
|
339
|
+
* real binding produces, so the array case has its test in the workerd suite.
|
|
340
|
+
* - `object`/`array`/`record`/`geoPoint`: JSON string → parsed value. `geoPoint`
|
|
341
|
+
* belongs here because {@link sqliteEncode} keys off the runtime JS type and
|
|
342
|
+
* stores the `{ lat, lng }` object as JSON in a TEXT column; without the case
|
|
343
|
+
* it fell through to `default:` and every client read back the raw JSON text.
|
|
344
|
+
* - `union`/`any`/`from`: parsed back when the stored string is a JSON
|
|
345
|
+
* non-scalar, or when it carries the {@link WIRE_PREFIX} marker.
|
|
346
|
+
* A `number` or `boolean` does NOT round-trip through the column's native
|
|
347
|
+
* type — all three kinds store as TEXT on every engine, which coerces a bound
|
|
348
|
+
* `42` to the text `42.0` — so {@link sqliteEncode} writes those two in the
|
|
349
|
+
* marked form and this branch reverses them by the marker alone. A number's
|
|
350
|
+
* marked payload is `float64SqlKey`, so its text order is numeric order; the
|
|
351
|
+
* marked JSON an earlier build wrote is still read back here, which is what
|
|
352
|
+
* makes a table correct between the format change and the rewrite pass.
|
|
353
|
+
* `from` belongs to THIS group, not to `object`/`array`/`record`: an external
|
|
354
|
+
* Standard Schema can describe a string just as easily as an object, and
|
|
355
|
+
* {@link sqliteEncode} keys off the runtime JS type — so a `v.from(z.string())`
|
|
356
|
+
* column holding `"123"` is stored verbatim, and unconditional parsing would
|
|
357
|
+
* read it back as the NUMBER 123.
|
|
358
|
+
* A `bigint` member decodes too, by SHAPE: {@link sqliteEncode} keys off the
|
|
359
|
+
* runtime type, so a bigint here is stored as the same order-preserving key a
|
|
360
|
+
* declared `v.bigint()` column gets, and returning that verbatim handed the
|
|
361
|
+
* caller 40 characters of padding instead of the value it wrote. See the
|
|
362
|
+
* ambiguity this buys, and why the wire marker cannot carry it instead, below.
|
|
363
|
+
* CAVEAT: a union/any/from member is stored verbatim by {@link sqliteEncode}, so
|
|
364
|
+
* a legitimate *string* value that itself looks like JSON (`'{"a":1}'`, `'[1,2]'`)
|
|
365
|
+
* is ambiguous on read and decodes back to the parsed object/array, not the
|
|
366
|
+
* original string. The bigint test admits the same class of ambiguity, narrower
|
|
367
|
+
* but real: `decodeBigintSqlKey` accepts EXACTLY 40 characters — `"0"` or `"1"`,
|
|
368
|
+
* then 39 digits — and a stored *string* of that shape (a zero-padded account
|
|
369
|
+
* number, a numeric external id) is byte-identical on disk to a key and reads
|
|
370
|
+
* back as a `bigint`. Pinned by a test in `ctx-db.test.ts` so the trade is
|
|
371
|
+
* visible rather than rediscovered.
|
|
372
|
+
*
|
|
373
|
+
* Preferred anyway, because the alternative is not "no ambiguity" but
|
|
374
|
+
* guaranteed corruption: WITHOUT the test, every bigint any code writes to an
|
|
375
|
+
* untyped column comes back as padding, on every read. The narrow false
|
|
376
|
+
* positive costs a specific 40-character digit string its type; the absent test
|
|
377
|
+
* cost every such column its value.
|
|
378
|
+
*
|
|
379
|
+
* The unambiguous {@link WIRE_PREFIX} marker cannot carry this instead. It is
|
|
380
|
+
* not reached: {@link sqliteEncode} returns at its `bigint` branch first, and it
|
|
381
|
+
* takes no `kind`, so it cannot encode an untyped column differently from a
|
|
382
|
+
* declared one. It is also `serializeColumnValue`, which builds every WHERE
|
|
383
|
+
* binding — a prefixed bigint would never match a row stored as a key, and the
|
|
384
|
+
* padded key is order-preserving on purpose (indexes, range predicates, MIN/MAX
|
|
385
|
+
* all read it). Two storage forms for one runtime type breaks comparison
|
|
386
|
+
* between them. Disambiguating properly means tagging every encoded non-scalar,
|
|
387
|
+
* a breaking storage-format change, so it is documented rather than fixed.
|
|
388
|
+
* - everything else (string/number/date/timestamp/id/literal): verbatim.
|
|
389
|
+
*/
|
|
173
390
|
declare const sqliteDecode: (raw: unknown, kind: string | undefined) => unknown;
|
|
174
|
-
export {
|
|
391
|
+
export { SEARCH_STATE_TABLE,
|
|
392
|
+
/**
|
|
393
|
+
* `@lunora/sql-store` — the internal, dialect-parameterized SQL store core
|
|
394
|
+
* shared by Lunora's `.global()` table backends.
|
|
395
|
+
*
|
|
396
|
+
* One ORM implementation (`createSqlCtxDb`) drives any SQL engine through a
|
|
397
|
+
* `SqlDialect`: SQLite via `@lunora/d1`, and Postgres/MySQL (PlanetScale, Neon,
|
|
398
|
+
* any Hyperdrive-reachable database) via `@lunora/hyperdrive`. Reactivity is
|
|
399
|
+
* unaffected — the writer is injected as `globalDb` into `createShardCtxDb`,
|
|
400
|
+
* whose `broadcast` hook drives live queries regardless of engine.
|
|
401
|
+
*
|
|
402
|
+
* This package is internal: consumers depend on `@lunora/d1` or
|
|
403
|
+
* `@lunora/hyperdrive`, which assemble the concrete dialect and wrap the core.
|
|
404
|
+
*/
|
|
405
|
+
type SqlCtxDbOptions,
|
|
406
|
+
/**
|
|
407
|
+
* `@lunora/sql-store` — the internal, dialect-parameterized SQL store core
|
|
408
|
+
* shared by Lunora's `.global()` table backends.
|
|
409
|
+
*
|
|
410
|
+
* One ORM implementation (`createSqlCtxDb`) drives any SQL engine through a
|
|
411
|
+
* `SqlDialect`: SQLite via `@lunora/d1`, and Postgres/MySQL (PlanetScale, Neon,
|
|
412
|
+
* any Hyperdrive-reachable database) via `@lunora/hyperdrive`. Reactivity is
|
|
413
|
+
* unaffected — the writer is injected as `globalDb` into `createShardCtxDb`,
|
|
414
|
+
* whose `broadcast` hook drives live queries regardless of engine.
|
|
415
|
+
*
|
|
416
|
+
* This package is internal: consumers depend on `@lunora/d1` or
|
|
417
|
+
* `@lunora/hyperdrive`, which assemble the concrete dialect and wrap the core.
|
|
418
|
+
*/
|
|
419
|
+
type SqlCtxExec, type SqlDialect, type SqlRunResult, type SqlSearchBackfillResult, backfillSqlSearchIndexes, createSqlCtxDb, decodeBigint, decodeGlobalRow, effectiveColumnKind, migrateSearchState, readSqlCdcChangedTables, readSqlCdcChanges, readSqlCdcFloor, runSqlAggregateMigrations, runSqlCdcMigration, runSqlGlobalTableMigrations, runSqlRankMigrations, runSqlSearchMigrations, sqliteDecode, sqliteEncode, sweepSqlCdcRetention, tryJsonParse };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { decodeBigint, effectiveColumnKind, sqliteDecode, sqliteEncode, tryJsonParse } from './packem_shared/decodeBigint-Dedu92k4.mjs';
|
|
1
|
+
import{createSqlCtxDb as a,readSqlCdcChangedTables as o,readSqlCdcChanges as t,readSqlCdcFloor as l,runSqlCdcMigration as s,sweepSqlCdcRetention as n}from"./packem_shared/createSqlCtxDb-LjE6a4t0.mjs";import{runSqlAggregateMigrations as d,runSqlGlobalTableMigrations as S,runSqlRankMigrations as c}from"./packem_shared/runSqlAggregateMigrations-ByqTfw-y.mjs";import{d as q,r as C}from"./packem_shared/ctx-db-search-CpGgDAhl.mjs";import{S as m,m as x}from"./packem_shared/ctx-db-search-state-TMs6Zlae.mjs";import{g as b,a as u,c as h,s as M,t as T}from"./packem_shared/value-codec-CBr0_L1N.mjs";import{decodeGlobalRow as E}from"./packem_shared/decodeGlobalRow-P2Z1dMjU.mjs";export{m as SEARCH_STATE_TABLE,q as backfillSqlSearchIndexes,a as createSqlCtxDb,b as decodeBigint,E as decodeGlobalRow,u as effectiveColumnKind,x as migrateSearchState,o as readSqlCdcChangedTables,t as readSqlCdcChanges,l as readSqlCdcFloor,d as runSqlAggregateMigrations,s as runSqlCdcMigration,S as runSqlGlobalTableMigrations,c as runSqlRankMigrations,C as runSqlSearchMigrations,h as sqliteDecode,M as sqliteEncode,n as sweepSqlCdcRetention,T as tryJsonParse};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{S as t,a as S,m as c,r as l,b as i,w as s}from"./ctx-db-search-state-TMs6Zlae.mjs";import"./decodeGlobalRow-P2Z1dMjU.mjs";export{t as SEARCH_STATE_TABLE,S as clearSearchBackfillState,c as migrateSearchState,l as readSearchBackfillState,i as readSearchIndexCoverage,s as writeSearchBackfillState};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./ctx-db-search-state-TMs6Zlae.mjs";import{d as S,c as s,a as i,r as l}from"./ctx-db-search-CpGgDAhl.mjs";import"./decodeGlobalRow-P2Z1dMjU.mjs";export{S as backfillSqlSearchIndexes,s as createSearchSync,i as runSqlSearch,l as runSqlSearchMigrations};
|