@lunora/sql-store 1.0.0-alpha.5 → 1.0.0-alpha.51

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,121 +1,130 @@
1
- import { ServerDefaultContextLike, DatabaseWriterLike, SchedulerLike, SchemaLike, TableDefinitionLike, ValidatorLike } from '@lunora/do';
2
- import { SqlDialect, SqlRunResult } from "./dialect.mjs";
1
+ import { TableDefinitionLike, SchemaLike, ServerDefaultContextLike, DatabaseWriterLike, SchedulerLike, ValidatorLike } from '@lunora/do';
2
+ import { SqlRunResult, SqlDialect } from "./dialect.mjs";
3
3
  export type { SqlExec } from "./dialect.mjs";
4
4
  import 'drizzle-orm';
5
+ import '@lunora/search-core';
5
6
  /**
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
- */
7
+ * Async SQL surface the D1 ORM needs: `all` for reads, `run` for writes.
8
+ * Satisfied by a `D1Session`/`D1Client` in production and a `node:sqlite`
9
+ * adapter in tests, so the query logic runs against a real SQLite engine.
10
+ */
10
11
  interface SqlCtxExec {
11
12
  all: (sql: string, parameters: ReadonlyArray<unknown>) => Promise<Record<string, unknown>[]>;
12
13
  run: (sql: string, parameters: ReadonlyArray<unknown>) => Promise<SqlRunResult | void>;
13
14
  }
15
+ /**
16
+ * Decode a SELECTed row back into a document: `id` → `_id`, `_creationTime`
17
+ * preserved, and every column run through the shared {@link sqliteDecode} so the
18
+ * stored form is reversed back into its JS shape. Exported so the data-browser
19
+ * (`introspect.ts`) and admin export/import paths share the exact same decode.
20
+ *
21
+ * The decode is engine-agnostic: every backend stores SQLite-shaped values
22
+ * (boolean → 1/0, JSON → text, bigint → decimal string), and `sqliteDecode` is
23
+ * robust to a driver returning either the stored string OR a natively-parsed
24
+ * value (e.g. mysql2 returns JSON columns pre-parsed) — so the same decoder is
25
+ * correct on SQLite, Postgres and MySQL.
26
+ */
27
+ declare const decodeGlobalRow: (definition: TableDefinitionLike, row: Record<string, unknown>) => Record<string, unknown>;
28
+ /**
29
+ * Provision the search companions, then index one bounded page of the rows that
30
+ * predate each index — unless it is declared `staged: true`, which leaves the
31
+ * whole backfill to {@link backfillSqlSearchIndexes}.
32
+ *
33
+ * Idempotent (`CREATE … IF NOT EXISTS` throughout, and the backfill resumes
34
+ * from recorded progress).
35
+ */
36
+ declare const runSqlSearchMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
37
+ /**
38
+ * Run every declared search index — including the `staged: true` ones the
39
+ * migration pass skips — through to completion. The entry point a host calls
40
+ * out-of-band after deploying a search index over a table too large to index a
41
+ * page at a time.
42
+ *
43
+ * Idempotent and resumable: an index recorded as complete is skipped, and an
44
+ * interrupted run picks up from its cursor.
45
+ */
46
+ declare const backfillSqlSearchIndexes: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
14
47
  interface SqlCtxDbOptions {
15
48
  /**
16
- * Resolved request auth handed to `.serverDefault(fn)` column factories so
17
- * server-trusted columns (owner/tenant ids) stamp from the verified caller,
18
- * never the client. The generated worker passes the per-request identity;
19
- * absent it, server-trusted columns stamp the anonymous slice (`userId: null`).
20
- */
49
+ * Resolved request auth handed to `.serverDefault(fn)` column factories so
50
+ * server-trusted columns (owner/tenant ids) stamp from the verified caller,
51
+ * never the client. The generated worker passes the per-request identity;
52
+ * absent it, server-trusted columns stamp the anonymous slice (`userId: null`).
53
+ */
21
54
  auth?: ServerDefaultContextLike["auth"];
22
55
  /**
23
- * Opt into change-data-capture: when `true`, every committed write appends a
24
- * post-image to the `__cdc_log` table (created lazily alongside the other
25
- * companion tables). Backs CDC streaming export for `.global()` tables — the
26
- * log is for export/CDC consumers, NOT point-in-time recovery: D1's PITR is
27
- * the platform's own Time Travel (`wrangler d1 time-travel restore`), an
28
- * atomic restore, not a changelog replay. Leave undefined for zero-cost
29
- * legacy behaviour.
30
- */
56
+ * Opt into change-data-capture: when `true`, every committed write appends a
57
+ * post-image to the `__cdc_log` table (created lazily alongside the other
58
+ * companion tables). Backs CDC streaming export for `.global()` tables — the
59
+ * log is for export/CDC consumers, NOT point-in-time recovery: D1's PITR is
60
+ * the platform's own Time Travel (`wrangler d1 time-travel restore`), an
61
+ * atomic restore, not a changelog replay. Leave undefined for zero-cost
62
+ * legacy behaviour.
63
+ */
31
64
  cdc?: boolean;
32
65
  clock?: () => number;
33
66
  /**
34
- * Cross-shard counter for **reverse cross-backend relations** — the `_count`
35
- * mirror of the `crossShardReader` option below.
36
- */
67
+ * Cross-shard counter for **reverse cross-backend relations** — the `_count`
68
+ * mirror of the `crossShardReader` option below.
69
+ */
37
70
  crossShardCounter?: DatabaseWriterLike["count"];
38
71
  /**
39
- * Optional cross-shard reader for **reverse cross-backend relations**: a
40
- * `.global()` (D1) parent loading a shard-local (`.shardBy()`/root) child.
41
- * Such a child's rows are partitioned across every shard DO, so the local D1
42
- * writer can't resolve it. When provided, the relation loader routes the
43
- * child's read through this (the host wires it to the Query Coordinator's
44
- * RLS-correct `fanOut`, with identity forwarded so each shard applies its own
45
- * RLS). Absent it, loading such a relation throws a clear "not supported"
46
- * error (legacy behaviour). The forward direction (shard-local parent →
47
- * global child) and same-backend relations never touch this.
48
- */
72
+ * Optional cross-shard reader for **reverse cross-backend relations**: a
73
+ * `.global()` (D1) parent loading a shard-local (`.shardBy()`/root) child.
74
+ * Such a child's rows are partitioned across every shard DO, so the local D1
75
+ * writer can't resolve it. When provided, the relation loader routes the
76
+ * child's read through this (the host wires it to the Query Coordinator's
77
+ * RLS-correct `fanOut`, with identity forwarded so each shard applies its own
78
+ * RLS). Absent it, loading such a relation throws a clear "not supported"
79
+ * error (legacy behaviour). The forward direction (shard-local parent →
80
+ * global child) and same-backend relations never touch this.
81
+ */
49
82
  crossShardReader?: DatabaseWriterLike["findMany"];
