@lunora/d1 0.0.0 → 1.0.0-alpha.2

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.
@@ -0,0 +1,339 @@
1
+ import { SchemaLike, DatabaseWriterLike } from '@lunora/do';
2
+ import { SqlCtxDbOptions, createSqlCtxDb, SqlCtxExec, readSqlCdcChanges, SqlDialect } from '@lunora/sql-store';
3
+ export { type SqlCtxExec as D1Exec, type SqlCtxDbOptions, type SqlCtxExec, createSqlCtxDb } from '@lunora/sql-store';
4
+ import { BatchItem, BatchResponse } from 'drizzle-orm/batch';
5
+ import { DrizzleD1Database } from 'drizzle-orm/d1';
6
+ /** The D1 store options — the shared store options minus `dialect` (the SQLite dialect is injected for you). */
7
+ type D1ContextDatabaseOptions = Omit<SqlCtxDbOptions, "dialect">;
8
+ /** Build a D1-backed `.global()` writer (the shared store core bound to the SQLite dialect). */
9
+ declare const createD1ContextDatabase: (options: D1ContextDatabaseOptions) => ReturnType<typeof createSqlCtxDb>;
10
+ /** Auto-provision the schema's `.global()` tables in D1 (idempotent `CREATE TABLE IF NOT EXISTS`). */
11
+ declare const runD1GlobalTableMigrations: (exec: SqlCtxExec, schema: SchemaLike) => Promise<void>;
12
+ /** Materialize the `__agg_&lt;index>` companion tables for the schema's aggregate indexes. */
13
+ declare const runD1AggregateMigrations: (exec: SqlCtxExec, schema: SchemaLike) => Promise<void>;
14
+ /** Materialize the `__rank_&lt;index>` companion tables for the schema's rank indexes. */
15
+ 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). */
17
+ declare const runD1SearchMigrations: (exec: SqlCtxExec, schema: SchemaLike) => Promise<void>;
18
+ /** Create the `__cdc_log` table in D1 (idempotent; only run when CDC is enabled). */
19
+ declare const runD1CdcMigration: (exec: SqlCtxExec) => Promise<void>;
20
+ /** Read changelog entries newer than `sinceSeq` in commit order. */
21
+ declare const readD1CdcChanges: (exec: SqlCtxExec, options?: {
22
+ limit?: number;
23
+ sinceSeq?: number;
24
+ }) => ReturnType<typeof readSqlCdcChanges>;
25
+ /** Drop changelog entries at or below a checkpointed `throughSeq`. */
26
+ declare const trimD1CdcChanges: (exec: SqlCtxExec, throughSeq: number) => Promise<void>;
27
+ /** One exported row: `doc` is reconstructed from the column tuple. */
28
+ interface ExportRow {
29
+ doc: Record<string, unknown>;
30
+ table: string;
31
+ }
32
+ interface ImportError {
33
+ code: string;
34
+ line: number;
35
+ message: string;
36
+ table: string;
37
+ }
38
+ interface ImportResult {
39
+ /** Skipped rows whose `_id` already exists. */
40
+ conflicts: number;
41
+ errors: ImportError[];
42
+ inserted: Record<string, number>;
43
+ }
44
+ /**
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
+ */
49
+ declare const selectGlobalTables: (schema: SchemaLike, requested?: ReadonlyArray<string>) => string[];
50
+ interface ExportGlobalArgs {
51
+ batchSize?: number;
52
+ tables?: ReadonlyArray<string>;
53
+ }
54
+ /**
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
+ */
61
+ declare const exportGlobalRows: (exec: SqlCtxExec, schema: SchemaLike, args: ExportGlobalArgs) => AsyncGenerator<ExportRow, void, undefined>;
62
+ interface ImportGlobalArgs {
63
+ /**
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
+ */
71
+ exec?: D1ExecLike;
72
+ rows: ReadonlyArray<ExportRow>;
73
+ startLine?: number;
74
+ }
75
+ /** Minimal slice of `D1Exec` (declared locally to avoid a circular import). */
76
+ interface D1ExecLike {
77
+ all: (sql: string, parameters: ReadonlyArray<unknown>) => Promise<Record<string, unknown>[]>;
78
+ }
79
+ /**
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
+ */
86
+ declare const importGlobalRows: (writer: DatabaseWriterLike, schema: SchemaLike, args: ImportGlobalArgs) => Promise<ImportResult>;
87
+ /**
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. */
117
+ declare class D1Session {
118
+ private readonly session;
119
+ /** See {@link D1Client.stmtCache}. Scoped per session. */
120
+ private readonly stmtCache;
121
+ constructor(session: D1SessionLike);
122
+ prepare(sql: string): D1PreparedStatementLike;
123
+ run<T = unknown>(sql: string, ...binds: unknown[]): Promise<{
124
+ meta?: Record<string, unknown>;
125
+ results?: T[];
126
+ success: boolean;
127
+ }>;
128
+ all<T = unknown>(sql: string, ...binds: unknown[]): Promise<{
129
+ results: T[];
130
+ success: boolean;
131
+ }>;
132
+ first<T = unknown>(sql: string, ...binds: unknown[]): Promise<T | null>;
133
+ /**
134
+ * Returns the most recent bookmark known to the session, or `undefined`
135
+ * when D1 has not issued one yet.
136
+ */
137
+ getBookmark(): string | undefined;
138
+ }
139
+ declare class D1Client {
140
+ private readonly db;
141
+ /**
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
+ */
147
+ private readonly stmtCache;
148
+ /**
149
+ * Lazily-built drizzle handle over the bare binding. Memoised so a single
150
+ * `D1Client` reuses the same dialect/session machinery across calls.
151
+ */
152
+ private drizzleHandle;
153
+ constructor(database: D1DatabaseLike);
154
+ /**
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
+ */
165
+ withSession(bookmark?: string): D1Session;
166
+ /**
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
+ */
172
+ prepare(sql: string): D1PreparedStatementLike;
173
+ /**
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
+ */
179
+ get drizzle(): DrizzleD1Database<Record<string, unknown>>;
180
+ /**
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
+ */
189
+ drizzleSession(bookmark?: string): DrizzleD1Database<Record<string, unknown>>;
190
+ /**
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
+ */
195
+ batch<U extends BatchItem<"sqlite">, T extends Readonly<[U, ...U[]]>>(items: T): Promise<BatchResponse<T>>;
196
+ /** Direct access to the underlying binding (advanced use only). */
197
+ get raw(): D1DatabaseLike;
198
+ }
199
+ /** A table plus its current row count. */
200
+ interface GlobalTableInfo {
201
+ name: string;
202
+ rowCount: number;
203
+ }
204
+ /** A window of rows from one table, plus the column list and total size. */
205
+ interface GlobalTablePage {
206
+ columns: string[];
207
+ /**
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
+ */
214
+ refs?: Record<string, string>;
215
+ rows: Record<string, unknown>[];
216
+ total: number;
217
+ }
218
+ /**
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
+ */
226
+ interface GlobalFilterClause {
227
+ column: string;
228
+ value: unknown;
229
+ }
230
+ interface ReadGlobalTablePageOptions {
231
+ filters?: GlobalFilterClause[];
232
+ limit?: number;
233
+ offset?: number;
234
+ table: string;
235
+ }
236
+ /**
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
+ */
244
+ interface FacetGlobalColumnOptions {
245
+ column: string;
246
+ filters?: GlobalFilterClause[];
247
+ limit?: number;
248
+ table: string;
249
+ }
250
+ /** One distinct value of a faceted global column with its row count over the active view. */
251
+ interface GlobalFacetValue {
252
+ count: number;
253
+ value: unknown;
254
+ }
255
+ /**
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
+ */
261
+ interface GlobalFacetResult {
262
+ truncated: boolean;
263
+ values: GlobalFacetValue[];
264
+ }
265
+ /**
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
+ */
270
+ declare const listGlobalTables: (exec: SqlCtxExec, schema: SchemaLike) => Promise<GlobalTableInfo[]>;
271
+ /**
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
+ */
279
+ declare const readGlobalTablePage: (exec: SqlCtxExec, schema: SchemaLike, options: ReadGlobalTablePageOptions) => Promise<GlobalTablePage>;
280
+ /**
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
+ */
293
+ declare const facetGlobalColumn: (exec: SqlCtxExec, schema: SchemaLike, options: FacetGlobalColumnOptions) => Promise<GlobalFacetResult>;
294
+ interface Migration {
295
+ /** Human-readable name, e.g. `001_init` (used in logs). */
296
+ name: string;
297
+ /** Raw SQL to apply. Should be idempotent where possible. */
298
+ sql: string;
299
+ /** Monotonically increasing integer used to order migrations. */
300
+ version: number;
301
+ }
302
+ interface MigrationRunnerResult {
303
+ applied: {
304
+ name: string;
305
+ version: number;
306
+ }[];
307
+ skipped: {
308
+ name: string;
309
+ version: number;
310
+ }[];
311
+ }
312
+ /**
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
+ */
319
+ declare class MigrationRunner {
320
+ private readonly client;
321
+ private readonly migrations;
322
+ /**
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
+ */
327
+ constructor(database: D1Client | D1DatabaseLike, migrations: Migration[]);
328
+ run(): Promise<MigrationRunnerResult>;
329
+ private applyOne;
330
+ private assertUniqueVersions;
331
+ private assertUniqueSql;
332
+ }
333
+ /**
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
+ */
338
+ 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 };
package/dist/index.mjs ADDED
@@ -0,0 +1,7 @@
1
+ export { exportGlobalRows, importGlobalRows, selectGlobalTables } from './packem_shared/exportGlobalRows-BGCPm_nA.mjs';
2
+ export { D1Client, D1Session } from './packem_shared/D1Client-DA3flo1o.mjs';
3
+ export { createD1CtxDb, readD1CdcChanges, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, trimD1CdcChanges } from './packem_shared/createD1CtxDb-BMR8J0dT.mjs';
4
+ export { facetGlobalColumn, listGlobalTables, readGlobalTablePage } from './packem_shared/facetGlobalColumn-C6u_WMIY.mjs';
5
+ export { MigrationRunner } from './packem_shared/MigrationRunner-BkEwQ-Ya.mjs';
6
+ export { default as sqliteDialect } from './packem_shared/sqliteDialect-DqYnHPuu.mjs';
7
+ export { createSqlCtxDb } from '@lunora/sql-store';
@@ -0,0 +1,143 @@
1
+ import { drizzle } from 'drizzle-orm/d1';
2
+
3
+ const STMT_CACHE_CAPACITY = 256;
4
+ const D1_FIRST_UNCONSTRAINED = "first-unconstrained";
5
+ class D1Session {
6
+ session;
7
+ /** See {@link D1Client.stmtCache}. Scoped per session. */
8
+ stmtCache = /* @__PURE__ */ new Map();
9
+ constructor(session) {
10
+ this.session = session;
11
+ }
12
+ prepare(sql) {
13
+ const cached = this.stmtCache.get(sql);
14
+ if (cached) {
15
+ this.stmtCache.delete(sql);
16
+ this.stmtCache.set(sql, cached);
17
+ return cached;
18
+ }
19
+ const stmt = this.session.prepare(sql);
20
+ if (this.stmtCache.size >= STMT_CACHE_CAPACITY) {
21
+ const oldest = this.stmtCache.keys().next().value;
22
+ if (oldest !== void 0) {
23
+ this.stmtCache.delete(oldest);
24
+ }
25
+ }
26
+ this.stmtCache.set(sql, stmt);
27
+ return stmt;
28
+ }
29
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T types the result rows for the caller and is forwarded to the prepared statement.
30
+ async run(sql, ...binds) {
31
+ return this.prepare(sql).bind(...binds).run();
32
+ }
33
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T types the result rows for the caller and is forwarded to the prepared statement.
34
+ async all(sql, ...binds) {
35
+ return this.prepare(sql).bind(...binds).all();
36
+ }
37
+ async first(sql, ...binds) {
38
+ return this.prepare(sql).bind(...binds).first();
39
+ }
40
+ /**
41
+ * Returns the most recent bookmark known to the session, or `undefined`
42
+ * when D1 has not issued one yet.
43
+ */
44
+ getBookmark() {
45
+ return this.session.getBookmark() ?? void 0;
46
+ }
47
+ }
48
+ class D1Client {
49
+ db;
50
+ /**
51
+ * SQL string -> prepared statement. Prepared statements are reusable in
52
+ * D1; preparing the same SQL twice forces the worker to round-trip the
53
+ * statement plan. Caching is per-instance so unit-test isolation holds.
54
+ * Bounded to {@link STMT_CACHE_CAPACITY} via LRU eviction.
55
+ */
56
+ stmtCache = /* @__PURE__ */ new Map();
57
+ /**
58
+ * Lazily-built drizzle handle over the bare binding. Memoised so a single
59
+ * `D1Client` reuses the same dialect/session machinery across calls.
60
+ */
61
+ drizzleHandle;
62
+ constructor(database) {
63
+ this.db = database;
64
+ }
65
+ /**
66
+ * Open a Sessions-API scoped session. Pass the bookmark forwarded by
67
+ * the client to opt into read-your-writes consistency.
68
+ *
69
+ * With no bookmark this is the first request of a session — there is no
70
+ * prior write to read, so we open with the explicit `"first-unconstrained"`
71
+ * constraint (Cloudflare's lowest-latency default: the first read may serve
72
+ * from any replica). Read-your-writes for sequenced requests still flows
73
+ * through the forwarded bookmark; a caller needing a strongly-consistent
74
+ * very-first read should pass `"first-primary"` as the bookmark instead.
75
+ */
76
+ withSession(bookmark) {
77
+ const session = this.db.withSession(bookmark ?? D1_FIRST_UNCONSTRAINED);
78
+ return new D1Session(session);
79
+ }
80
+ /**
81
+ * Prepare a statement, reusing a cached one when the SQL text matches.
82
+ * `bind()` on a prepared statement returns a new bound statement and
83
+ * leaves the underlying prepared plan reusable, so cache hits are safe
84
+ * even when the previous caller already called `.bind(...).run()`.
85
+ */
86
+ prepare(sql) {
87
+ const cached = this.stmtCache.get(sql);
88
+ if (cached) {
89
+ this.stmtCache.delete(sql);
90
+ this.stmtCache.set(sql, cached);
91
+ return cached;
92
+ }
93
+ const stmt = this.db.prepare(sql);
94
+ if (this.stmtCache.size >= STMT_CACHE_CAPACITY) {
95
+ const oldest = this.stmtCache.keys().next().value;
96
+ if (oldest !== void 0) {
97
+ this.stmtCache.delete(oldest);
98
+ }
99
+ }
100
+ this.stmtCache.set(sql, stmt);
101
+ return stmt;
102
+ }
103
+ /**
104
+ * Drizzle handle over the bare `env.DB` binding. Used for typed queries
105
+ * against generated `sqliteTable` schemas; does **not** participate in the
106
+ * D1 Sessions API (no bookmark pinning). For bookmark-scoped reads, use
107
+ * {@link drizzleSession} instead.
108
+ */
109
+ get drizzle() {
110
+ if (this.drizzleHandle) {
111
+ return this.drizzleHandle;
112
+ }
113
+ this.drizzleHandle = drizzle(this.db, { logger: false });
114
+ return this.drizzleHandle;
115
+ }
116
+ /**
117
+ * Drizzle handle scoped to a D1 Sessions-API session. The bookmark, when
118
+ * supplied, opts into read-your-writes consistency for follow-up reads on
119
+ * the same session.
120
+ *
121
+ * A `D1DatabaseSession` exposes the same `prepare` / `batch` surface
122
+ * drizzle calls into, so a single `unknown` cast lets us treat the session
123
+ * as a `D1Database` for driver-construction purposes.
124
+ */
125
+ drizzleSession(bookmark) {
126
+ const session = this.db.withSession(bookmark ?? D1_FIRST_UNCONSTRAINED);
127
+ return drizzle(session, { logger: false });
128
+ }
129
+ /**
130
+ * Atomic batch over the drizzle d1 driver. Mirrors `db.batch([...])`
131
+ * exactly; exposed on the client so callers don't need to hold a drizzle
132
+ * handle just to run a typed batch.
133
+ */
134
+ async batch(items) {
135
+ return this.drizzle.batch(items);
136
+ }
137
+ /** Direct access to the underlying binding (advanced use only). */
138
+ get raw() {
139
+ return this.db;
140
+ }
141
+ }
142
+
143
+ export { D1Client, D1Session };
@@ -0,0 +1,149 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { D1Client } from './D1Client-DA3flo1o.mjs';
3
+
4
+ const TRACKING_TABLE_NAME = "__drizzle_migrations";
5
+ const TRACKING_TABLE_DDL = `CREATE TABLE IF NOT EXISTS ${TRACKING_TABLE_NAME} (id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT NOT NULL, created_at NUMERIC)`;
6
+ const WHITESPACE_RE = /\s/u;
7
+ const TRAILING_SEMICOLON_RE = /;\s*$/u;
8
+ const SHA256_HEX_RE = /^[0-9a-f]{64}$/u;
9
+ const assertSingleStatement = (migration) => {
10
+ const text = migration.sql;
11
+ let inSingle = false;
12
+ let inDouble = false;
13
+ let inLineComment = false;
14
+ let inBlockComment = false;
15
+ let seenStatement = false;
16
+ for (let index = 0; index < text.length; index += 1) {
17
+ const character = text[index];
18
+ const next = text[index + 1];
19
+ if (inLineComment) {
20
+ if (character === "\n") {
21
+ inLineComment = false;
22
+ }
23
+ continue;
24
+ }
25
+ if (inBlockComment) {
26
+ if (character === "*" && next === "/") {
27
+ inBlockComment = false;
28
+ index += 1;
29
+ }
30
+ continue;
31
+ }
32
+ if (inSingle) {
33
+ if (character === "'") {
34
+ if (next === "'") {
35
+ index += 1;
36
+ } else {
37
+ inSingle = false;
38
+ }
39
+ }
40
+ continue;
41
+ }
42
+ if (inDouble) {
43
+ if (character === '"') {
44
+ if (next === '"') {
45
+ index += 1;
46
+ } else {
47
+ inDouble = false;
48
+ }
49
+ }
50
+ continue;
51
+ }
52
+ if (character === "'") {
53
+ inSingle = true;
54
+ continue;
55
+ }
56
+ if (character === '"') {
57
+ inDouble = true;
58
+ continue;
59
+ }
60
+ if (character === "-" && next === "-") {
61
+ inLineComment = true;
62
+ index += 1;
63
+ continue;
64
+ }
65
+ if (character === "/" && next === "*") {
66
+ inBlockComment = true;
67
+ index += 1;
68
+ continue;
69
+ }
70
+ if (character === ";") {
71
+ seenStatement = true;
72
+ continue;
73
+ }
74
+ if (seenStatement && character !== void 0 && !WHITESPACE_RE.test(character)) {
75
+ throw new Error(
76
+ `Migration "${migration.name}" (v${String(migration.version)}) contains more than one SQL statement. Split it into separate migrations — batch() runs them atomically.`
77
+ );
78
+ }
79
+ }
80
+ };
81
+ const hashMigration = async (text) => {
82
+ const bytes = new TextEncoder().encode(text);
83
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
84
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
85
+ };
86
+ class MigrationRunner {
87
+ client;
88
+ migrations;
89
+ /**
90
+ * Accepts either a {@link D1Client} (preferred — gets typed batches +
91
+ * drizzle handle for free) or a raw `D1DatabaseLike` binding (wrapped on
92
+ * the caller's behalf so existing `@lunora/cli` callers keep working).
93
+ */
94
+ constructor(database, migrations) {
95
+ this.client = database instanceof D1Client ? database : new D1Client(database);
96
+ this.migrations = [...migrations].toSorted((a, b) => a.version - b.version);
97
+ this.assertUniqueVersions();
98
+ this.assertUniqueSql();
99
+ }
100
+ async run() {
101
+ await this.client.drizzle.run(sql.raw(TRACKING_TABLE_DDL));
102
+ const appliedRows = await this.client.drizzle.all(sql.raw(`SELECT hash FROM ${TRACKING_TABLE_NAME}`));
103
+ const appliedHashes = new Set(appliedRows.map((row) => row.hash));
104
+ const applied = [];
105
+ const skipped = [];
106
+ const hashes = await Promise.all(this.migrations.map(async (migration) => hashMigration(migration.sql)));
107
+ for (const [index, migration] of this.migrations.entries()) {
108
+ const hash = hashes[index];
109
+ if (appliedHashes.has(hash)) {
110
+ skipped.push({ name: migration.name, version: migration.version });
111
+ continue;
112
+ }
113
+ await this.applyOne(migration, hash);
114
+ applied.push({ name: migration.name, version: migration.version });
115
+ }
116
+ return { applied, skipped };
117
+ }
118
+ async applyOne(migration, hash) {
119
+ assertSingleStatement(migration);
120
+ const statementText = migration.sql.replace(TRAILING_SEMICOLON_RE, "").trim();
121
+ if (!SHA256_HEX_RE.test(hash)) {
122
+ throw new Error(`migration "${migration.name}" produced a non-hex hash; refusing to inline into SQL`);
123
+ }
124
+ const trackingInsertSql = `INSERT INTO ${TRACKING_TABLE_NAME} (hash, created_at) VALUES ('${hash}', ${String(Date.now())})`;
125
+ const items = [this.client.drizzle.run(sql.raw(statementText)), this.client.drizzle.run(sql.raw(trackingInsertSql))];
126
+ await this.client.batch(items);
127
+ }
128
+ assertUniqueVersions() {
129
+ const seen = /* @__PURE__ */ new Set();
130
+ for (const m of this.migrations) {
131
+ if (seen.has(m.version)) {
132
+ throw new Error(`Duplicate migration version ${String(m.version)}`);
133
+ }
134
+ seen.add(m.version);
135
+ }
136
+ }
137
+ assertUniqueSql() {
138
+ const seen = /* @__PURE__ */ new Map();
139
+ for (const m of this.migrations) {
140
+ const previousVersion = seen.get(m.sql);
141
+ if (previousVersion !== void 0) {
142
+ throw new Error(`Migrations ${String(previousVersion)} and ${String(m.version)} have identical SQL — bump the content, not just the version.`);
143
+ }
144
+ seen.set(m.sql, m.version);
145
+ }
146
+ }
147
+ }
148
+
149
+ export { MigrationRunner };
@@ -0,0 +1,14 @@
1
+ import { createSqlCtxDb, readSqlCdcChanges, runSqlAggregateMigrations, runSqlCdcMigration, runSqlGlobalTableMigrations, runSqlRankMigrations, runSqlSearchMigrations, trimSqlCdcChanges } from '@lunora/sql-store';
2
+ export { createSqlCtxDb, decodeGlobalRow } from '@lunora/sql-store';
3
+ import sqliteDialect from './sqliteDialect-DqYnHPuu.mjs';
4
+
5
+ const createD1ContextDatabase = (options) => createSqlCtxDb({ ...options, dialect: sqliteDialect });
6
+ const runD1GlobalTableMigrations = (exec, schema) => runSqlGlobalTableMigrations(exec, schema, sqliteDialect);
7
+ const runD1AggregateMigrations = (exec, schema) => runSqlAggregateMigrations(exec, schema, sqliteDialect);
8
+ const runD1RankMigrations = (exec, schema) => runSqlRankMigrations(exec, schema, sqliteDialect);
9
+ const runD1SearchMigrations = (exec, schema) => runSqlSearchMigrations(exec, schema, sqliteDialect);
10
+ const runD1CdcMigration = (exec) => runSqlCdcMigration(exec, sqliteDialect);
11
+ const readD1CdcChanges = (exec, options = {}) => readSqlCdcChanges(exec, options, sqliteDialect);
12
+ const trimD1CdcChanges = (exec, throughSeq) => trimSqlCdcChanges(exec, throughSeq, sqliteDialect);
13
+
14
+ export { createD1ContextDatabase as createD1CtxDb, readD1CdcChanges, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, trimD1CdcChanges };