@lunora/d1 1.0.0-alpha.18 → 1.0.0-alpha.181

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/dist/index.d.mts CHANGED
@@ -1,6 +1,9 @@
1
- import { SchemaLike, DatabaseWriterLike } from '@lunora/do';
2
- import { SqlCtxDbOptions, createSqlCtxDb, SqlCtxExec, readSqlCdcChanges, SqlDialect } from '@lunora/sql-store';
1
+ import { SchemaLike, DatabaseWriterLike } from '@lunora/shard-engine';
2
+ export { applyCdcChanges } from '@lunora/shard-engine';
3
+ import { SqlCtxDbOptions, SqlCtxExec, SqlSearchBackfillResult, createSqlCtxDb, readSqlCdcChanges, SqlDialect } from '@lunora/sql-store';
3
4
  export { type SqlCtxExec as D1Exec, type SqlCtxDbOptions, type SqlCtxExec, createSqlCtxDb } from '@lunora/sql-store';
5
+ import { D1DatabaseLike, D1SessionLike, D1PreparedStatementLike } from '@lunora/platform';
6
+ export type { D1DatabaseLike, D1PreparedStatementLike, D1SessionLike } from '@lunora/platform';
4
7
  import { BatchItem, BatchResponse } from 'drizzle-orm/batch';
5
8
  import { DrizzleD1Database } from 'drizzle-orm/d1';
6
9
  /** The D1 store options — the shared store options minus `dialect` (the SQLite dialect is injected for you). */
@@ -9,12 +12,14 @@ type D1ContextDatabaseOptions = Omit<SqlCtxDbOptions, "dialect">;
9
12
  declare const createD1ContextDatabase: (options: D1ContextDatabaseOptions) => ReturnType<typeof createSqlCtxDb>;
10
13
  /** Auto-provision the schema's `.global()` tables in D1 (idempotent `CREATE TABLE IF NOT EXISTS`). */
11
14
  declare const runD1GlobalTableMigrations: (exec: SqlCtxExec, schema: SchemaLike) => Promise<void>;
12
- /** Materialize the `__agg_&lt;index>` companion tables for the schema's aggregate indexes. */
15
+ /** Materialize the `__agg_<index>` companion tables for the schema's aggregate indexes. */
13
16
  declare const runD1AggregateMigrations: (exec: SqlCtxExec, schema: SchemaLike) => Promise<void>;
14
- /** Materialize the `__rank_&lt;index>` companion tables for the schema's rank indexes. */
17
+ /** Materialize the `__rank_<index>` companion tables for the schema's rank indexes. */
15
18
  declare const runD1RankMigrations: (exec: SqlCtxExec, schema: SchemaLike) => Promise<void>;
16
- /** Materialize the `__fts_&lt;index>` fts5 shadow tables for the schema's search indexes (no-op without fts5). */
19
+ /** Materialize (and backfill) the `__fts_<index>` fts5 shadow tables for the schema's search indexes. */
17
20
  declare const runD1SearchMigrations: (exec: SqlCtxExec, schema: SchemaLike) => Promise<void>;
21
+ /** Index existing rows into every search companion, including the `staged: true` ones migrations leave empty. */
22
+ declare const backfillD1SearchIndexes: (exec: SqlCtxExec, schema: SchemaLike) => Promise<SqlSearchBackfillResult>;
18
23
  /** Create the `__cdc_log` table in D1 (idempotent; only run when CDC is enabled). */
19
24
  declare const runD1CdcMigration: (exec: SqlCtxExec) => Promise<void>;
20
25
  /** Read changelog entries newer than `sinceSeq` in commit order. */
@@ -22,11 +27,30 @@ declare const readD1CdcChanges: (exec: SqlCtxExec, options?: {
22
27
  limit?: number;
23
28
  sinceSeq?: number;
24
29
  }) => ReturnType<typeof readSqlCdcChanges>;
25
- /** Drop changelog entries at or below a checkpointed `throughSeq`. */
26
- declare const trimD1CdcChanges: (exec: SqlCtxExec, throughSeq: number) => Promise<void>;
30
+ /**
31
+ * Delete `.global()` changelog entries older than `retentionMs`, if this writer
32
+ * wins the fleet's sweep lease. See `sweepSqlCdcRetention`.
33
+ *
34
+ * Replaces the seq-based `trimD1CdcChanges`, which took a checkpoint no caller
35
+ * could compute: the log is shared across every shard and region, so no single
36
+ * caller knows a `seq` that is safe for all of them. A time window is a bound
37
+ * they can all reason about, and the lease is what stops them racing.
38
+ */
39
+ declare const sweepD1CdcRetention: (exec: SqlCtxExec, retentionMs: number, now: number) => Promise<void>;
27
40
  /** One exported row: `doc` is reconstructed from the column tuple. */