50
83
  /**
51
- * The SQL dialect that shapes every statement (identifier quoting, value
52
- * encode/decode, column types, upserts, RETURNING vs affected-rows).
53
- * `@lunora/d1` passes its `sqliteDialect`; the PlanetScale/Hyperdrive backend
54
- * passes its Postgres/MySQL dialect. Required — the core is engine-blind.
55
- */
84
+ * The SQL dialect that shapes every statement (identifier quoting, value
85
+ * encode/decode, column types, upserts, RETURNING vs affected-rows).
86
+ * `@lunora/d1` passes its `sqliteDialect`; the PlanetScale/Hyperdrive backend
87
+ * passes its Postgres/MySQL dialect. Required — the core is engine-blind.
88
+ */
56
89
  dialect: SqlDialect;
57
90
  exec: SqlCtxExec;
58
91
  idGenerator?: () => string;
59
92
  /**
60
- * Ceiling on the number of child join keys a relation-crossing `where`
61
- * predicate may materialize before the semijoin pre-resolver fails closed
62
- * (`relation predicate … exceeding the N-key limit`). D1 has no EXISTS
63
- * push-down, so an overflow here can only fail closed — never truncate the
64
- * `IN (...)` and silently mis-match. Defaults to the pre-resolver's shared
65
- * key cap when omitted.
66
- */
93
+ * Ceiling on the number of child join keys a relation-crossing `where`
94
+ * predicate may materialize before the semijoin pre-resolver fails closed
95
+ * (`relation predicate … exceeding the N-key limit`). D1 has no EXISTS
96
+ * push-down, so an overflow here can only fail closed — never truncate the
97
+ * `IN (...)` and silently mis-match. Defaults to the pre-resolver's shared
98
+ * key cap when omitted.
99
+ */
67
100
  maxRelationKeys?: number;
68
101
  /**
69
- * Scheduler exposed to global-table trigger handlers as `ctx.scheduler`.
70
- * Absent it, `ctx.scheduler` is a stub that throws on use — pass one when
71
- * triggers on `.global()` tables need to enqueue follow-up work.
72
- */
102
+ * Scheduler exposed to global-table trigger handlers as `ctx.scheduler`.
103
+ * Absent it, `ctx.scheduler` is a stub that throws on use — pass one when
104
+ * triggers on `.global()` tables need to enqueue follow-up work.
105
+ */
73
106
  scheduler?: SchedulerLike;
74
107
  schema: SchemaLike;
75
108
  }
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
109
  declare const runSqlGlobalTableMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
90
110
  /**
91
- * Materialize the `__agg_&lt;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
- */
111
+ * Materialize the `__agg_&lt;index>` companion tables for every declared
112
+ * `aggregateIndex` on a global table. Global tables in Lunora ship their own
113
+ * DDL — counter tables are opt-in so production hosts can decide where they
114
+ * live. Tests and dev hosts can call this once after their schema migration to
115
+ * unlock O(1) counts.
116
+ *
117
+ * Idempotent (`CREATE TABLE IF NOT EXISTS`).
118
+ */
99
119
  declare const runSqlAggregateMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
100
120
  /**
101
- * Materialize the `__rank_&lt;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
- */
121
+ * Materialize the `__rank_&lt;index>` companion tables for every declared
122
+ * `rankIndex` on a global table. Mirrors `runSqlAggregateMigrations` — same
123
+ * opt-in pattern so production hosts decide whether to spend the DDL.
124
+ *
125
+ * Idempotent (`CREATE TABLE IF NOT EXISTS` + `createIndexIfNotExists`).
126
+ */
107
127
  declare const runSqlRankMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
108
- /**
109
- * Materialize the `__fts_&lt;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
128
  /** One change-data-capture entry: a committed mutation, in monotonic `seq` order. Mirrors the DO twin. */
