@lunora/sql-store 1.0.0-alpha.15 → 1.0.0-alpha.151
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +6 -0
- package/README.md +10 -8
- package/dist/dialect.d.mts +161 -41
- package/dist/dialect.d.ts +161 -41
- package/dist/dialect.mjs +0 -1
- package/dist/index.d.mts +374 -129
- package/dist/index.d.ts +374 -129
- package/dist/index.mjs +1 -2
- package/dist/packem_shared/SEARCH_STATE_TABLE-CTjm9HUb.mjs +1 -0
- package/dist/packem_shared/backfillSqlSearchIndexes-BWtqC8SB.mjs +1 -0
- package/dist/packem_shared/createSqlCtxDb-LjE6a4t0.mjs +7 -0
- package/dist/packem_shared/ctx-db-search-CpGgDAhl.mjs +1 -0
- package/dist/packem_shared/ctx-db-search-state-TMs6Zlae.mjs +1 -0
- package/dist/packem_shared/decodeBigint-BQtyR2O3.mjs +1 -0
- package/dist/packem_shared/decodeGlobalRow-P2Z1dMjU.mjs +1 -0
- package/dist/packem_shared/runSqlAggregateMigrations-ByqTfw-y.mjs +1 -0
- package/dist/packem_shared/value-codec-CBr0_L1N.mjs +1 -0
- package/package.json +3 -2
- package/dist/packem_shared/createSqlCtxDb-BlGq23Wp.mjs +0 -1784
- package/dist/packem_shared/decodeBigint-Dedu92k4.mjs +0 -69
package/LICENSE.md
CHANGED
|
@@ -103,3 +103,9 @@ Unless required by applicable law or agreed to in writing, software distributed
|
|
|
103
103
|
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
104
|
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
105
|
specific language governing permissions and limitations under the License.
|
|
106
|
+
|
|
107
|
+
<!-- DEPENDENCIES -->
|
|
108
|
+
<!-- /DEPENDENCIES -->
|
|
109
|
+
|
|
110
|
+
<!-- TYPE_DEPENDENCIES -->
|
|
111
|
+
<!-- /TYPE_DEPENDENCIES -->
|
package/README.md
CHANGED
|
@@ -57,12 +57,13 @@ hand-rolled rewriting.
|
|
|
57
57
|
|
|
58
58
|
The small per-engine `SqlDialect` value object carries only what drizzle can't
|
|
59
59
|
infer from a dynamic, column-per-field schema: the framework columns every
|
|
60
|
-
global table carries, column and companion-table types,
|
|
61
|
-
(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
global table carries, column and companion-table types, `RETURNING` availability
|
|
61
|
+
(with an affected-rows fallback for MySQL), unique-violation detection, the MySQL index
|
|
62
|
+
key-prefix, and the system-catalog (`tableExists`) probe. The value codec is
|
|
63
|
+
**not** a dialect member — every engine stores SQLite-shaped values through the
|
|
64
|
+
core's own `sqliteEncode`/`sqliteDecode`. Full-text search is declared, not
|
|
65
|
+
probed: the dialect carries `supportsFts5`, and the core picks the FTS5 or
|
|
66
|
+
portable inverted-index layout from it.
|
|
66
67
|
|
|
67
68
|
Reactivity is engine-independent: the writer is injected as `globalDb` into
|
|
68
69
|
`createShardCtxDb`, whose `broadcast` hook drives live queries no matter which
|
|
@@ -83,8 +84,9 @@ seam from the root); consumers import everything from the root `@lunora/sql-stor
|
|
|
83
84
|
- Migration runners (each takes `(exec, schema, dialect)`, or `(exec, dialect)`
|
|
84
85
|
for CDC): `runSqlGlobalTableMigrations`, `runSqlAggregateMigrations`,
|
|
85
86
|
`runSqlRankMigrations`, `runSqlSearchMigrations`, `runSqlCdcMigration`.
|
|
86
|
-
- CDC log helpers: `readSqlCdcChanges`, `
|
|
87
|
-
|
|
87
|
+
- CDC log helpers: `readSqlCdcChanges`, `readSqlCdcChangedTables`,
|
|
88
|
+
`readSqlCdcFloor`, `sweepSqlCdcRetention`.
|
|
89
|
+
- Value codec building blocks (the core runs these on every engine):
|
|
88
90
|
`sqliteEncode`, `sqliteDecode`, `decodeBigint`, `tryJsonParse`,
|
|
89
91
|
`effectiveColumnKind`.
|
|
90
92
|
- Types: `SqlCtxDbOptions`, `SqlCtxExec`, `SqlDialect`, `SqlExec`,
|
package/dist/dialect.d.mts
CHANGED
|
@@ -5,20 +5,50 @@ interface SqlRunResult {
|
|
|
5
5
|
rowsAffected: number;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* The async SQL surface the store core consumes. Satisfied by a
|
|
9
|
-
* `D1Session`/`D1Client` (D1), a `node:sqlite` adapter (tests), or a
|
|
10
|
-
* Hyperdrive-backed `postgres`/`pg`/`mysql2` driver (PlanetScale).
|
|
11
|
-
*
|
|
12
|
-
* `all` runs a row-returning statement (incl. `... RETURNING ...`); `run` runs a
|
|
13
|
-
* write and reports `rowsAffected` — the matched-rows count that drives the
|
|
14
|
-
* MySQL optimistic-concurrency guard (which has no `RETURNING`).
|
|
15
|
-
*
|
|
16
|
-
* Note: companion writes (aggregate/rank/FTS/CDC) run as separate sequential
|
|
17
|
-
* statements after the row write on every engine, so they share D1's
|
|
18
|
-
* at-least-once caveat — there is no cross-statement transaction here.
|
|
19
|
-
*/
|
|
8
|
+
* The async SQL surface the store core consumes. Satisfied by a
|
|
9
|
+
* `D1Session`/`D1Client` (D1), a `node:sqlite` adapter (tests), or a
|
|
10
|
+
* Hyperdrive-backed `postgres`/`pg`/`mysql2` driver (PlanetScale).
|
|
11
|
+
*
|
|
12
|
+
* `all` runs a row-returning statement (incl. `... RETURNING ...`); `run` runs a
|
|
13
|
+
* write and reports `rowsAffected` — the matched-rows count that drives the
|
|
14
|
+
* MySQL optimistic-concurrency guard (which has no `RETURNING`).
|
|
15
|
+
*
|
|
16
|
+
* Note: companion writes (aggregate/rank/FTS/CDC) run as separate sequential
|
|
17
|
+
* statements after the row write on every engine, so they share D1's
|
|
18
|
+
* at-least-once caveat — there is no cross-statement transaction here.
|
|
19
|
+
*/
|
|
20
20
|
interface SqlExec {
|
|
21
21
|
all: (sql: string, params: ReadonlyArray<unknown>) => Promise<Record<string, unknown>[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Optional: run several write statements as one round trip instead of
|
|
24
|
+
* `run()` called once per statement in sequence. Statements MUST be
|
|
25
|
+
* mutually independent — an implementation MAY reorder or parallelize
|
|
26
|
+
* across elements, so callers must never rely on array order between
|
|
27
|
+
* elements (e.g. a purge-then-insert pair belongs in separate sequential
|
|
28
|
+
* `queryRun`/`run` calls, not one `batch`).
|
|
29
|
+
*
|
|
30
|
+
* Absent, the store core falls back to its historical sequential `run()`
|
|
31
|
+
* loop, so an exec that doesn't implement this keeps working unchanged.
|
|
32
|
+
*
|
|
33
|
+
* D1's `client.batch` executes the whole array atomically (all-or-nothing)
|
|
34
|
+
* in one request, preserving array order — an actual atomicity improvement
|
|
35
|
+
* over the sequential fallback, not just a round-trip one. The Hyperdrive
|
|
36
|
+
* `postgres`/`pg` adapters instead dispatch every statement concurrently
|
|
37
|
+
* (`Promise.all`) over the same connection/pool rather than awaiting each
|
|
38
|
+
* in turn — still "at-least-once, non-atomic" like the fallback, but no
|
|
39
|
+
* longer serialized one full RTT at a time, and with no ordering between
|
|
40
|
+
* elements. Safe only for statements whose effects don't depend on each
|
|
41
|
+
* other (distinct-keyed rows), which is what every current caller batches.
|
|
42
|
+
*
|
|
43
|
+
* The one exception is the `sqlite` dialect, whose engines ship FTS5: there
|
|
44
|
+
* an implementation MUST run the array in order, as one transaction, as
|
|
45
|
+
* D1's does. The FTS5 search companion's writes are ordered statement
|
|
46
|
+
* lists that must not interleave with another writer's (`runInOrder`).
|
|
47
|
+
*/
|
|
48
|
+
batch?: (statements: ReadonlyArray<{
|
|
49
|
+
params: ReadonlyArray<unknown>;
|
|
50
|
+
sql: string;
|
|
51
|
+
}>) => Promise<void>;
|
|
22
52
|
run: (sql: string, params: ReadonlyArray<unknown>) => Promise<SqlRunResult>;
|
|
23
53
|
}
|
|
24
54
|
/** Everything engine-specific the store core needs. One value per engine; `sqliteDialect` (in `@lunora/d1`) is the reference, Postgres/MySQL live in `@lunora/hyperdrive/global`. */
|
|
@@ -26,19 +56,38 @@ interface SqlDialect {
|
|
|
26
56
|
/** Affected-rows extractor for the OCC fallback when `supportsReturning` is false (MySQL). */
|
|
27
57
|
affectedRows?: (result: SqlRunResult) => number;
|
|
28
58
|
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
* Storage SQL column type for a validator `kind`. Every engine stores
|
|
60
|
+
* SQLite-shaped values (see `value-codec.ts`), so the types are the ones
|
|
61
|
+
* those forms fit, not the engine's richest equivalent:
|
|
62
|
+
*
|
|
63
|
+
* - SQLite affinity: `TEXT`/`INTEGER`/`REAL`/`BLOB`.
|
|
64
|
+
* - Postgres: `DOUBLE PRECISION` (number/date/timestamp), `BYTEA` (bytes),
|
|
65
|
+
* `INTEGER` (boolean, stored 1/0), `TEXT` for everything else — including
|
|
66
|
+
* the composites, which are JSON text rather than `JSONB`.
|
|
67
|
+
* - MySQL: `DOUBLE`, `LONGBLOB`, `TINYINT`, `VARCHAR(64)` (bigint as a
|
|
68
|
+
* decimal string), and `LONGTEXT` for everything else — strings unbounded
|
|
69
|
+
* so they never truncate, composites because their wire-marked form is not
|
|
70
|
+
* valid JSON and a `JSON` column would reject it on insert.
|
|
71
|
+
*
|
|
72
|
+
* `unique` says the column carries a `.unique()` constraint, so the type has
|
|
73
|
+
* to be one the engine can index in FULL. It exists for MySQL: InnoDB cannot
|
|
74
|
+
* index a `LONGTEXT` without a key prefix, and a prefixed UNIQUE index
|
|
75
|
+
* enforces uniqueness of the PREFIX — two distinct 200-character emails
|
|
76
|
+
* sharing their first 191 characters collided as a duplicate. A bounded
|
|
77
|
+
* `VARCHAR` indexes whole, so the constraint means what it says; a value
|
|
78
|
+
* past the bound is a loud write error rather than a wrong conflict. SQLite
|
|
79
|
+
* and Postgres index text of any length and ignore the flag.
|
|
80
|
+
*/
|
|
81
|
+
columnType: (kind: string | undefined, options?: {
|
|
82
|
+
unique?: boolean;
|
|
83
|
+
}) => string;
|
|
35
84
|
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
85
|
+
* Engine SQL types for the **internal companion tables** (aggregate / rank /
|
|
86
|
+
* CDC), which are built from raw SQL types, not validator kinds. SQLite uses
|
|
87
|
+
* `TEXT`/`REAL`/`INTEGER`/`BLOB` and `INTEGER PRIMARY KEY AUTOINCREMENT`;
|
|
88
|
+
* Postgres `TEXT`/`DOUBLE PRECISION`/`INTEGER`/`BYTEA` + `BIGSERIAL`; MySQL
|
|
89
|
+
* needs a bounded `VARCHAR` key, `DOUBLE`, `LONGBLOB`, `AUTO_INCREMENT`.
|
|
90
|
+
*/
|
|
42
91
|
companionTypes: {
|
|
43
92
|
autoincrementPrimaryKey: string;
|
|
44
93
|
integer: string;
|
|
@@ -46,37 +95,108 @@ interface SqlDialect {
|
|
|
46
95
|
real: string;
|
|
47
96
|
text: string;
|
|
48
97
|
};
|
|
49
|
-
/** Map a stored value back to its JS form, by effective validator `kind` (inverse of `encode`). */
|
|
50
|
-
decode: (value: unknown, kind: string | undefined) => unknown;
|
|
51
|
-
/** Map a JS value to its bound storage form (boolean→1/0, bigint→string, object→JSON on SQLite; mostly native on PG). */
|
|
52
|
-
encode: (value: unknown) => unknown;
|
|
53
98
|
/** The framework columns every global table carries — the `id` primary key and `_creationTime` — as `{ name, type }` so the DDL builder can quote each name through the engine's dialect. */
|
|
54
99
|
frameworkColumns: () => ReadonlyArray<{
|
|
55
100
|
name: string;
|
|
56
101
|
type: string;
|
|
57
102
|
}>;
|
|
58
103
|
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
104
|
+
* Optional: the key-prefix length an indexed column of this `kind` needs.
|
|
105
|
+
* MySQL/InnoDB can't index a `TEXT`/`LONGTEXT`/`BLOB` column without a prefix
|
|
106
|
+
* (the store appends `(<n>)` to the column reference); SQLite/Postgres index
|
|
107
|
+
* text columns directly and omit this hook (or return `undefined`). `kind` is
|
|
108
|
+
* the column's effective validator kind.
|
|
109
|
+
*/
|
|
65
110
|
indexKeyPrefix?: (kind: string | undefined) => number | undefined;
|
|
66
111
|
/** True when an `error` thrown by a write is a UNIQUE-constraint breach (mapped to a 409 ConflictError). */
|
|
67
112
|
isUniqueViolation: (error: unknown) => boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Most columns one table may carry on this engine, framework columns
|
|
115
|
+
* included. Omit it and the DDL builder does not check — the right answer
|
|
116
|
+
* for an engine whose ceiling is high enough that no real schema reaches it.
|
|
117
|
+
*
|
|
118
|
+
* Declared because the ceilings differ by more than an order of magnitude:
|
|
119
|
+
* D1 runs Workerd's SQLite build, which caps a table at 100 columns, where
|
|
120
|
+
* Postgres allows 1,600 and MySQL 4,096. A fixed number here would either
|
|
121
|
+
* miss the D1 failure or reject schemas the other two engines run happily.
|
|
122
|
+
*/
|
|
123
|
+
maxTableColumns?: number;
|
|
68
124
|
/** A short engine tag for diagnostics/branching (`"sqlite" | "postgres" | "mysql"`). The store core selects drizzle's matching dialect for rendering off this. */
|
|
69
125
|
readonly name: "mysql" | "postgres" | "sqlite";
|
|
126
|
+
/**
|
|
127
|
+
* Optional: the engine's own full-text index, opted into per search index
|
|
128
|
+
* with `.searchIndex({ strategy: "native" })`.
|
|
129
|
+
*
|
|
130
|
+
* Only Postgres supplies one today (`tsvector` + GIN + `to_tsquery`). It
|
|
131
|
+
* scales sublinearly where the portable inverted companion aggregates every
|
|
132
|
+
* matching token row, but it ranks with the engine's formula rather than the
|
|
133
|
+
* shared scorer — which is why it is opt-in and why the parity suite asserts
|
|
134
|
+
* matching, not order, for indexes that use it.
|
|
135
|
+
*
|
|
136
|
+
* Recall still matches the portable path: the stored form is built from the
|
|
137
|
+
* tokens Lunora's analyzer already produced, under a configuration that adds
|
|
138
|
+
* no stemming or stopwords of the engine's own.
|
|
139
|
+
*
|
|
140
|
+
* Every member returns a *statement*, not a fragment, so no engine grammar
|
|
141
|
+
* reaches the store core: Postgres matches with `@@` against a `tsvector`
|
|
142
|
+
* column while MySQL would use `MATCH … AGAINST` against a text column, and
|
|
143
|
+
* both fit here without the caller knowing which.
|
|
144
|
+
*/
|
|
145
|
+
nativeTextSearch?: {
|
|
146
|
+
/** DDL for the companion table holding the engine's indexed form, keyed by document id. */
|
|
147
|
+
createCompanion: (companion: string, keyType: string) => SQL;
|
|
148
|
+
/** DDL for the indexes that make the match fast. */
|
|
149
|
+
createIndexes: (companion: string) => SQL[];
|
|
150
|
+
/**
|
|
151
|
+
* Replace one document's row, given its already-analyzed token stream,
|
|
152
|
+
* only where `guard` holds. One idempotent statement: two writers of one
|
|
153
|
+
* id converge on one row, never raise a key conflict, and a writer whose
|
|
154
|
+
* `guard` fails writes nothing.
|
|
155
|
+
*/
|
|
156
|
+
indexDocument: (companion: string, id: string, analyzed: string, guard: SQL) => SQL;
|
|
157
|
+
/** The `WHERE` predicate matching a query's analyzed terms, final term as a prefix. */
|
|
158
|
+
matches: (companion: string, terms: ReadonlyArray<string>) => SQL;
|
|
159
|
+
/** The `ORDER BY` expression, best first. */
|
|
160
|
+
rank: (companion: string, terms: ReadonlyArray<string>) => SQL;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* How an operator completes a search index that is still backfilling on
|
|
164
|
+
* this backend, in words, for the refusal a read against it raises: each
|
|
165
|
+
* backend's entry point differs. Absent, the refusal says only to retry
|
|
166
|
+
* once the backfill finishes.
|
|
167
|
+
*/
|
|
168
|
+
searchBackfillHint?: string;
|
|
169
|
+
/**
|
|
170
|
+
* True when the engine ships SQLite's FTS5 module, which decides whether a
|
|
171
|
+
* search index is stored as an FTS5 shadow or as the portable inverted
|
|
172
|
+
* companion.
|
|
173
|
+
*
|
|
174
|
+
* A static property of the engine, so it is declared rather than probed:
|
|
175
|
+
* the previous `CREATE VIRTUAL TABLE` capability probe spent a round trip
|
|
176
|
+
* (and an error in the database's log) on every fresh connection to
|
|
177
|
+
* rediscover something the dialect already knows. Tests that want the
|
|
178
|
+
* portable layout override this instead of intercepting SQL strings.
|
|
179
|
+
*/
|
|
180
|
+
supportsFts5: boolean;
|
|
70
181
|
/** True when the engine supports `UPDATE/DELETE ... RETURNING` (SQLite/PG yes, MySQL no → use `affectedRows`). */
|
|
71
182
|
supportsReturning: boolean;
|
|
72
183
|
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
184
|
+
* The catalog probe for whether a physical `table` exists — backs the opt-in
|
|
185
|
+
* companion-table (`__agg_`/`__rank_`) existence checks. Returns a drizzle
|
|
186
|
+
* {@link SQL} (a non-empty result ⇒ the table exists) so it renders through
|
|
187
|
+
* the same per-engine path as every other statement, never a hand-built
|
|
188
|
+
* placeholder string. SQLite reads `sqlite_master`; Postgres/MySQL read
|
|
189
|
+
* `information_schema.tables`.
|
|
190
|
+
*/
|
|
80
191
|
tableExists: (table: string) => SQL;
|
|
192
|
+
/**
|
|
193
|
+
* Optional: btree operator class appended to an indexed text column so a
|
|
194
|
+
* `LIKE 'prefix%'` scan can use the index whatever the database collation.
|
|
195
|
+
* Postgres needs `text_pattern_ops` (a default `text_ops` btree built under
|
|
196
|
+
* e.g. `en_US.UTF-8` is useless to `LIKE`); SQLite and MySQL index prefix
|
|
197
|
+
* matches off the plain index and omit this. Only the search companion's
|
|
198
|
+
* token index reads it.
|
|
199
|
+
*/
|
|
200
|
+
textPatternOperatorClass?: string;
|
|
81
201
|
}
|
|
82
202
|
export { SqlDialect, SqlExec, SqlRunResult };
|
package/dist/dialect.d.ts
CHANGED
|
@@ -5,20 +5,50 @@ interface SqlRunResult {
|
|
|
5
5
|
rowsAffected: number;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* The async SQL surface the store core consumes. Satisfied by a
|
|
9
|
-
* `D1Session`/`D1Client` (D1), a `node:sqlite` adapter (tests), or a
|
|
10
|
-
* Hyperdrive-backed `postgres`/`pg`/`mysql2` driver (PlanetScale).
|
|
11
|
-
*
|
|
12
|
-
* `all` runs a row-returning statement (incl. `... RETURNING ...`); `run` runs a
|
|
13
|
-
* write and reports `rowsAffected` — the matched-rows count that drives the
|
|
14
|
-
* MySQL optimistic-concurrency guard (which has no `RETURNING`).
|
|
15
|
-
*
|
|
16
|
-
* Note: companion writes (aggregate/rank/FTS/CDC) run as separate sequential
|
|
17
|
-
* statements after the row write on every engine, so they share D1's
|
|
18
|
-
* at-least-once caveat — there is no cross-statement transaction here.
|
|
19
|
-
*/
|
|
8
|
+
* The async SQL surface the store core consumes. Satisfied by a
|
|
9
|
+
* `D1Session`/`D1Client` (D1), a `node:sqlite` adapter (tests), or a
|
|
10
|
+
* Hyperdrive-backed `postgres`/`pg`/`mysql2` driver (PlanetScale).
|
|
11
|
+
*
|
|
12
|
+
* `all` runs a row-returning statement (incl. `... RETURNING ...`); `run` runs a
|
|
13
|
+
* write and reports `rowsAffected` — the matched-rows count that drives the
|
|
14
|
+
* MySQL optimistic-concurrency guard (which has no `RETURNING`).
|
|
15
|
+
*
|
|
16
|
+
* Note: companion writes (aggregate/rank/FTS/CDC) run as separate sequential
|
|
17
|
+
* statements after the row write on every engine, so they share D1's
|
|
18
|
+
* at-least-once caveat — there is no cross-statement transaction here.
|
|
19
|
+
*/
|
|
20
20
|
interface SqlExec {
|
|
21
21
|
all: (sql: string, params: ReadonlyArray<unknown>) => Promise<Record<string, unknown>[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Optional: run several write statements as one round trip instead of
|
|
24
|
+
* `run()` called once per statement in sequence. Statements MUST be
|
|
25
|
+
* mutually independent — an implementation MAY reorder or parallelize
|
|
26
|
+
* across elements, so callers must never rely on array order between
|
|
27
|
+
* elements (e.g. a purge-then-insert pair belongs in separate sequential
|
|
28
|
+
* `queryRun`/`run` calls, not one `batch`).
|
|
29
|
+
*
|
|
30
|
+
* Absent, the store core falls back to its historical sequential `run()`
|
|
31
|
+
* loop, so an exec that doesn't implement this keeps working unchanged.
|
|
32
|
+
*
|
|
33
|
+
* D1's `client.batch` executes the whole array atomically (all-or-nothing)
|
|
34
|
+
* in one request, preserving array order — an actual atomicity improvement
|
|
35
|
+
* over the sequential fallback, not just a round-trip one. The Hyperdrive
|
|
36
|
+
* `postgres`/`pg` adapters instead dispatch every statement concurrently
|
|
37
|
+
* (`Promise.all`) over the same connection/pool rather than awaiting each
|
|
38
|
+
* in turn — still "at-least-once, non-atomic" like the fallback, but no
|
|
39
|
+
* longer serialized one full RTT at a time, and with no ordering between
|
|
40
|
+
* elements. Safe only for statements whose effects don't depend on each
|
|
41
|
+
* other (distinct-keyed rows), which is what every current caller batches.
|
|
42
|
+
*
|
|
43
|
+
* The one exception is the `sqlite` dialect, whose engines ship FTS5: there
|
|
44
|
+
* an implementation MUST run the array in order, as one transaction, as
|
|
45
|
+
* D1's does. The FTS5 search companion's writes are ordered statement
|
|
46
|
+
* lists that must not interleave with another writer's (`runInOrder`).
|
|
47
|
+
*/
|
|
48
|
+
batch?: (statements: ReadonlyArray<{
|
|
49
|
+
params: ReadonlyArray<unknown>;
|
|
50
|
+
sql: string;
|
|
51
|
+
}>) => Promise<void>;
|
|
22
52
|
run: (sql: string, params: ReadonlyArray<unknown>) => Promise<SqlRunResult>;
|
|
23
53
|
}
|
|
24
54
|
/** Everything engine-specific the store core needs. One value per engine; `sqliteDialect` (in `@lunora/d1`) is the reference, Postgres/MySQL live in `@lunora/hyperdrive/global`. */
|
|
@@ -26,19 +56,38 @@ interface SqlDialect {
|
|
|
26
56
|
/** Affected-rows extractor for the OCC fallback when `supportsReturning` is false (MySQL). */
|
|
27
57
|
affectedRows?: (result: SqlRunResult) => number;
|
|
28
58
|
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
* Storage SQL column type for a validator `kind`. Every engine stores
|
|
60
|
+
* SQLite-shaped values (see `value-codec.ts`), so the types are the ones
|
|
61
|
+
* those forms fit, not the engine's richest equivalent:
|
|
62
|
+
*
|
|
63
|
+
* - SQLite affinity: `TEXT`/`INTEGER`/`REAL`/`BLOB`.
|
|
64
|
+
* - Postgres: `DOUBLE PRECISION` (number/date/timestamp), `BYTEA` (bytes),
|
|
65
|
+
* `INTEGER` (boolean, stored 1/0), `TEXT` for everything else — including
|
|
66
|
+
* the composites, which are JSON text rather than `JSONB`.
|
|
67
|
+
* - MySQL: `DOUBLE`, `LONGBLOB`, `TINYINT`, `VARCHAR(64)` (bigint as a
|
|
68
|
+
* decimal string), and `LONGTEXT` for everything else — strings unbounded
|
|
69
|
+
* so they never truncate, composites because their wire-marked form is not
|
|
70
|
+
* valid JSON and a `JSON` column would reject it on insert.
|
|
71
|
+
*
|
|
72
|
+
* `unique` says the column carries a `.unique()` constraint, so the type has
|
|
73
|
+
* to be one the engine can index in FULL. It exists for MySQL: InnoDB cannot
|
|
74
|
+
* index a `LONGTEXT` without a key prefix, and a prefixed UNIQUE index
|
|
75
|
+
* enforces uniqueness of the PREFIX — two distinct 200-character emails
|
|
76
|
+
* sharing their first 191 characters collided as a duplicate. A bounded
|
|
77
|
+
* `VARCHAR` indexes whole, so the constraint means what it says; a value
|
|
78
|
+
* past the bound is a loud write error rather than a wrong conflict. SQLite
|
|
79
|
+
* and Postgres index text of any length and ignore the flag.
|
|
80
|
+
*/
|
|
81
|
+
columnType: (kind: string | undefined, options?: {
|
|
82
|
+
unique?: boolean;
|
|
83
|
+
}) => string;
|
|
35
84
|
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
85
|
+
* Engine SQL types for the **internal companion tables** (aggregate / rank /
|
|
86
|
+
* CDC), which are built from raw SQL types, not validator kinds. SQLite uses
|
|
87
|
+
* `TEXT`/`REAL`/`INTEGER`/`BLOB` and `INTEGER PRIMARY KEY AUTOINCREMENT`;
|
|
88
|
+
* Postgres `TEXT`/`DOUBLE PRECISION`/`INTEGER`/`BYTEA` + `BIGSERIAL`; MySQL
|
|
89
|
+
* needs a bounded `VARCHAR` key, `DOUBLE`, `LONGBLOB`, `AUTO_INCREMENT`.
|
|
90
|
+
*/
|
|
42
91
|
companionTypes: {
|
|
43
92
|
autoincrementPrimaryKey: string;
|
|
44
93
|
integer: string;
|
|
@@ -46,37 +95,108 @@ interface SqlDialect {
|
|
|
46
95
|
real: string;
|
|
47
96
|
text: string;
|
|
48
97
|
};
|
|
49
|
-
/** Map a stored value back to its JS form, by effective validator `kind` (inverse of `encode`). */
|
|
50
|
-
decode: (value: unknown, kind: string | undefined) => unknown;
|
|
51
|
-
/** Map a JS value to its bound storage form (boolean→1/0, bigint→string, object→JSON on SQLite; mostly native on PG). */
|
|
52
|
-
encode: (value: unknown) => unknown;
|
|
53
98
|
/** The framework columns every global table carries — the `id` primary key and `_creationTime` — as `{ name, type }` so the DDL builder can quote each name through the engine's dialect. */
|
|
54
99
|
frameworkColumns: () => ReadonlyArray<{
|
|
55
100
|
name: string;
|
|
56
101
|
type: string;
|
|
57
102
|
}>;
|
|
58
103
|
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
104
|
+
* Optional: the key-prefix length an indexed column of this `kind` needs.
|
|
105
|
+
* MySQL/InnoDB can't index a `TEXT`/`LONGTEXT`/`BLOB` column without a prefix
|
|
106
|
+
* (the store appends `(<n>)` to the column reference); SQLite/Postgres index
|
|
107
|
+
* text columns directly and omit this hook (or return `undefined`). `kind` is
|
|
108
|
+
* the column's effective validator kind.
|
|
109
|
+
*/
|
|
65
110
|
indexKeyPrefix?: (kind: string | undefined) => number | undefined;
|
|
66
111
|
/** True when an `error` thrown by a write is a UNIQUE-constraint breach (mapped to a 409 ConflictError). */
|
|
67
112
|
isUniqueViolation: (error: unknown) => boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Most columns one table may carry on this engine, framework columns
|
|
115
|
+
* included. Omit it and the DDL builder does not check — the right answer
|
|
116
|
+
* for an engine whose ceiling is high enough that no real schema reaches it.
|
|
117
|
+
*
|
|
118
|
+
* Declared because the ceilings differ by more than an order of magnitude:
|
|
119
|
+
* D1 runs Workerd's SQLite build, which caps a table at 100 columns, where
|
|
120
|
+
* Postgres allows 1,600 and MySQL 4,096. A fixed number here would either
|
|
121
|
+
* miss the D1 failure or reject schemas the other two engines run happily.
|
|
122
|
+
*/
|
|
123
|
+
maxTableColumns?: number;
|
|
68
124
|
/** A short engine tag for diagnostics/branching (`"sqlite" | "postgres" | "mysql"`). The store core selects drizzle's matching dialect for rendering off this. */
|
|
69
125
|
readonly name: "mysql" | "postgres" | "sqlite";
|
|
126
|
+
/**
|
|
127
|
+
* Optional: the engine's own full-text index, opted into per search index
|
|
128
|
+
* with `.searchIndex({ strategy: "native" })`.
|
|
129
|
+
*
|
|
130
|
+
* Only Postgres supplies one today (`tsvector` + GIN + `to_tsquery`). It
|
|
131
|
+
* scales sublinearly where the portable inverted companion aggregates every
|
|
132
|
+
* matching token row, but it ranks with the engine's formula rather than the
|
|
133
|
+
* shared scorer — which is why it is opt-in and why the parity suite asserts
|
|
134
|
+
* matching, not order, for indexes that use it.
|
|
135
|
+
*
|
|
136
|
+
* Recall still matches the portable path: the stored form is built from the
|
|
137
|
+
* tokens Lunora's analyzer already produced, under a configuration that adds
|
|
138
|
+
* no stemming or stopwords of the engine's own.
|
|
139
|
+
*
|
|
140
|
+
* Every member returns a *statement*, not a fragment, so no engine grammar
|
|
141
|
+
* reaches the store core: Postgres matches with `@@` against a `tsvector`
|
|
142
|
+
* column while MySQL would use `MATCH … AGAINST` against a text column, and
|
|
143
|
+
* both fit here without the caller knowing which.
|
|
144
|
+
*/
|
|
145
|
+
nativeTextSearch?: {
|
|
146
|
+
/** DDL for the companion table holding the engine's indexed form, keyed by document id. */
|
|
147
|
+
createCompanion: (companion: string, keyType: string) => SQL;
|
|
148
|
+
/** DDL for the indexes that make the match fast. */
|
|
149
|
+
createIndexes: (companion: string) => SQL[];
|
|
150
|
+
/**
|
|
151
|
+
* Replace one document's row, given its already-analyzed token stream,
|
|
152
|
+
* only where `guard` holds. One idempotent statement: two writers of one
|
|
153
|
+
* id converge on one row, never raise a key conflict, and a writer whose
|
|
154
|
+
* `guard` fails writes nothing.
|
|
155
|
+
*/
|
|
156
|
+
indexDocument: (companion: string, id: string, analyzed: string, guard: SQL) => SQL;
|
|
157
|
+
/** The `WHERE` predicate matching a query's analyzed terms, final term as a prefix. */
|
|
158
|
+
matches: (companion: string, terms: ReadonlyArray<string>) => SQL;
|
|
159
|
+
/** The `ORDER BY` expression, best first. */
|
|
160
|
+
rank: (companion: string, terms: ReadonlyArray<string>) => SQL;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* How an operator completes a search index that is still backfilling on
|
|
164
|
+
* this backend, in words, for the refusal a read against it raises: each
|
|
165
|
+
* backend's entry point differs. Absent, the refusal says only to retry
|
|
166
|
+
* once the backfill finishes.
|
|
167
|
+
*/
|
|
168
|
+
searchBackfillHint?: string;
|
|
169
|
+
/**
|
|
170
|
+
* True when the engine ships SQLite's FTS5 module, which decides whether a
|
|
171
|
+
* search index is stored as an FTS5 shadow or as the portable inverted
|
|
172
|
+
* companion.
|
|
173
|
+
*
|
|
174
|
+
* A static property of the engine, so it is declared rather than probed:
|
|
175
|
+
* the previous `CREATE VIRTUAL TABLE` capability probe spent a round trip
|
|
176
|
+
* (and an error in the database's log) on every fresh connection to
|
|
177
|
+
* rediscover something the dialect already knows. Tests that want the
|
|
178
|
+
* portable layout override this instead of intercepting SQL strings.
|
|
179
|
+
*/
|
|
180
|
+
supportsFts5: boolean;
|
|
70
181
|
/** True when the engine supports `UPDATE/DELETE ... RETURNING` (SQLite/PG yes, MySQL no → use `affectedRows`). */
|
|
71
182
|
supportsReturning: boolean;
|
|
72
183
|
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
184
|
+
* The catalog probe for whether a physical `table` exists — backs the opt-in
|
|
185
|
+
* companion-table (`__agg_`/`__rank_`) existence checks. Returns a drizzle
|
|
186
|
+
* {@link SQL} (a non-empty result ⇒ the table exists) so it renders through
|
|
187
|
+
* the same per-engine path as every other statement, never a hand-built
|
|
188
|
+
* placeholder string. SQLite reads `sqlite_master`; Postgres/MySQL read
|
|
189
|
+
* `information_schema.tables`.
|
|
190
|
+
*/
|
|
80
191
|
tableExists: (table: string) => SQL;
|
|
192
|
+
/**
|
|
193
|
+
* Optional: btree operator class appended to an indexed text column so a
|
|
194
|
+
* `LIKE 'prefix%'` scan can use the index whatever the database collation.
|
|
195
|
+
* Postgres needs `text_pattern_ops` (a default `text_ops` btree built under
|
|
196
|
+
* e.g. `en_US.UTF-8` is useless to `LIKE`); SQLite and MySQL index prefix
|
|
197
|
+
* matches off the plain index and omit this. Only the search companion's
|
|
198
|
+
* token index reads it.
|
|
199
|
+
*/
|
|
200
|
+
textPatternOperatorClass?: string;
|
|
81
201
|
}
|
|
82
202
|
export { SqlDialect, SqlExec, SqlRunResult };
|
package/dist/dialect.mjs
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|