28
41
  interface ExportRow {
29
42
  doc: Record<string, unknown>;
43
+ /**
44
+ * True physical source line, when the caller has it (e.g. a global row
45
+ * pulled out of an interspersed NDJSON import stream by
46
+ * `@lunora/runtime`'s `import-stream.ts`). When present this overrides the
47
+ * position-derived line below — the caller-side positions of a
48
+ * global-only subset don't line up with the original file once
49
+ * non-global rows have been filtered out. Absent for callers with no
50
+ * physical source to attribute (e.g. a fresh `exportGlobalRows` roundtrip
51
+ * in tests), which keeps the positional fallback.
52
+ */
53
+ line?: number;
30
54
  table: string;
31
55
  }
32
56
  interface ImportError {
@@ -42,32 +66,42 @@ interface ImportResult {
42
66
  inserted: Record<string, number>;
43
67
  }
44
68
  /**
45
- * Return every `.global()` table in the schema, optionally narrowed by an
46
- * allowlist. Shard-local tables are skipped here — they're handled by the DO
47
- * helpers — so callers get a clean separation between the two storage planes.
48
- */
69
+ * Return every `.global()` table in the schema, optionally narrowed by an
70
+ * allowlist. Shard-local tables are skipped here — they're handled by the DO
71
+ * helpers — so callers get a clean separation between the two storage planes.
72
+ */
49
73
  declare const selectGlobalTables: (schema: SchemaLike, requested?: ReadonlyArray<string>) => string[];
50
74
  interface ExportGlobalArgs {
51
75
  batchSize?: number;
52
76
  tables?: ReadonlyArray<string>;
53
77
  }
54
78
  /**
55
- * Yield rows from every requested `.global()` table in batches. Uses
56
- * `LIMIT ?/OFFSET ?` because D1 globals don't have a stable keyset abstraction
57
- * here (the writer's `findMany` does, but at the cost of routing through the
58
- * full validator pipeline; for a snapshot stream a plain offset scan is
59
- * sufficient and predictable).
60
- */
79
+ * Yield rows from every requested `.global()` table in batches.
80
+ *
81
+ * Keyset-paginates on the physical primary key (`id` — every `.global()` table
82
+ * carries it, see `frameworkColumnDdl`): `WHERE "id" > ? ORDER BY "id" LIMIT ?`,
83
+ * carrying the last id forward. A plain `LIMIT/OFFSET` scan would be wrong here —
84
+ * SQLite gives no ordering guarantee for an unordered SELECT and each page is a
85
+ * separate query, so pages could overlap or skip rows (and any concurrent
86
+ * insert/delete would shift offsets and silently drop/duplicate rows in the
87
+ * snapshot). Keyset paging is deterministic under concurrent writes and avoids
88
+ * O(n^2) OFFSET scans on large tables.
89
+ *
90
+ * Tables are provisioned first (idempotent `CREATE … IF NOT EXISTS`): `.global()`
91
+ * tables are created lazily on first write, so a fresh deployment — or any table
92
+ * never written — would otherwise abort the stream with a raw `no such table`
93
+ * instead of exporting it as empty.
94
+ */
61
95
  declare const exportGlobalRows: (exec: SqlCtxExec, schema: SchemaLike, args: ExportGlobalArgs) => AsyncGenerator<ExportRow, void, undefined>;
62
96
  interface ImportGlobalArgs {
63
97
  /**
64
- * Optional direct exec handle to the same D1 database the writer targets.
65
- * When supplied, the conflict pre-probe issues a single
66
- * `SELECT 1 FROM &lt;table> WHERE id = ? LIMIT 1` against the row's declared
67
- * table instead of falling back to `writer.get(id)`, which scans every
68
- * global table looking for the id. Strongly recommended for large schemas
69
- * — the writer-fallback is O(N tables) per row.
70
- */
98
+ * Optional direct exec handle to the same D1 database the writer targets.
99
+ * When supplied, the conflict pre-probe issues a single
100
+ * `SELECT 1 FROM <table> WHERE id = ? LIMIT 1` against the row's declared
101
+ * table instead of falling back to `writer.get(id)`, which scans every
102
+ * global table looking for the id. Strongly recommended for large schemas
103
+ * — the writer-fallback is O(N tables) per row.
104
+ */
71
105
  exec?: D1ExecLike;
72
106
  rows: ReadonlyArray<ExportRow>;
73
107
  startLine?: number;
@@ -77,121 +111,129 @@ interface D1ExecLike {
77
111
  all: (sql: string, parameters: ReadonlyArray<unknown>) => Promise<Record<string, unknown>[]>;
78
112
  }
79
113
  /**
80
- * Import rows into `.global()` tables via the schema-aware D1 writer. The
81
- * writer rejects unknown ids on `insert` (the writer assigns one when `_id` is
82
- * absent); we pre-probe each row's `_id` so a collision is reported as a
83
- * conflict instead of bubbled as a UNIQUE error. Schema-failed rows surface in
84
- * `errors`; the rest land.
85
- */
114
+ * Import rows into `.global()` tables via the schema-aware D1 writer. The
115
+ * writer rejects unknown ids on `insert` (the writer assigns one when `_id` is
116
+ * absent); we pre-probe each row's `_id` so a collision is reported as a
117
+ * conflict instead of bubbled as a UNIQUE error. Schema-failed rows surface in
118
+ * `errors`; the rest land.
119
+ */
86
120
  declare const importGlobalRows: (writer: DatabaseWriterLike, schema: SchemaLike, args: ImportGlobalArgs) => Promise<ImportResult>;
87
121
  /**
88
- * Minimal structural projection of `D1Database` to keep the adapter
89
- * compatible with the real workers-types value as well as unit-test doubles.
90
- */
91
- interface D1DatabaseLike {
92
- batch?: (statements: D1PreparedStatementLike[]) => Promise<unknown[]>;
93
- exec?: (sql: string) => Promise<unknown>;
94
- prepare: (sql: string) => D1PreparedStatementLike;
95
- withSession: (bookmark?: string) => D1SessionLike;
96
- }
97
- interface D1SessionLike {
98
- batch?: (statements: D1PreparedStatementLike[]) => Promise<unknown[]>;
99
- getBookmark: () => string | null;
100
- prepare: (sql: string) => D1PreparedStatementLike;
101
- }
102
- interface D1PreparedStatementLike {
103
- all: <T = unknown>() => Promise<{
104
- results: T[];
105
- success: boolean;
106
- }>;
107
- bind: (...values: unknown[]) => D1PreparedStatementLike;
108
- first: <T = unknown>(column?: string) => Promise<T | null>;
109
- raw: <T = unknown>() => Promise<T[][]>;
110
- run: <T = unknown>() => Promise<{
111
- meta?: Record<string, unknown>;
112
- results?: T[];
113
- success: boolean;
114
- }>;
115
- }
116
- /** Thin wrapper over a `D1DatabaseSession` exposing bookmark plumbing. */
122
+ * Thin wrapper over a `D1DatabaseSession` exposing bookmark plumbing.
123
+ *
124
+ * `all` and `first` retry D1's expected transient failures — but only for a
125
+ * statement that is provably read-only. D1 produces a documented
126
+ * baseline of infrastructural errors even when healthy (Cloudflare's team calls
127
+ * a handful every few hours "not unexpected") and their guidance is to retry;
128
+ * a client that does not has adopted that error rate as the application's own.
129
+ *
130
+ * Nothing else on this class or on {@link D1Client} retries. `run` is the write
131
+ * path, and every transient D1 error is ambiguous about whether the statement
132
+ * applied — re-running a non-idempotent write after a lost response
133
+ * double-applies it. `all` is not a read path either: D1 runs
134
+ * `UPDATE … RETURNING` through it, so the retry is gated on the statement's
135
+ * leading keyword rather than on the method name. Wrap a genuinely idempotent
136
+ * write in {@link withD1Retry} yourself, per call.
137
+ */
117
138
  declare class D1Session {
118
139
  private readonly session;
119
140
  /** See {@link D1Client.stmtCache}. Scoped per session. */
120
141
  private readonly stmtCache;
121
142
  constructor(session: D1SessionLike);
122
143
  prepare(sql: string): D1PreparedStatementLike;
144
+ /**
145
+ * Execute a statement. **Not retried** — `run` is the write path, and a
146
+ * transient D1 error does not say whether the write applied.
147
+ */
123
148
  run<T = unknown>(sql: string, ...binds: unknown[]): Promise<{
124
149
  meta?: Record<string, unknown>;
125
150
  results?: T[];
126
151
  success: boolean;
127
152
  }>;
153
+ /**
154
+ * Read all rows, retrying D1's transient failures when `sql` is a
155
+ * read-only statement. An `UPDATE … RETURNING` runs through here too and is
156
+ * left unretried.
157
+ */
128
158
  all<T = unknown>(sql: string, ...binds: unknown[]): Promise<{
159
+ meta?: Record<string, unknown>;
129
160
  results: T[];
130
161
  success: boolean;
131
162
  }>;
163
+ /** Read the first row, retrying under the same read-only rule as {@link all}. */
132
164
  first<T = unknown>(sql: string, ...binds: unknown[]): Promise<T | null>;
133
165
  /**
134
- * Returns the most recent bookmark known to the session, or `undefined`
135
- * when D1 has not issued one yet.
136
- */
166
+ * Returns the most recent bookmark known to the session, or `undefined`
167
+ * when D1 has not issued one yet.
168
+ */
137
169
  getBookmark(): string | undefined;
138
170
  }
171
+ /**
172
+ * A D1 handle: prepared-statement caching, Sessions-API sessions, and the
173
+ * drizzle-typed accessors.
174
+ *
175
+ * **Nothing on this class retries.** `prepare`, `drizzle`, `drizzleSession`,
176
+ * `batch` and `raw` all hand back the underlying D1 surface untouched. The
177
+ * automatic retry lives on {@link D1Session.all} / {@link D1Session.first} and
178
+ * on `retryingExec` (the seam `.global()` tables read through); wrap anything
179
+ * else in {@link withD1Retry} yourself, and only when it is safe to run twice.
180
+ */
139
181
  declare class D1Client {
140
182
  private readonly db;
141
183
  /**
142
- * SQL string -> prepared statement. Prepared statements are reusable in
143
- * D1; preparing the same SQL twice forces the worker to round-trip the
144
- * statement plan. Caching is per-instance so unit-test isolation holds.
145
- * Bounded to {@link STMT_CACHE_CAPACITY} via LRU eviction.
146
- */
184
+ * SQL string -> prepared statement. Prepared statements are reusable in
185
+ * D1; preparing the same SQL twice forces the worker to round-trip the
186
+ * statement plan. Caching is per-instance so unit-test isolation holds.
187
+ * Bounded to {@link STMT_CACHE_CAPACITY} via LRU eviction.
188
+ */
147
189
  private readonly stmtCache;
148
190
  /**
149
- * Lazily-built drizzle handle over the bare binding. Memoised so a single
150
- * `D1Client` reuses the same dialect/session machinery across calls.
151
- */
191
+ * Lazily-built drizzle handle over the bare binding. Memoised so a single
192
+ * `D1Client` reuses the same dialect/session machinery across calls.
193
+ */
152
194
  private drizzleHandle;
153
195
  constructor(database: D1DatabaseLike);
154
196
  /**
155
- * Open a Sessions-API scoped session. Pass the bookmark forwarded by
156
- * the client to opt into read-your-writes consistency.
157
- *
158
- * With no bookmark this is the first request of a session — there is no
159
- * prior write to read, so we open with the explicit `"first-unconstrained"`
160
- * constraint (Cloudflare's lowest-latency default: the first read may serve
161
- * from any replica). Read-your-writes for sequenced requests still flows
162
- * through the forwarded bookmark; a caller needing a strongly-consistent
163
- * very-first read should pass `"first-primary"` as the bookmark instead.
164
- */
197
+ * Open a Sessions-API scoped session. Pass the bookmark forwarded by
198
+ * the client to opt into read-your-writes consistency.
199
+ *
200
+ * With no bookmark this is the first request of a session — there is no
201
+ * prior write to read, so we open with the explicit `"first-unconstrained"`
202
+ * constraint (Cloudflare's lowest-latency default: the first read may serve
203
+ * from any replica). Read-your-writes for sequenced requests still flows
204
+ * through the forwarded bookmark; a caller needing a strongly-consistent
205
+ * very-first read should pass `"first-primary"` as the bookmark instead.
206
+ */
165
207
  withSession(bookmark?: string): D1Session;
166
208
  /**
167
- * Prepare a statement, reusing a cached one when the SQL text matches.
168
- * `bind()` on a prepared statement returns a new bound statement and
169
- * leaves the underlying prepared plan reusable, so cache hits are safe
170
- * even when the previous caller already called `.bind(...).run()`.
171
- */
209
+ * Prepare a statement, reusing a cached one when the SQL text matches.
210
+ * `bind()` on a prepared statement returns a new bound statement and
211
+ * leaves the underlying prepared plan reusable, so cache hits are safe
212
+ * even when the previous caller already called `.bind(...).run()`.
213
+ */
172
214
  prepare(sql: string): D1PreparedStatementLike;
173
215
  /**
174
- * Drizzle handle over the bare `env.DB` binding. Used for typed queries
175
- * against generated `sqliteTable` schemas; does **not** participate in the
176
- * D1 Sessions API (no bookmark pinning). For bookmark-scoped reads, use
177
- * {@link drizzleSession} instead.
178
- */
216
+ * Drizzle handle over the bare `env.DB` binding. Used for typed queries
217
+ * against generated `sqliteTable` schemas; does **not** participate in the
218
+ * D1 Sessions API (no bookmark pinning). For bookmark-scoped reads, use
219
+ * {@link drizzleSession} instead.
220
+ */
179
221
  get drizzle(): DrizzleD1Database<Record<string, unknown>>;
180
222
  /**
181
- * Drizzle handle scoped to a D1 Sessions-API session. The bookmark, when
182
- * supplied, opts into read-your-writes consistency for follow-up reads on
183
- * the same session.
184
- *
185
- * A `D1DatabaseSession` exposes the same `prepare` / `batch` surface
186
- * drizzle calls into, so a single `unknown` cast lets us treat the session
187
- * as a `D1Database` for driver-construction purposes.
188
- */
223
+ * Drizzle handle scoped to a D1 Sessions-API session. The bookmark, when
224
+ * supplied, opts into read-your-writes consistency for follow-up reads on
225
+ * the same session.
226
+ *
227
+ * A `D1DatabaseSession` exposes the same `prepare` / `batch` surface
228
+ * drizzle calls into, so a single `unknown` cast lets us hand the session to
229
+ * the driver.
230
+ */
189
231
  drizzleSession(bookmark?: string): DrizzleD1Database<Record<string, unknown>>;
190
232
  /**
191
- * Atomic batch over the drizzle d1 driver. Mirrors `db.batch([...])`
192
- * exactly; exposed on the client so callers don't need to hold a drizzle
193
- * handle just to run a typed batch.
194
- */
233
+ * Atomic batch over the drizzle d1 driver. Mirrors `db.batch([...])`
234
+ * exactly; exposed on the client so callers don't need to hold a drizzle
235
+ * handle just to run a typed batch.
236
+ */
195
237
  batch<U extends BatchItem<"sqlite">, T extends Readonly<[U, ...U[]]>>(items: T): Promise<BatchResponse<T>>;
196
238
  /** Direct access to the underlying binding (advanced use only). */
197
239
  get raw(): D1DatabaseLike;
@@ -205,24 +247,26 @@ interface GlobalTableInfo {
205
247
  interface GlobalTablePage {
206
248
  columns: string[];
207
249
  /**
208
- * Foreign-key columns (local column → referenced table) for tables that carry
209
- * real SQL `REFERENCES` constraints — recovered from `PRAGMA foreign_key_list`.
210
- * Schema `.global()` tables omit this (their refs come from `describeTables`);
211
- * external tables (e.g. better-auth's `session`/`twoFactor`) expose it so the
212
- * schema diagram can draw their global→global FK edges.
213
- */
250
+ * Foreign-key columns (local column → referenced table) for tables that carry
251
+ * real SQL `REFERENCES` constraints — recovered from `PRAGMA foreign_key_list`.
252
+ * Schema `.global()` tables omit this (their refs come from `describeTables`);
253
+ * external tables (e.g. better-auth's `session`/`twoFactor`) expose it so the
254
+ * schema diagram can draw their global→global FK edges.
255
+ */
214
256
  refs?: Record<string, string>;
215
257
  rows: Record<string, unknown>[];
216
258
  total: number;
217
259
  }
218
260
  /**
219
- * One equality constraint a facet-value click adds to the global browser's view:
220
- * `column = value` (or `column IS NULL` when `value` is nullish). `column` is a
221
- * displayed column name, validated against the table's columns and mapped to its
222
- * physical column (`_id` → `id`) before it is quoted; `value` is the **raw stored
223
- * value** the facet returned (a SQLite scalar), bound as a parameter and never
224
- * interpolated. AND-combined with the other clauses.
225
- */
261
+ * One equality constraint a facet-value click adds to the global browser's view:
262
+ * `column = value` (or `column IS NULL` when `value` is nullish). `column` is a
263
+ * displayed column name, validated against the table's columns and mapped to its
264
+ * physical column (`_id` → `id`) before it is quoted; `value` is the **raw stored
265
+ * value** the facet returned (a SQLite scalar — including a BLOB, which reaches
266
+ * here as bytes because the facet payload is wire-encoded), normalised by
267
+ * {@link sqliteEncode} and bound as a parameter, never interpolated.
268
+ * AND-combined with the other clauses.
269
+ */
226
270
  interface GlobalFilterClause {
227
271
  column: string;
228
272
  value: unknown;
@@ -234,13 +278,13 @@ interface ReadGlobalTablePageOptions {
234
278
  table: string;
235
279
  }
236
280
  /**
237
- * Options for {@link facetGlobalColumn} — the read-only "what values does this
238
- * column hold?" summary for the global (D1) browser. `column` is the displayed
239
- * column to group by (validated and mapped to its physical column, never
240
- * interpolated); `filters` mirrors {@link ReadGlobalTablePageOptions}'s eq
241
- * constraints so the facet reflects the **active view** (the same rows the
242
- * browser is previewing); `limit` caps the distinct values returned (clamped).
243
- */
281
+ * Options for {@link facetGlobalColumn} — the read-only "what values does this
282
+ * column hold?" summary for the global (D1) browser. `column` is the displayed
283
+ * column to group by (validated and mapped to its physical column, never
284
+ * interpolated); `filters` mirrors {@link ReadGlobalTablePageOptions}'s eq
285
+ * constraints so the facet reflects the **active view** (the same rows the
286
+ * browser is previewing); `limit` caps the distinct values returned (clamped).
287
+ */
244
288
  interface FacetGlobalColumnOptions {
245
289
  column: string;
246
290
  filters?: GlobalFilterClause[];
@@ -253,43 +297,49 @@ interface GlobalFacetValue {
253
297
  value: unknown;
254
298
  }
255
299
  /**
256
- * Payload of a {@link facetGlobalColumn} call: the top-N distinct `values` (each
257
- * with a `count`) ordered by frequency, plus `truncated` — `true` when more
258
- * distinct values existed beyond the cap, so the UI can say so rather than imply
259
- * the list is exhaustive. Mirrors the shard browser's `FacetColumnResult`.
260
- */
300
+ * Payload of a {@link facetGlobalColumn} call: the top-N distinct `values` (each
301
+ * with a `count`) ordered by frequency, plus `truncated` — `true` when more
302
+ * distinct values existed beyond the cap, so the UI can say so rather than imply
303
+ * the list is exhaustive. Mirrors the shard browser's `FacetColumnResult`.
304
+ */
261
305
  interface GlobalFacetResult {
262
306
  truncated: boolean;
263
307
  values: GlobalFacetValue[];
264
308
  }
265
309
  /**
266
- * List every browsable D1 table with its row count, ordered by name. Surfaces
267
- * both the schema's `.global()` tables (provisioned first) and external tables
268
- * (auth, etc.); internal/companion tables are excluded.
269
- */
310
+ * List every browsable D1 table with its row count, ordered by name. Surfaces
311
+ * both the schema's `.global()` tables (provisioned first) and external tables
312
+ * (auth, etc.); internal/companion tables are excluded.
313
+ */
270
314
  declare const listGlobalTables: (exec: SqlCtxExec, schema: SchemaLike) => Promise<GlobalTableInfo[]>;
271
315
  /**
272
- * Read a page of rows from one D1 table. The table is validated against the live
273
- * browsable-table list before its name is interpolated, so this can't be coerced
274
- * into reading an internal table or injecting SQL. `limit` is clamped to
275
- * `[1, 500]`; `offset` floors at `0`. `filters` AND-narrows the page to rows
276
- * matching each `column = value` eq constraint (a facet-value drill-down), bound
277
- * through {@link buildEqPredicate} so they never inject SQL.
278
- */
316
+ * Read a page of rows from one D1 table. The table is validated against the live
317
+ * browsable-table list before its name is interpolated, so this can't be coerced
318
+ * into reading an internal table or injecting SQL. `limit` is clamped to
319
+ * `[1, 500]`; `offset` floors at `0`. `filters` AND-narrows the page to rows
320
+ * matching each `column = value` eq constraint (a facet-value drill-down), bound
321
+ * through {@link buildEqPredicate} so they never inject SQL.
322
+ */
279
323
  declare const readGlobalTablePage: (exec: SqlCtxExec, schema: SchemaLike, options: ReadGlobalTablePageOptions) => Promise<GlobalTablePage>;
280
324
  /**
281
- * Summarise the distinct values of one displayed column over the **active view**
282
- * (the same eq `filters` the global browser is previewing) — the D1 twin of the
283
- * shard browser's `facetColumn`. Read-only: a `SELECT col AS value, COUNT(*) AS
284
- * count … GROUP BY col ORDER BY count DESC LIMIT N+1`, with the column validated
285
- * against the table's displayed columns (typed 404 if unknown), mapped to its
286
- * physical column, and quoted — never interpolated from caller input. The extra
287
- * over-fetched row is dropped and surfaced as `truncated`. A sensitive column on
288
- * an external (non-schema) table is never grouped — it collapses to a single
289
- * redacted `•••` bucket — mirroring the page browser's value redaction so the
290
- * facet can't leak credentials. The returned `value` is the raw stored scalar, so
291
- * a click feeds it straight back as an eq filter.
292
- */
325
+ * Summarise the distinct values of one displayed column over the **active view**
326
+ * (the same eq `filters` the global browser is previewing) — the D1 twin of the
327
+ * shard browser's `facetColumn`. Read-only: a `SELECT col AS value, COUNT(*) AS
328
+ * count … GROUP BY col ORDER BY count DESC LIMIT N+1`, with the column validated
329
+ * against the table's displayed columns (typed 404 if unknown), mapped to its
330
+ * physical column, and quoted — never interpolated from caller input. The extra
331
+ * over-fetched row is dropped and surfaced as `truncated`. A sensitive column on
332
+ * an external (non-schema) table is never grouped — it collapses to a single
333
+ * redacted `•••` bucket — mirroring the page browser's value redaction so the
334
+ * facet can't leak credentials. The returned `value` is the raw stored scalar, so
335
+ * a click feeds it straight back as an eq filter.
336
+ *
337
+ * The payload is wire-encoded on the way out, like the page read's rows: a BLOB
338
+ * column's stored value is bytes, and `Response.json` flattens those to `{}` —
339
+ * which is not a display glitch but a broken drill-down, since the flattened
340
+ * value is what the click sends back as the filter. `needsWireEncoding` leaves a
341
+ * facet over an ordinary text/numeric column untouched.
342
+ */
293
343
  declare const facetGlobalColumn: (exec: SqlCtxExec, schema: SchemaLike, options: FacetGlobalColumnOptions) => Promise<GlobalFacetResult>;
294
344
  interface Migration {
295
345
  /** Human-readable name, e.g. `001_init` (used in logs). */
@@ -310,30 +360,171 @@ interface MigrationRunnerResult {
310
360
  }[];
311
361
  }
312
362
  /**
313
- * Sequentially applies pending migrations against a D1 database via the
314
- * drizzle-orm/d1 driver. Each migration is hashed (SHA-256 over its SQL
315
- * text); the hash is stored in `__drizzle_migrations`, so re-applying the
316
- * same SQL under a different `version` is rejected and identical migrations
317
- * are skipped idempotently.
318
- */
363
+ * Sequentially applies pending migrations against a D1 database via the
364
+ * drizzle-orm/d1 driver, recording each one's `version` and SHA-256 in
365
+ * `__drizzle_migrations`.
366
+ *
367
+ * A migration is skipped when its VERSION is recorded, never by its text alone.
368
+ * Deduplicating by hash re-applied any migration whose text changed after it
369
+ * ran — a comment, a whitespace fix, a CRLF checkout — so a non-idempotent one
370
+ * (`SET balance = balance * 100`) ran twice. A recorded version whose hash no
371
+ * longer matches is refused as `MIGRATION_DRIFT` instead: the runner cannot
372
+ * tell a harmless edit from a changed migration, and running either again is
373
+ * the one outcome that is never right.
374
+ */
319
375
  declare class MigrationRunner {
320
376
  private readonly client;
321
377
  private readonly migrations;
322
378
  /**
323
- * Accepts either a {@link D1Client} (preferred — gets typed batches +
324
- * drizzle handle for free) or a raw `D1DatabaseLike` binding (wrapped on
325
- * the caller's behalf so existing `@lunora/cli` callers keep working).
326
- */
379
+ * Accepts either a {@link D1Client} (preferred — gets typed batches +
380
+ * drizzle handle for free) or a raw `D1DatabaseLike` binding (wrapped on
381
+ * the caller's behalf so existing `@lunora/cli` callers keep working).
382
+ */
327
383
  constructor(database: D1Client | D1DatabaseLike, migrations: Migration[]);
328
384
  run(): Promise<MigrationRunnerResult>;
385
+ /** Does the tracking table carry the `version` column yet? One catalog read. */
386
+ private hasVersionColumn;
387
+ /**
388
+ * Create the tracking table, or bring one written before `version` existed
389
+ * up to the current shape.
390
+ *
391
+ * Two runners can upgrade the same table at once (parallel deploys): both
392
+ * see no column, and the second `ADD COLUMN` fails with "duplicate column
393
+ * name". That failure is only accepted once a re-read shows the column
394
+ * really is there — any other failure of the `ALTER` still surfaces.
395
+ */
396
+ private ensureTrackingTable;
397
+ /**
398
+ * Read the tracking rows, first giving every row recorded before versions
399
+ * were tracked the version whose SQL it hashed — the only link to a version
400
+ * such a row has. A row whose hash matches no current migration stays
401
+ * unversioned, and {@link run} refuses to guess around it.
402
+ */
403
+ private readTrackingRows;
329
404
  private applyOne;
330
405
  private assertUniqueVersions;
331
406
  private assertUniqueSql;
332
407
  }
333
408
  /**
334
- * The canonical SQLite dialect: column affinities, the shared SQLite value
335
- * codec, `RETURNING` support (both D1 and `node:sqlite`), and `sqlite_master`
336
- * table probing. The rest of the per-statement shaping is drizzle's.
337
- */
409
+ * Derive a low-cardinality tag for a rendered statement — `select:users`,
410
+ * `insert:orders`, `select:unknown` when no table token is recognisable.
411
+ *
412
+ * Cardinality is the whole design constraint. The rendered SQL is unique per
413
+ * shape and near-unique per call site, so grouping on it produces a tag space
414
+ * that grows without bound and answers nothing; verb + table is the coarsest
415
+ * label that still points at a specific culprit.
416
+ */
417
+ declare const d1QueryTag: (sql: string) => string;
418
+ /** One statement's cost, read off D1's `meta`. */
419
+ interface D1QueryCost {
420
+ /** D1's own server-side duration in ms, when reported. */
421
+ durationMs?: number;
422
+ /** Rows D1 SCANNED — the billed number, and the one a missing index inflates. */
423
+ rowsRead: number;
424
+ /** Rows written by a DML statement; `0` for reads. */
425
+ rowsWritten: number;
426
+ }
427
+ /**
428
+ * Project D1's `meta` onto {@link D1QueryCost}, or `undefined` when the result
429
+ * carried no usable accounting — a test double, or a `batch()` leg that reports
430
+ * none. Callers skip the emit entirely in that case rather than logging zeroes,
431
+ * which would read as "this query was free".
432
+ */
433
+ declare const readD1QueryCost: (meta: Record<string, unknown> | undefined) => D1QueryCost | undefined;
434
+ /**
435
+ * Emit one structured cost event per D1 statement into Workers Logs, so
436
+ * "D1 reads spiked" resolves to a tag by group-by.
437
+ *
438
+ * Best-effort by contract, exactly like the request-log emit: a serialization
439
+ * hiccup must never turn a served query into a failed one, so the caller wraps
440
+ * this and swallows. Emitting one line per D1 statement is proportionate —
441
+ * every one of them is already a network round trip, so the line is noise-free
442
+ * next to the work it describes.
443
+ *
444
+ * The SQL text itself is NEVER emitted: it carries inlined literals on some
445
+ * render paths, and the tag is the groupable thing anyway.
446
+ */
447
+ declare const emitD1QueryCost: (sql: string, meta: Record<string, unknown> | undefined) => void;
448
+ /** Tuning for {@link withD1Retry}. */
449
+ interface D1RetryOptions {
450
+ /** Total attempts including the first. Default 3. Must be >= 1. */
451
+ attempts?: number;
452
+ /**
453
+ * Total budget across every attempt and backoff, in ms. An attempt still
454
+ * running when the budget expires is abandoned, and a backoff never sleeps
455
+ * past it.
456
+ *
457
+ * `timeoutMs` bounds one attempt; this bounds the operation.
458
+ */
459
+ deadlineMs?: number;
460
+ /** Sleep implementation. Injected so tests need no timers. */
461
+ sleep?: (ms: number) => Promise<void>;
462
+ /**
463
+ * Abandon a single attempt that has not settled within this many ms, and
464
+ * treat it as a transient failure.
465
+ *
466
+ * There is no default — a legitimate analytical query and a stalled one
467
+ * look identical from outside, so the value has to come from what your
468
+ * workload actually needs. Unset, {@link SLOW_FAILURE_MS} stops a slow
469
+ * failure from being retried rather than guessing a bound for you.
470
+ *
471
+ * Only ever applied to operations that are safe to abandon. It does not
472
+ * cancel the underlying D1 call.
473
+ */
474
+ timeoutMs?: number;
475
+ }
476
+ /**
477
+ * True when `error` looks like one of D1's expected transient failures.
478
+ *
479
+ * Deliberately conservative: an unrecognised error is treated as **permanent**
480
+ * and surfaces immediately. Retrying a genuine bug — a syntax error, a
481
+ * constraint violation — turns one fast failure into three slow ones and
482
+ * hides the cause.
483
+ */
484
+ declare const isTransientD1Error: (error: unknown) => boolean;
485
+ /**
486
+ * Thrown when an attempt is abandoned for exceeding
487
+ * {@link D1RetryOptions.timeoutMs}.
488
+ *
489
+ * Its own class so a caller can tell "D1 hung" apart from "D1 returned an
490
+ * error" — they have different remedies, and collapsing them hides which one
491
+ * is happening.
492
+ */
493
+ declare class D1TimeoutError extends Error {
494
+ /** Milliseconds waited before the attempt was abandoned. */
495
+ readonly timeoutMs: number;
496
+ constructor(timeoutMs: number);
497
+ }
498
+ /**
499
+ * Run `operation`, retrying D1's transient failures with exponential backoff
500
+ * and jitter.
501
+ *
502
+ * **Only wrap operations that are safe to run more than once.** Reads always
503
+ * are. A write is only if it is idempotent — an upsert keyed on a primary key,
504
+ * a delete by id, an `INSERT OR IGNORE`. A bare `INSERT` or a relative
505
+ * `UPDATE` is not, and re-running one after a lost response double-applies it.
506
+ */
507
+ declare const withD1Retry: <T>(operation: () => Promise<T>, options?: D1RetryOptions) => Promise<T>;
508
+ /**
509
+ * Wrap a `.global()` exec so its **read-only** statements retry D1's transient
510
+ * failures.
511
+ *
512
+ * This is where the retry has to live to reach an application: `.global()`
513
+ * tables run every read through the exec codegen builds over the raw D1
514
+ * binding, not through a `D1Client`. `run` and `batch` are passed straight
515
+ * through — they are the write path, and a transient D1 error does not say
516
+ * whether the write applied.
517
+ *
518
+ * `all` is not the read path either. It only retries when
519
+ * {@link isReadOnlyD1Sql} proves the statement is one, because `UPDATE …
520
+ * RETURNING` runs through `all` too.
521
+ */
522
+ declare const retryingExec: (exec: SqlCtxExec, options?: D1RetryOptions) => SqlCtxExec;
523
+ /**
524
+ * The canonical SQLite dialect: column affinities, `RETURNING` support (both D1
525
+ * and `node:sqlite`), and `sqlite_master` table probing. The rest of the
526
+ * per-statement shaping is drizzle's. The value codec is NOT a dialect member —
527
+ * the store core runs `sqliteEncode`/`sqliteDecode` on every engine.
528
+ */
338
529
  declare const sqliteDialect: SqlDialect;
339
- export { D1Client, type D1ContextDatabaseOptions as D1CtxDbOptions, type D1DatabaseLike, type D1PreparedStatementLike, D1Session, type D1SessionLike, type ExportGlobalArgs, type FacetGlobalColumnOptions, type ExportRow as GlobalExportRow, type GlobalFacetResult, type GlobalFacetValue, type GlobalFilterClause, type ImportError as GlobalImportError, type ImportResult as GlobalImportResult, type GlobalTableInfo, type GlobalTablePage, type ImportGlobalArgs, type Migration, MigrationRunner, type MigrationRunnerResult, type ReadGlobalTablePageOptions, createD1ContextDatabase as createD1CtxDb, exportGlobalRows, facetGlobalColumn, importGlobalRows, listGlobalTables, readD1CdcChanges, readGlobalTablePage, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, selectGlobalTables, sqliteDialect, trimD1CdcChanges };
530
+ export { D1Client, type D1ContextDatabaseOptions as D1CtxDbOptions, type D1QueryCost, type D1RetryOptions, D1Session, D1TimeoutError, type ExportGlobalArgs, type FacetGlobalColumnOptions, type ExportRow as GlobalExportRow, type GlobalFacetResult, type GlobalFacetValue, type GlobalFilterClause, type ImportError as GlobalImportError, type ImportResult as GlobalImportResult, type GlobalTableInfo, type GlobalTablePage, type ImportGlobalArgs, type Migration, MigrationRunner, type MigrationRunnerResult, type ReadGlobalTablePageOptions, backfillD1SearchIndexes, createD1ContextDatabase as createD1CtxDb, d1QueryTag, emitD1QueryCost, exportGlobalRows, facetGlobalColumn, importGlobalRows, isTransientD1Error, listGlobalTables, readD1CdcChanges, readD1QueryCost, readGlobalTablePage, retryingExec, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, selectGlobalTables, sqliteDialect, sweepD1CdcRetention, withD1Retry };