120
129
  interface CdcChange {
121
130
  /** Post-image document for insert/update; absent for delete (the `id` identifies the removed row). */
@@ -131,9 +140,9 @@ interface CdcChange {
131
140
  /** Create the `__cdc_log` table. Idempotent; only run when CDC is enabled. */
132
141
  declare const runSqlCdcMigration: (exec: SqlCtxExec, dialect: SqlDialect) => Promise<void>;
133
142
  /**
134
- * Read changelog entries newer than `sinceSeq` in commit order, up to `limit`
135
- * (clamped to [1, 10000]); plus the cursor to resume from.
136
- */
143
+ * Read changelog entries newer than `sinceSeq` in commit order, up to `limit`
144
+ * (clamped to [1, 10000]); plus the cursor to resume from.
145
+ */
137
146
  declare const readSqlCdcChanges: (exec: SqlCtxExec, options: {
138
147
  limit?: number;
139
148
  sinceSeq?: number;
@@ -144,6 +153,10 @@ declare const readSqlCdcChanges: (exec: SqlCtxExec, options: {
144
153
  /** Drop changelog entries at or below a checkpointed `throughSeq` (retention). */
145
154
  declare const trimSqlCdcChanges: (exec: SqlCtxExec, throughSeq: number, dialect: SqlDialect) => Promise<void>;
146
155
  declare const createSqlCtxDb: (options: SqlCtxDbOptions) => DatabaseWriterLike;
156
+ /** Reserved table holding one backfill-progress row per search companion. */
157
+ declare const SEARCH_STATE_TABLE = "__lunora_search_state";
158
+ /** Create the progress table. Idempotent; runs alongside the companion DDL. */
159
+ declare const migrateSearchState: (exec: SqlCtxExec, dialect: SqlDialect) => Promise<void>;
147
160
  /** Map a JS value onto its SQLite storage form — SQLite has no boolean, so true/false → 1/0. */
148
161
  declare const sqliteEncode: (value: unknown) => unknown;
149
162
  /** Parse `raw` as JSON, returning `raw` unchanged when it is not valid JSON. */
@@ -151,24 +164,30 @@ declare const tryJsonParse: (raw: string) => unknown;
151
164
  /** Decode a `bigint` column: a decimal string back into a `BigInt`, else verbatim. */
152
165
  declare const decodeBigint: (raw: unknown) => unknown;
153
166
  /**
154
- * Resolve the *effective* storage kind of a column validator. Encoding keys off
155
- * the runtime value's JS type, so a `v.optional(inner)` column stores its
156
- * present value exactly as `inner` would. The validator's own `kind` is
157
- * `"optional"`, which hides that — unwrap to the inner validator's kind so the
158
- * decode reverses the real storage form. The inner validator is stashed on
159
- * `_meta.inner` by `@lunora/values`' `createValidator`.
160
- */
167
+ * Resolve the *effective* storage kind of a column validator. Encoding keys off
168
+ * the runtime value's JS type, so a `v.optional(inner)` column stores its
169
+ * present value exactly as `inner` would. The validator's own `kind` is
170
+ * `"optional"`, which hides that — unwrap to the inner validator's kind so the
171
+ * decode reverses the real storage form. The inner validator is stashed on
172
+ * `_meta.inner` by `@lunora/values`' `createValidator`.
173
+ */
161
174
  declare const effectiveColumnKind: (validator: ValidatorLike) => string | undefined;
162
175
  /**
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
- * - `object`/`array`/`record`: JSON string → parsed value.
169
- * - `union`/`any`: parsed back only when the stored string is a JSON non-scalar
170
- * (a scalar union member round-trips through SQLite's native column type).
171
- * - everything else (string/number/date/timestamp/id/literal): verbatim.
172
- */
176
+ * Inverse of {@link sqliteEncode}: map a SQLite storage value back onto its JS
177
+ * form, driven by the field's effective validator `kind`:
178
+ *
179
+ * - `boolean`: 1/0 → true/false (SQLite has no boolean type).
180
+ * - `bigint`: decimal string → `BigInt`.
181
+ * - `object`/`array`/`record`: JSON string → parsed value.
182
+ * - `union`/`any`: parsed back only when the stored string is a JSON non-scalar
183
+ * (a scalar union member round-trips through SQLite's native column type).
184
+ * CAVEAT: a union/any member is stored verbatim by {@link sqliteEncode}, so a
185
+ * legitimate *string* value that itself looks like JSON (`'{"a":1}'`, `'[1,2]'`)
186
+ * is ambiguous on read and decodes back to the parsed object/array, not the
187
+ * original string. This is inherent to sharing one TEXT column between a string
188
+ * and an object member; disambiguating would require a breaking storage-format
189
+ * change (tagging encoded non-scalars), so it is documented rather than fixed.
190
+ * - everything else (string/number/date/timestamp/id/literal): verbatim.
191
+ */
173
192
  declare const sqliteDecode: (raw: unknown, kind: string | undefined) => unknown;
174
- export { type SqlCtxDbOptions, type SqlCtxExec, type SqlDialect, type SqlRunResult, createSqlCtxDb, decodeBigint, decodeGlobalRow, effectiveColumnKind, readSqlCdcChanges, runSqlAggregateMigrations, runSqlCdcMigration, runSqlGlobalTableMigrations, runSqlRankMigrations, runSqlSearchMigrations, sqliteDecode, sqliteEncode, trimSqlCdcChanges, tryJsonParse };
193
+ export { SEARCH_STATE_TABLE, type SqlCtxDbOptions, type SqlCtxExec, type SqlDialect, type SqlRunResult, backfillSqlSearchIndexes, createSqlCtxDb, decodeBigint, decodeGlobalRow, effectiveColumnKind, migrateSearchState, readSqlCdcChanges, runSqlAggregateMigrations, runSqlCdcMigration, runSqlGlobalTableMigrations, runSqlRankMigrations, runSqlSearchMigrations, sqliteDecode, sqliteEncode, trimSqlCdcChanges, tryJsonParse };
package/dist/index.d.ts CHANGED
@@ -1,121 +1,130 @@
1
- import { ServerDefaultContextLike, DatabaseWriterLike, SchedulerLike, SchemaLike, TableDefinitionLike, ValidatorLike } from '@lunora/do';
2
- import { SqlDialect, SqlRunResult } from "./dialect.js";
1
+ import { TableDefinitionLike, SchemaLike, ServerDefaultContextLike, DatabaseWriterLike, SchedulerLike, ValidatorLike } from '@lunora/do';
2
+ import { SqlRunResult, SqlDialect } from "./dialect.js";
3
3
  export type { SqlExec } from "./dialect.js";
4
4
  import 'drizzle-orm';
5
+ import '@lunora/search-core';
5
6
  /**
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
- */
7
+ * Async SQL surface the D1 ORM needs: `all` for reads, `run` for writes.
8
+ * Satisfied by a `D1Session`/`D1Client` in production and a `node:sqlite`
9
+ * adapter in tests, so the query logic runs against a real SQLite engine.
10
+ */
10
11
  interface SqlCtxExec {
11
12
  all: (sql: string, parameters: ReadonlyArray<unknown>) => Promise<Record<string, unknown>[]>;
12
13
  run: (sql: string, parameters: ReadonlyArray<unknown>) => Promise<SqlRunResult | void>;
13
14
  }
15
+ /**
16
+ * Decode a SELECTed row back into a document: `id` → `_id`, `_creationTime`
17
+ * preserved, and every column run through the shared {@link sqliteDecode} so the
18
+ * stored form is reversed back into its JS shape. Exported so the data-browser
19
+ * (`introspect.ts`) and admin export/import paths share the exact same decode.
20
+ *
21
+ * The decode is engine-agnostic: every backend stores SQLite-shaped values
22
+ * (boolean → 1/0, JSON → text, bigint → decimal string), and `sqliteDecode` is
23
+ * robust to a driver returning either the stored string OR a natively-parsed
24
+ * value (e.g. mysql2 returns JSON columns pre-parsed) — so the same decoder is
25
+ * correct on SQLite, Postgres and MySQL.
26
+ */
27
+ declare const decodeGlobalRow: (definition: TableDefinitionLike, row: Record<string, unknown>) => Record<string, unknown>;
28
+ /**
29
+ * Provision the search companions, then index one bounded page of the rows that
30
+ * predate each index — unless it is declared `staged: true`, which leaves the
31
+ * whole backfill to {@link backfillSqlSearchIndexes}.
32
+ *
33
+ * Idempotent (`CREATE … IF NOT EXISTS` throughout, and the backfill resumes
34
+ * from recorded progress).
35
+ */
36
+ declare const runSqlSearchMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
37
+ /**
38
+ * Run every declared search index — including the `staged: true` ones the
39
+ * migration pass skips — through to completion. The entry point a host calls
40
+ * out-of-band after deploying a search index over a table too large to index a
41
+ * page at a time.
42
+ *
43
+ * Idempotent and resumable: an index recorded as complete is skipped, and an
44
+ * interrupted run picks up from its cursor.
45
+ */
46
+ declare const backfillSqlSearchIndexes: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
14
47
  interface SqlCtxDbOptions {
15
48
  /**
16
- * Resolved request auth handed to `.serverDefault(fn)` column factories so
17
- * server-trusted columns (owner/tenant ids) stamp from the verified caller,
18
- * never the client. The generated worker passes the per-request identity;
19
- * absent it, server-trusted columns stamp the anonymous slice (`userId: null`).
20
- */
49
+ * Resolved request auth handed to `.serverDefault(fn)` column factories so
50
+ * server-trusted columns (owner/tenant ids) stamp from the verified caller,
51
+ * never the client. The generated worker passes the per-request identity;
52
+ * absent it, server-trusted columns stamp the anonymous slice (`userId: null`).
53
+ */
21
54
  auth?: ServerDefaultContextLike["auth"];
22
55
  /**
23
- * Opt into change-data-capture: when `true`, every committed write appends a
24
- * post-image to the `__cdc_log` table (created lazily alongside the other
25
- * companion tables). Backs CDC streaming export for `.global()` tables — the
26
- * log is for export/CDC consumers, NOT point-in-time recovery: D1's PITR is
27
- * the platform's own Time Travel (`wrangler d1 time-travel restore`), an
28
- * atomic restore, not a changelog replay. Leave undefined for zero-cost
29
- * legacy behaviour.
30
- */
56
+ * Opt into change-data-capture: when `true`, every committed write appends a
57
+ * post-image to the `__cdc_log` table (created lazily alongside the other
58
+ * companion tables). Backs CDC streaming export for `.global()` tables — the
59
+ * log is for export/CDC consumers, NOT point-in-time recovery: D1's PITR is
60
+ * the platform's own Time Travel (`wrangler d1 time-travel restore`), an
61
+ * atomic restore, not a changelog replay. Leave undefined for zero-cost
62
+ * legacy behaviour.
63
+ */
31
64
  cdc?: boolean;
32
65
  clock?: () => number;
33
66
  /**
34
- * Cross-shard counter for **reverse cross-backend relations** — the `_count`
35
- * mirror of the `crossShardReader` option below.
36
- */
67
+ * Cross-shard counter for **reverse cross-backend relations** — the `_count`
68
+ * mirror of the `crossShardReader` option below.
69
+ */
37
70
  crossShardCounter?: DatabaseWriterLike["count"];
38
71
  /**
39
- * Optional cross-shard reader for **reverse cross-backend relations**: a
40
- * `.global()` (D1) parent loading a shard-local (`.shardBy()`/root) child.
41
- * Such a child's rows are partitioned across every shard DO, so the local D1
42
- * writer can't resolve it. When provided, the relation loader routes the
43
- * child's read through this (the host wires it to the Query Coordinator's
44
- * RLS-correct `fanOut`, with identity forwarded so each shard applies its own
45
- * RLS). Absent it, loading such a relation throws a clear "not supported"
46
- * error (legacy behaviour). The forward direction (shard-local parent →
47
- * global child) and same-backend relations never touch this.
48
- */
72
+ * Optional cross-shard reader for **reverse cross-backend relations**: a
73
+ * `.global()` (D1) parent loading a shard-local (`.shardBy()`/root) child.
74
+ * Such a child's rows are partitioned across every shard DO, so the local D1
75
+ * writer can't resolve it. When provided, the relation loader routes the
76
+ * child's read through this (the host wires it to the Query Coordinator's
77
+ * RLS-correct `fanOut`, with identity forwarded so each shard applies its own
78
+ * RLS). Absent it, loading such a relation throws a clear "not supported"
79
+ * error (legacy behaviour). The forward direction (shard-local parent →
80
+ * global child) and same-backend relations never touch this.
81
+ */
49
82
  crossShardReader?: DatabaseWriterLike["findMany"];
50
83
  /**
51
- * The SQL dialect that shapes every statement (identifier quoting, value
52
- * encode/decode, column types, upserts, RETURNING vs affected-rows).
53
- * `@lunora/d1` passes its `sqliteDialect`; the PlanetScale/Hyperdrive backend
54
- * passes its Postgres/MySQL dialect. Required — the core is engine-blind.
55
- */
84
+ * The SQL dialect that shapes every statement (identifier quoting, value
85
+ * encode/decode, column types, upserts, RETURNING vs affected-rows).
86
+ * `@lunora/d1` passes its `sqliteDialect`; the PlanetScale/Hyperdrive backend
87
+ * passes its Postgres/MySQL dialect. Required — the core is engine-blind.
88
+ */
56
89
  dialect: SqlDialect;
57
90
  exec: SqlCtxExec;
58
91
  idGenerator?: () => string;
59
92
  /**
60
- * Ceiling on the number of child join keys a relation-crossing `where`
61
- * predicate may materialize before the semijoin pre-resolver fails closed
62
- * (`relation predicate … exceeding the N-key limit`). D1 has no EXISTS
63
- * push-down, so an overflow here can only fail closed — never truncate the
64
- * `IN (...)` and silently mis-match. Defaults to the pre-resolver's shared
65
- * key cap when omitted.
66
- */
93
+ * Ceiling on the number of child join keys a relation-crossing `where`
94
+ * predicate may materialize before the semijoin pre-resolver fails closed
95
+ * (`relation predicate … exceeding the N-key limit`). D1 has no EXISTS
96
+ * push-down, so an overflow here can only fail closed — never truncate the
97
+ * `IN (...)` and silently mis-match. Defaults to the pre-resolver's shared
98
+ * key cap when omitted.
99
+ */
67
100
  maxRelationKeys?: number;
68
101
  /**
69
- * Scheduler exposed to global-table trigger handlers as `ctx.scheduler`.
70
- * Absent it, `ctx.scheduler` is a stub that throws on use — pass one when
71
- * triggers on `.global()` tables need to enqueue follow-up work.
72
- */
102
+ * Scheduler exposed to global-table trigger handlers as `ctx.scheduler`.
103
+ * Absent it, `ctx.scheduler` is a stub that throws on use — pass one when
104
+ * triggers on `.global()` tables need to enqueue follow-up work.
105
+ */
73
106
  scheduler?: SchedulerLike;
74
107
  schema: SchemaLike;
75
108
  }
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
109
  declare const runSqlGlobalTableMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
90
110
  /**
91
- * Materialize the `__agg_&lt;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
- */
111
+ * Materialize the `__agg_&lt;index>` companion tables for every declared
112
+ * `aggregateIndex` on a global table. Global tables in Lunora ship their own
113
+ * DDL — counter tables are opt-in so production hosts can decide where they
114
+ * live. Tests and dev hosts can call this once after their schema migration to
115
+ * unlock O(1) counts.
116
+ *
117
+ * Idempotent (`CREATE TABLE IF NOT EXISTS`).
118
+ */
99
119
  declare const runSqlAggregateMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
100
120
  /**
101
- * Materialize the `__rank_&lt;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
- */
121
+ * Materialize the `__rank_&lt;index>` companion tables for every declared
122
+ * `rankIndex` on a global table. Mirrors `runSqlAggregateMigrations` — same
123
+ * opt-in pattern so production hosts decide whether to spend the DDL.
124
+ *
125
+ * Idempotent (`CREATE TABLE IF NOT EXISTS` + `createIndexIfNotExists`).
126
+ */
107
127
  declare const runSqlRankMigrations: (exec: SqlCtxExec, schema: SchemaLike, dialect: SqlDialect) => Promise<void>;
108
- /**
109
- * Materialize the `__fts_&lt;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
128
  /** One change-data-capture entry: a committed mutation, in monotonic `seq` order. Mirrors the DO twin. */
120
129
  interface CdcChange {
121
130
  /** Post-image document for insert/update; absent for delete (the `id` identifies the removed row). */
@@ -131,9 +140,9 @@ interface CdcChange {
131
140
  /** Create the `__cdc_log` table. Idempotent; only run when CDC is enabled. */
132
141
  declare const runSqlCdcMigration: (exec: SqlCtxExec, dialect: SqlDialect) => Promise<void>;
133
142
  /**
134
- * Read changelog entries newer than `sinceSeq` in commit order, up to `limit`
135
- * (clamped to [1, 10000]); plus the cursor to resume from.
136
- */
143
+ * Read changelog entries newer than `sinceSeq` in commit order, up to `limit`
144
+ * (clamped to [1, 10000]); plus the cursor to resume from.
145
+ */
137
146
  declare const readSqlCdcChanges: (exec: SqlCtxExec, options: {
138
147
  limit?: number;
139
148
  sinceSeq?: number;
@@ -144,6 +153,10 @@ declare const readSqlCdcChanges: (exec: SqlCtxExec, options: {
144
153
  /** Drop changelog entries at or below a checkpointed `throughSeq` (retention). */
145
154
  declare const trimSqlCdcChanges: (exec: SqlCtxExec, throughSeq: number, dialect: SqlDialect) => Promise<void>;
146
155
  declare const createSqlCtxDb: (options: SqlCtxDbOptions) => DatabaseWriterLike;
156
+ /** Reserved table holding one backfill-progress row per search companion. */
157
+ declare const SEARCH_STATE_TABLE = "__lunora_search_state";
158
+ /** Create the progress table. Idempotent; runs alongside the companion DDL. */
159
+ declare const migrateSearchState: (exec: SqlCtxExec, dialect: SqlDialect) => Promise<void>;
147
160
  /** Map a JS value onto its SQLite storage form — SQLite has no boolean, so true/false → 1/0. */
148
161
  declare const sqliteEncode: (value: unknown) => unknown;
149
162
  /** Parse `raw` as JSON, returning `raw` unchanged when it is not valid JSON. */
@@ -151,24 +164,30 @@ declare const tryJsonParse: (raw: string) => unknown;
151
164
  /** Decode a `bigint` column: a decimal string back into a `BigInt`, else verbatim. */
152
165
  declare const decodeBigint: (raw: unknown) => unknown;
153
166
  /**
154
- * Resolve the *effective* storage kind of a column validator. Encoding keys off
155
- * the runtime value's JS type, so a `v.optional(inner)` column stores its
156
- * present value exactly as `inner` would. The validator's own `kind` is
157
- * `"optional"`, which hides that — unwrap to the inner validator's kind so the
158
- * decode reverses the real storage form. The inner validator is stashed on
159
- * `_meta.inner` by `@lunora/values`' `createValidator`.
160
- */
167
+ * Resolve the *effective* storage kind of a column validator. Encoding keys off
168
+ * the runtime value's JS type, so a `v.optional(inner)` column stores its
169
+ * present value exactly as `inner` would. The validator's own `kind` is
170
+ * `"optional"`, which hides that — unwrap to the inner validator's kind so the
171
+ * decode reverses the real storage form. The inner validator is stashed on
172
+ * `_meta.inner` by `@lunora/values`' `createValidator`.
173
+ */
161
174
  declare const effectiveColumnKind: (validator: ValidatorLike) => string | undefined;
162
175
  /**
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
- * - `object`/`array`/`record`: JSON string → parsed value.
169
- * - `union`/`any`: parsed back only when the stored string is a JSON non-scalar
170
- * (a scalar union member round-trips through SQLite's native column type).
171
- * - everything else (string/number/date/timestamp/id/literal): verbatim.
172
- */
176
+ * Inverse of {@link sqliteEncode}: map a SQLite storage value back onto its JS
177
+ * form, driven by the field's effective validator `kind`:
178
+ *
179
+ * - `boolean`: 1/0 → true/false (SQLite has no boolean type).
180
+ * - `bigint`: decimal string → `BigInt`.
181
+ * - `object`/`array`/`record`: JSON string → parsed value.
182
+ * - `union`/`any`: parsed back only when the stored string is a JSON non-scalar
183
+ * (a scalar union member round-trips through SQLite's native column type).
184
+ * CAVEAT: a union/any member is stored verbatim by {@link sqliteEncode}, so a
185
+ * legitimate *string* value that itself looks like JSON (`'{"a":1}'`, `'[1,2]'`)
186
+ * is ambiguous on read and decodes back to the parsed object/array, not the
187
+ * original string. This is inherent to sharing one TEXT column between a string
188
+ * and an object member; disambiguating would require a breaking storage-format
189
+ * change (tagging encoded non-scalars), so it is documented rather than fixed.
190
+ * - everything else (string/number/date/timestamp/id/literal): verbatim.
191
+ */
173
192
  declare const sqliteDecode: (raw: unknown, kind: string | undefined) => unknown;
174
- export { type SqlCtxDbOptions, type SqlCtxExec, type SqlDialect, type SqlRunResult, createSqlCtxDb, decodeBigint, decodeGlobalRow, effectiveColumnKind, readSqlCdcChanges, runSqlAggregateMigrations, runSqlCdcMigration, runSqlGlobalTableMigrations, runSqlRankMigrations, runSqlSearchMigrations, sqliteDecode, sqliteEncode, trimSqlCdcChanges, tryJsonParse };
193
+ export { SEARCH_STATE_TABLE, type SqlCtxDbOptions, type SqlCtxExec, type SqlDialect, type SqlRunResult, backfillSqlSearchIndexes, createSqlCtxDb, decodeBigint, decodeGlobalRow, effectiveColumnKind, migrateSearchState, readSqlCdcChanges, runSqlAggregateMigrations, runSqlCdcMigration, runSqlGlobalTableMigrations, runSqlRankMigrations, runSqlSearchMigrations, sqliteDecode, sqliteEncode, trimSqlCdcChanges, tryJsonParse };
package/dist/index.mjs CHANGED
@@ -1,2 +1 @@
1
- export { createSqlCtxDb, decodeGlobalRow, readSqlCdcChanges, runSqlAggregateMigrations, runSqlCdcMigration, runSqlGlobalTableMigrations, runSqlRankMigrations, runSqlSearchMigrations, trimSqlCdcChanges } from './packem_shared/createSqlCtxDb-DYqDyPq8.mjs';
2
- export { decodeBigint, effectiveColumnKind, sqliteDecode, sqliteEncode, tryJsonParse } from './packem_shared/decodeBigint-Dedu92k4.mjs';
1
+ import{createSqlCtxDb as o,readSqlCdcChanges as t,runSqlAggregateMigrations as a,runSqlCdcMigration as i,runSqlGlobalTableMigrations as l,runSqlRankMigrations as n,trimSqlCdcChanges as S}from"./packem_shared/createSqlCtxDb-GMqRy9kV.mjs";import{backfillSqlSearchIndexes as d,runSqlSearchMigrations as g}from"./packem_shared/backfillSqlSearchIndexes-DBD_C3SK.mjs";import{SEARCH_STATE_TABLE as s,migrateSearchState as f}from"./packem_shared/SEARCH_STATE_TABLE-DWm7Rza8.mjs";import{decodeBigint as C,effectiveColumnKind as x,sqliteDecode as u,sqliteEncode as b,tryJsonParse as h}from"./packem_shared/decodeBigint-DfiI8pkL.mjs";import{decodeGlobalRow as M}from"./packem_shared/decodeGlobalRow-k4q7Ma1g.mjs";export{s as SEARCH_STATE_TABLE,d as backfillSqlSearchIndexes,o as createSqlCtxDb,C as decodeBigint,M as decodeGlobalRow,x as effectiveColumnKind,f as migrateSearchState,t as readSqlCdcChanges,a as runSqlAggregateMigrations,i as runSqlCdcMigration,l as runSqlGlobalTableMigrations,n as runSqlRankMigrations,g as runSqlSearchMigrations,u as sqliteDecode,b as sqliteEncode,S as trimSqlCdcChanges,h as tryJsonParse};
@@ -0,0 +1 @@
1
+ import{sql as i}from"drizzle-orm";import{queryRun as $,queryAll as E}from"./decodeGlobalRow-k4q7Ma1g.mjs";const a="__lunora_search_state",T=async(e,r)=>{const{integer:n,key:t}=r.companionTypes,o=async()=>{try{await $(e,r,i`ALTER TABLE ${i.identifier(a)} ADD COLUMN ${i.identifier("profile")} ${i.raw(t)}`)}catch{}};await $(e,r,i`CREATE TABLE IF NOT EXISTS ${i.identifier(a)} (${i.identifier("companion")} ${i.raw(t)} PRIMARY KEY, ${i.identifier("cursor")} ${i.raw(t)}, ${i.identifier("done")} ${i.raw(n)} NOT NULL DEFAULT 0, ${i.identifier("profile")} ${i.raw(t)})`),await o()},p=e=>e===1||e===!0||e==="1",S=async(e,r,n)=>{const t=(await E(e,r,i`SELECT ${i.identifier("cursor")}, ${i.identifier("done")}, ${i.identifier("profile")} FROM ${i.identifier(a)} WHERE ${i.identifier("companion")} = ${n}`))[0];if(!t)return{cursor:void 0,done:!1,profile:void 0};const{cursor:o,profile:f}=t;return{cursor:typeof o=="string"?o:void 0,done:p(t.done),profile:typeof f=="string"?f:void 0}},w=async(e,r,n,t,o,f)=>{const d=t??null,c=i`UPDATE ${i.identifier(a)} SET ${i.identifier("cursor")} = ${d}, ${i.identifier("done")} = ${o?1:0}, ${i.identifier("profile")} = ${f} WHERE ${i.identifier("companion")} = ${n}`;if((await E(e,r,i`SELECT ${i.identifier("companion")} FROM ${i.identifier(a)} WHERE ${i.identifier("companion")} = ${n}`)).length>0){await $(e,r,c);return}try{await $(e,r,i`INSERT INTO ${i.identifier(a)} (${i.identifier("companion")}, ${i.identifier("cursor")}, ${i.identifier("done")}, ${i.identifier("profile")}) VALUES (${n}, ${d}, ${o?1:0}, ${f})`)}catch(s){if(!r.isUniqueViolation(s))throw s;await $(e,r,c)}};export{a as SEARCH_STATE_TABLE,T as migrateSearchState,S as readSearchBackfillState,w as writeSearchBackfillState};
@@ -0,0 +1 @@
1
+ import{ftsTableName as y,FTS_ID_COLUMN as d,createSearchAnalyzer as I,analyzedSearchText as C,tokenizeSearch as x,FTS_TEXT_COLUMN as P,searchTermRange as V,countSearchTokens as Y,FTS_TOKEN_COLUMN as L,FTS_COUNT_COLUMN as A,searchTextUnchanged as X,planSearchBackfillPass as z}from"@lunora/search-core";import{sql as e}from"drizzle-orm";import{migrateSearchState as J,readSearchBackfillState as j,writeSearchBackfillState as p}from"./SEARCH_STATE_TABLE-DWm7Rza8.mjs";import{queryRun as u,columnRefSql as h,serializeColumnValue as M,decodeRows as U,queryAll as R,createIndexIfNotExists as b,forEachRowPaged as K}from"./decodeGlobalRow-k4q7Ma1g.mjs";const g=async(t,i,a,r)=>{await u(t,i,e`DELETE FROM ${e.identifier(a)} WHERE ${e.identifier(d)} = ${r}`)},D=(t,i)=>t.name==="mysql"?e`${e.identifier(i)}(191)`:t.textPatternOperatorClass===void 0?e`${e.identifier(i)}`:e`${e.identifier(i)} ${e.raw(t.textPatternOperatorClass)}`,B=50,Q=(t,i)=>i?e`${e.identifier(L)} LIKE ${`${t}%`}`:e`${e.identifier(L)} = ${t}`,Z=async(t,i,a,r,n,s)=>{const o=x(n.query,I(n.definition.language));if(o.length===0)return[];const $=y(r,n.indexName),f=o.length-1,c=o.map((S,w)=>Q(S,w===f)),l=e.join(c,e` OR `),m=c.map(S=>e`SUM(CASE WHEN ${S} THEN ${e.identifier(A)} ELSE 0 END)`),N=e.join(m.map(S=>e`${S} > 0`),e` AND `),T=e`SELECT ${e.identifier(d)}, ${e.join(m,e` + `)} AS ${e.identifier("__score__")} FROM ${e.identifier($)} WHERE ${l} GROUP BY ${e.identifier(d)} HAVING ${N}`,_=[];for(const S of n.filters)_.push(e`m.${h(S.field)} = ${M(S.value)}`);a.softDeleteMode&&_.push(e`m.${h(a.softDeleteMode.field)} IS NULL`);let E=e`SELECT m.* FROM (${T}) s JOIN ${e.identifier(r)} m ON m.${e.identifier("id")} = s.${e.identifier(d)}`;return _.length>0&&(E=e`${E} WHERE ${e.join(_,e` AND `)}`),E=e`${E} ORDER BY s.${e.identifier("__score__")} DESC, m.${e.identifier("_creationTime")} DESC, m.${e.identifier("id")} ASC LIMIT ${e.raw(String(s))}`,U(a,await R(t,i,E))},ee=async(t,i,a,r,n,s)=>{const o=x(n.query,I(n.definition.language));if(o.length===0)return[];const $=y(r,n.indexName),f=`${$}__vocab`,c=o.length-1,l=o.map((E,S)=>{const w=V(E,S===c),G=w.exact?e`${e.identifier("term")} = ${w.lower}`:e`${e.identifier("term")} >= ${w.lower} AND ${e.identifier("term")} < ${w.upper}`;return e`SELECT ${e.identifier("doc")}, ${e.raw(String(S))} AS ${e.identifier("__term__")}, COUNT(*) AS ${e.identifier("__n__")} FROM ${e.identifier(f)} WHERE ${G} GROUP BY ${e.identifier("doc")}`}),m=o.map((E,S)=>e`SUM(CASE WHEN u.${e.identifier("__term__")} = ${e.raw(String(S))} THEN u.${e.identifier("__n__")} ELSE 0 END)`),N=e`SELECT f.${e.identifier(d)} AS ${e.identifier(d)}, ${e.join(m,e` + `)} AS ${e.identifier("__score__")} FROM (${e.join(l,e` UNION ALL `)}) u JOIN ${e.identifier($)} f ON f.rowid = u.${e.identifier("doc")} GROUP BY f.${e.identifier(d)} HAVING ${e.join(m.map(E=>e`${E} > 0`),e` AND `)}`,T=[];for(const E of n.filters)T.push(e`m.${h(E.field)} = ${M(E.value)}`);a.softDeleteMode&&T.push(e`m.${h(a.softDeleteMode.field)} IS NULL`);let _=e`SELECT m.* FROM (${N}) s JOIN ${e.identifier(r)} m ON m.${e.identifier("id")} = s.${e.identifier(d)}`;return T.length>0&&(_=e`${_} WHERE ${e.join(T,e` AND `)}`),_=e`${_} ORDER BY s.${e.identifier("__score__")} DESC, m.${e.identifier("_creationTime")} DESC, m.${e.identifier("id")} ASC LIMIT ${e.raw(String(s))}`,U(a,await R(t,i,_))},ie=async(t,i,a,r,n,s)=>{const o=i.nativeTextSearch,$=x(n.query,I(n.definition.language));if(!o||$.length===0)return[];const f=y(r,n.indexName),c=[o.matches(f,$)];for(const m of n.filters)c.push(e`m.${h(m.field)} = ${M(m.value)}`);a.softDeleteMode&&c.push(e`m.${h(a.softDeleteMode.field)} IS NULL`);const l=e`SELECT m.* FROM ${e.identifier(f)} JOIN ${e.identifier(r)} m ON m.${e.identifier("id")} = ${e.identifier(f)}.${e.identifier(d)} WHERE ${e.join(c,e` AND `)} ORDER BY ${o.rank(f,$)} DESC, m.${e.identifier("_creationTime")} DESC, m.${e.identifier("id")} ASC LIMIT ${e.raw(String(s))}`;return U(a,await R(t,i,l))},te={ensureCompanion:async(t,i,a)=>{const{integer:r,key:n}=i.companionTypes;await u(t,i,e`CREATE TABLE IF NOT EXISTS ${e.identifier(a)} (${e.identifier(L)} ${e.raw(n)} NOT NULL, ${e.identifier(d)} ${e.raw(n)} NOT NULL, ${e.identifier(A)} ${e.raw(r)} NOT NULL)`),await b(t,i,{columns:e`${D(i,L)}, ${D(i,d)}`,name:`${a}__btree`,table:a,unique:!1}),await b(t,i,{columns:D(i,d),name:`${a}__by_id`,table:a,unique:!1})},indexDocument:async(t,i,a,r,n,s)=>{await g(t,i,a,r);const o=[...Y(C(n,s),I(s.language))],$=e.join([L,d,A].map(f=>e.identifier(f)),e`, `);for(let f=0;f<o.length;f+=B){const c=e.join(o.slice(f,f+B).map(([l,m])=>e`(${l}, ${r}, ${m})`),e`, `);await u(t,i,e`INSERT INTO ${e.identifier(a)} (${$}) VALUES ${c}`)}},name:"inverted",runSearch:Z},ae={ensureCompanion:async(t,i,a)=>{await u(t,i,e`CREATE VIRTUAL TABLE IF NOT EXISTS ${e.identifier(a)} USING fts5(${e.identifier(P)}, ${e.identifier(d)} UNINDEXED)`),await u(t,i,e`CREATE VIRTUAL TABLE IF NOT EXISTS ${e.identifier(`${a}__vocab`)} USING fts5vocab(${e.identifier(a)}, ${e.raw("instance")})`)},indexDocument:async(t,i,a,r,n,s)=>{await g(t,i,a,r),await u(t,i,e`INSERT INTO ${e.identifier(a)} (${e.identifier("__text__")}, ${e.identifier(d)}) VALUES (${C(n,s)}, ${r})`)},name:"fts5",runSearch:ee},ne={ensureCompanion:async(t,i,a)=>{const r=i.nativeTextSearch;if(r){await u(t,i,r.createCompanion(a,i.companionTypes.key));for(const n of r.createIndexes(a))await u(t,i,n)}},indexDocument:async(t,i,a,r,n,s)=>{const o=i.nativeTextSearch;o&&(await g(t,i,a,r),await u(t,i,o.indexDocument(a,r,C(n,s))))},name:"native",runSearch:ie},O=(t,i)=>t.strategy==="native"&&i.nativeTextSearch!==void 0?ne:i.supportsFts5?ae:te,H=(t,i)=>`${I(t.language).profile}/${O(t,i).name}`,v=function*(t){for(const[i,a]of Object.entries(t.tables)){const r=a.searchIndexes;if(!(a.shardMode!==void 0&&a.shardMode.kind!=="global"||!r))for(const n of r)yield[i,a,n]}},F=(t,i)=>y(t,i.name),$e=async(t,i,a,r,n,s)=>O(n.definition,i).runSearch(t,i,a,r,n,s),q=200,k=async(t,i,a,r,n)=>{const s=F(r,n),o=O(n,i),$=H(n,i),f=z(await j(t,i,s),$);if(f.finished)return!0;if(f.wipe&&(await u(t,i,e`DELETE FROM ${e.identifier(s)}`),await p(t,i,s,void 0,!1,$)),(await R(t,i,i.tableExists(r))).length===0)return await p(t,i,s,void 0,!0,$),!0;let c=0,l=f.cursor;await K(t,i,a,r,async N=>{c+=1;const T=N._id;typeof T=="string"&&(l=T,await o.indexDocument(t,i,s,T,N,n))},{after:f.cursor,limit:q});const m=c<q;return await p(t,i,s,l,m,$),m},W=async(t,i,a)=>{await J(t,a);for(const[r,,n]of v(i)){const s=F(r,n),o=H(n,a),$=await j(t,a,s);$.profile!==void 0&&$.profile!==o&&(await u(t,a,e`DROP TABLE IF EXISTS ${e.identifier(s)}`),await p(t,a,s,void 0,!1,o)),await O(n,a).ensureCompanion(t,a,s)}},ce=async(t,i,a)=>{await W(t,i,a);for(const[r,n,s]of v(i))s.staged||await k(t,a,n,r,s)},de=async(t,i,a)=>{await W(t,i,a);for(const[r,n,s]of v(i)){let o=!1;for(;!o;)o=await k(t,a,n,r,s)}},le=t=>{const{dialect:i,exec:a,schema:r}=t;return async(n,s,o,$)=>{const f=r.tables[n]?.searchIndexes;if(!(!f||f.length===0))for(const c of f){if(X($,o,c))continue;const l=F(n,c);if(o){await O(c,i).indexDocument(a,i,l,s,o,c);continue}await g(a,i,l,s)}}};export{de as backfillSqlSearchIndexes,le as createSearchSync,$e as runSqlSearch,ce as runSqlSearchMigrations};