@lunora/hyperdrive 1.0.0-alpha.211 → 1.0.0-alpha.213

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/README.md CHANGED
@@ -101,7 +101,7 @@ import { defineApp } from "../../lunora/_generated/app";
101
101
 
102
102
  const app = defineApp<Env>()
103
103
  .shard((env) => env.SHARD)
104
- .hyperdrive((env) => fromPostgresJs(postgres(createHyperdrive(env.HYPERDRIVE as HyperdriveLike).connectionString)))
104
+ .hyperdrive((env) => fromPostgresJs(postgres(createHyperdrive(env.HYPERDRIVE as HyperdriveLike).connectionString, { fetch_types: false, max: 5 })))
105
105
  .build();
106
106
 
107
107
  export const ShardDO = app.ShardDO;
@@ -157,7 +157,7 @@ Supply the driver once, on the app builder — required, or every tick records `
157
157
  ```ts
158
158
  export default defineApp<Env>()
159
159
  .shard((env) => env.SHARD)
160
- .sourceClient((env, binding) => fromPostgresJs(postgres((env[binding] as { connectionString: string }).connectionString)))
160
+ .sourceClient((env, binding) => fromPostgresJs(postgres((env[binding] as { connectionString: string }).connectionString, { fetch_types: false, max: 5 })))
161
161
  .build();
162
162
  ```
163
163
 
@@ -176,14 +176,17 @@ Three things to know before you build on it:
176
176
  The `@lunora/hyperdrive/global` subpath is the opposite trade-off: Lunora **owns** the schema and a `.global()` table gets a real column-per-field layout on your Postgres/MySQL, with every write routed through the shared store core. Live queries stay reactive — identical to D1 — because the writer drives the same broadcast hook. Build the writer inside the Durable Object that hosts the `.global()` store and inject it as `globalDb`:
177
177
 
178
178
  ```ts
179
+ import { fromPostgresJs } from "@lunora/hyperdrive";
179
180
  import { createPostgresGlobalCtxDb } from "@lunora/hyperdrive/global";
180
181
  import postgres from "postgres";
181
182
 
182
- // `sql` is a postgres.js client built from the HYPERDRIVE binding's connection string.
183
- const globalDb = createPostgresGlobalCtxDb({ query: (text, params) => sql.unsafe(text, params) }, { schema });
183
+ const sql = postgres(env.HYPERDRIVE.connectionString, { fetch_types: false, max: 5 });
184
+ const globalDb = createPostgresGlobalCtxDb(fromPostgresJs(sql), { schema });
184
185
  ```
185
186
 
186
- For MySQL use `createMysqlGlobalCtxDb` with a `mysql2/promise` pool created with `flags: ["FOUND_ROWS"]` — without `CLIENT_FOUND_ROWS` the affected-rows OCC guard sees changed (not matched) rows and raises spurious conflicts. Lower-level building blocks (`buildPgExec`, `buildMysqlExec`, `postgresDialect`, `mysqlDialect`, `createHyperdriveGlobalCtxDb`) are exported for custom wiring.
187
+ For MySQL use `createMysqlGlobalCtxDb` with a `mysql2/promise` pool created with `disableEval: true` (required on Workers) and `flags: ["FOUND_ROWS"]` — without `CLIENT_FOUND_ROWS` the affected-rows OCC guard sees changed (not matched) rows and raises spurious conflicts. Lower-level building blocks (`buildPgExec`, `buildMysqlExec`, `postgresDialect`, `mysqlDialect`, `createHyperdriveGlobalCtxDb`) are exported for custom wiring.
188
+
189
+ The Hyperdrive config behind `.global()` must have **query caching disabled** (`wrangler hyperdrive create … --caching-disabled`, or `wrangler hyperdrive update <id> --caching-disabled`). Hyperdrive caches reads for up to 75 s by default and writes don't invalidate them, so the store's OCC re-reads, change-log polls and live-query re-runs would see stale rows. `lunora deploy` sets this on the configs it creates when the schema has `.global({ backend: "hyperdrive" })` tables, and warns about an already-bound config that still caches (when Cloudflare API credentials are set).
187
190
 
188
191
  > This README covers the basics. For the full API and the determinism/realtime rationale, see the **[documentation](https://lunora.sh/docs/packages/hyperdrive)**.
189
192
 
package/dist/global.d.mts CHANGED
@@ -21,10 +21,16 @@
21
21
  * **MySQL connections must enable `CLIENT_FOUND_ROWS`** (mysql2:
22
22
  * `createPool({ flags: ["FOUND_ROWS"] })`) so the affected-rows OCC guard sees
23
23
  * matched (not changed) rows — see {@link buildMysqlExec}.
24
+ *
25
+ * **The Hyperdrive config must have query caching disabled**
26
+ * (`wrangler hyperdrive create … --caching-disabled`). Hyperdrive caches reads by
27
+ * default and a write does not invalidate them, so OCC re-reads, change-log polls
28
+ * and live-query re-runs would read rows up to `max_age + stale_while_revalidate`
29
+ * old.
24
30
  */
25
- import { DatabaseWriterLike } from '@lunora/shard-engine';
26
- import { SqlExec, SqlDialect, SqlCtxDbOptions } from '@lunora/sql-store';
27
- import { M as Mysql2Like, S as SqlClient } from "./packem_shared/types.d-B2JdXSms.mjs";
31
+ import { SchemaLike, ImportConflictMode, CdcChange, DatabaseWriterLike } from '@lunora/shard-engine';
32
+ import { SqlExec, SqlDialect, SqlCtxDbOptions, FacetGlobalColumnOptions, GlobalFacetResult, GlobalTableInfo, ReadGlobalTablePageOptions, GlobalTablePage, ExportRow, ImportResult } from '@lunora/sql-store';
33
+ import { M as Mysql2Like, S as SqlClient } from "./packem_shared/types.d-Zm06MRhL.mjs";
28
34
  import { VectorMetric, VectorizeIndexLike } from '@lunora/platform';
29
35
  /**
30
36
  * Minimal row-returning client (e.g. `@lunora/hyperdrive`'s `fromPostgresJs`/`fromNodePg`
@@ -33,7 +39,7 @@ import { VectorMetric, VectorizeIndexLike } from '@lunora/platform';
33
39
  */
34
40
  type RowClient = SqlClient;
35
41
  /**
36
- * Minimal `mysql2/promise` connection/pool surface — `execute` resolves to
42
+ * Minimal `mysql2/promise` connection/pool surface — `query` resolves to
37
43
  * `[rows | ResultSetHeader, fields]`. Aliases {@link Mysql2Like} so the /global
38
44
  * entry's driver surface can never drift from the main entry's, widened with the
39
45
  * (optional, structural) shape {@link assertFoundRows}'s `CLIENT_FOUND_ROWS`
@@ -60,9 +66,9 @@ type RowClient = SqlClient;
60
66
  * typings, so spelling them out made a real `createPool(...)` fail to type-check
61
67
  * (TS2345, weak-type mismatch on `config`). {@link readMysqlClientFlags} reads
62
68
  * them through the internal `FlagProbeShape` instead, and a minimal test double
63
- * (only `execute`) resolves to "undeterminable" rather than a `TypeError`.
69
+ * (only `query`) resolves to "undeterminable" rather than a `TypeError`.
64
70
  */
65
- type Mysql2Execute = Mysql2Like & {
71
+ type Mysql2Client = Mysql2Like & {
66
72
  config?: unknown;
67
73
  pool?: unknown;
68
74
  };
@@ -92,6 +98,9 @@ declare const buildPgExec: (client: RowClient) => SqlExec;
92
98
  * forward verbatim. MySQL has no `RETURNING`, so `run` surfaces `affectedRows`
93
99
  * for the store's affected-rows OCC guard.
94
100
  *
101
+ * Statements go over as text with client-side escaping, so the server must not
102
+ * run with `NO_BACKSLASH_ESCAPES` (see {@link Mysql2Like}).
103
+ *
95
104
  * **The connection MUST be created with the `CLIENT_FOUND_ROWS` flag**
96
105
  * (mysql2: `createPool({ flags: ["FOUND_ROWS"] })`). Without it, `affectedRows`
97
106
  * counts *changed* rows, so an idempotent `patch`/`replace` that re-writes the
@@ -100,12 +109,12 @@ declare const buildPgExec: (client: RowClient) => SqlExec;
100
109
  *
101
110
  * `batch` dispatches every statement concurrently (`Promise.all`), same
102
111
  * rationale as {@link buildPgExec}'s `batch` — `Mysql2Like` only exposes a
103
- * single-statement `execute`, so this is "spread across a pool's connections"
112
+ * single-statement `query`, so this is "spread across a pool's connections"
104
113
  * rather than one wire-level multi-statement command; still non-atomic,
105
114
  * at-least-once, and unordered between elements, safe only for statements
106
115
  * with no cross-effect (what every current caller batches).
107
116
  */
108
- declare const buildMysqlExec: (connection: Mysql2Execute) => SqlExec;
117
+ declare const buildMysqlExec: (connection: Mysql2Client) => SqlExec;
109
118
  /**
110
119
  * **Postgres** dialect. Differs from SQLite only in column types
111
120
  * (`DOUBLE PRECISION`/`BYTEA`/`BIGSERIAL`), the `information_schema` catalog
@@ -171,8 +180,77 @@ interface CreateHyperdriveGlobalCtxDbOptions extends Omit<SqlCtxDbOptions, "dial
171
180
  * D1 store options.
172
181
  */
173
182
  declare const createHyperdriveGlobalCtxDb: ({ engine, exec, ...rest }: CreateHyperdriveGlobalCtxDbOptions) => DatabaseWriterLike;
183
+ /** Options for {@link exportHyperdriveGlobalRows}. */
184
+ interface ExportHyperdriveGlobalRowsOptions {
185
+ engine: HyperdriveEngine;
186
+ exec: SqlExec;
187
+ schema: SchemaLike;
188
+ /** Table allowlist; empty or absent exports every `.global()` table. */
189
+ tables?: ReadonlyArray<string>;
190
+ }
191
+ /**
192
+ * The admin export's global plane on Hyperdrive: yield every requested
193
+ * `.global()` table's rows, keyset-paged. Provisions the schema's tables first,
194
+ * so a table that was never written exports as empty instead of failing.
195
+ */
196
+ declare const exportHyperdriveGlobalRows: ({ engine, exec, schema, tables }: ExportHyperdriveGlobalRowsOptions) => AsyncGenerator<ExportRow, void, undefined>;
197
+ /** Options for {@link importHyperdriveGlobalRows}. */
198
+ interface ImportHyperdriveGlobalRowsOptions {
199
+ /** Append each insert to the global CDC log, like any other write — the app's own `cdc` setting. */
200
+ cdc: boolean;
201
+ /** Validate and report shape errors only; nothing is probed or written. */
202
+ dryRun?: boolean;
203
+ engine: HyperdriveEngine;
204
+ exec: SqlExec;
205
+ /** `"replace"` overwrites a row whose `_id` already exists; `"skip"` (the default) counts it as a conflict. */
206
+ onConflict?: ImportConflictMode;
207
+ rows: ReadonlyArray<ExportRow>;
208
+ schema: SchemaLike;
209
+ startLine?: number;
210
+ }
211
+ /**
212
+ * The admin import's global plane on Hyperdrive: insert snapshot rows through
213
+ * the same store core `.global()` writes use. A row whose `_id` already exists
214
+ * is a conflict left unchanged, unless `onConflict` is `"replace"`.
215
+ */
216
+ declare const importHyperdriveGlobalRows: ({ cdc, dryRun, engine, exec, onConflict, rows, schema, startLine }: ImportHyperdriveGlobalRowsOptions) => Promise<ImportResult>;
174
217
  /** Convenience: a **Postgres** `.global()` writer from a row-client (postgres.js/pg over Hyperdrive). */
175
218
  declare const createPostgresGlobalCtxDb: (client: RowClient, options: Omit<CreateHyperdriveGlobalCtxDbOptions, "engine" | "exec">) => DatabaseWriterLike;
176
219
  /** Convenience: a **MySQL** `.global()` writer from a `mysql2/promise` connection/pool (created with `flags: ["FOUND_ROWS"]`). */
177
- declare const createMysqlGlobalCtxDb: (connection: Mysql2Execute, options: Omit<CreateHyperdriveGlobalCtxDbOptions, "engine" | "exec">) => DatabaseWriterLike;
178
- export { CreateHyperdriveGlobalCtxDbOptions, HyperdriveEngine, type Mysql2Execute, type PgVectorIndexOptions, type RowClient, buildMysqlExec, buildPgExec, createHyperdriveGlobalCtxDb, createMysqlGlobalCtxDb, createPgVectorIndex, createPostgresGlobalCtxDb, mysqlDialect, postgresDialect };
220
+ declare const createMysqlGlobalCtxDb: (connection: Mysql2Client, options: Omit<CreateHyperdriveGlobalCtxDbOptions, "engine" | "exec">) => DatabaseWriterLike;
221
+ /** The studio's global data browser over a Hyperdrive database — every table in the connection's schema, internal ones hidden. */
222
+ interface HyperdriveGlobalIntrospector {
223
+ facetColumn: (options: FacetGlobalColumnOptions) => Promise<GlobalFacetResult>;
224
+ listTables: () => Promise<GlobalTableInfo[]>;
225
+ readTablePage: (options: ReadGlobalTablePageOptions) => Promise<GlobalTablePage>;
226
+ }
227
+ /** Build the studio's global-browser introspector for a Hyperdrive `.global()` store. */
228
+ declare const createHyperdriveGlobalIntrospector: (engine: HyperdriveEngine, exec: SqlExec, schema: SchemaLike) => HyperdriveGlobalIntrospector;
229
+ /**
230
+ * The admin CDC sync's global plane on Hyperdrive: a page of the global
231
+ * `__cdc_log` past `sinceSeq`. The log only exists once a CDC-enabled writer has
232
+ * run, so its absence is "nothing has changed yet" — an empty page that leaves
233
+ * the cursor where it was — not an error.
234
+ */
235
+ declare const readHyperdriveGlobalCdc: (engine: HyperdriveEngine, exec: SqlExec, request: {
236
+ limit?: number;
237
+ now?: number;
238
+ sinceSeq: number;
239
+ }) => Promise<{
240
+ changes: CdcChange[];
241
+ cursor: number;
242
+ }>;
243
+ /**
244
+ * The admin point-in-time apply's global plane on Hyperdrive: replay CDC changes
245
+ * in order through the store core (insert, falling back to replace). The writer
246
+ * carries the app's `cdc` setting so the replay appends to the global changelog
247
+ * like any other write. Returns the number replayed.
248
+ */
249
+ declare const applyHyperdriveGlobalCdc: (options: {
250
+ cdc: boolean;
251
+ changes: ReadonlyArray<CdcChange>;
252
+ engine: HyperdriveEngine;
253
+ exec: SqlExec;
254
+ schema: SchemaLike;
255
+ }) => Promise<number>;
256
+ export { CreateHyperdriveGlobalCtxDbOptions, ExportHyperdriveGlobalRowsOptions, HyperdriveEngine, HyperdriveGlobalIntrospector, ImportHyperdriveGlobalRowsOptions, type Mysql2Client, type PgVectorIndexOptions, type RowClient, applyHyperdriveGlobalCdc, buildMysqlExec, buildPgExec, createHyperdriveGlobalCtxDb, createHyperdriveGlobalIntrospector, createMysqlGlobalCtxDb, createPgVectorIndex, createPostgresGlobalCtxDb, exportHyperdriveGlobalRows, importHyperdriveGlobalRows, mysqlDialect, postgresDialect, readHyperdriveGlobalCdc };
package/dist/global.d.ts CHANGED
@@ -21,10 +21,16 @@
21
21
  * **MySQL connections must enable `CLIENT_FOUND_ROWS`** (mysql2:
22
22
  * `createPool({ flags: ["FOUND_ROWS"] })`) so the affected-rows OCC guard sees
23
23
  * matched (not changed) rows — see {@link buildMysqlExec}.
24
+ *
25
+ * **The Hyperdrive config must have query caching disabled**
26
+ * (`wrangler hyperdrive create … --caching-disabled`). Hyperdrive caches reads by
27
+ * default and a write does not invalidate them, so OCC re-reads, change-log polls
28
+ * and live-query re-runs would read rows up to `max_age + stale_while_revalidate`
29
+ * old.
24
30
  */
25
- import { DatabaseWriterLike } from '@lunora/shard-engine';
26
- import { SqlExec, SqlDialect, SqlCtxDbOptions } from '@lunora/sql-store';
27
- import { M as Mysql2Like, S as SqlClient } from "./packem_shared/types.d-B2JdXSms.js";
31
+ import { SchemaLike, ImportConflictMode, CdcChange, DatabaseWriterLike } from '@lunora/shard-engine';
32
+ import { SqlExec, SqlDialect, SqlCtxDbOptions, FacetGlobalColumnOptions, GlobalFacetResult, GlobalTableInfo, ReadGlobalTablePageOptions, GlobalTablePage, ExportRow, ImportResult } from '@lunora/sql-store';
33
+ import { M as Mysql2Like, S as SqlClient } from "./packem_shared/types.d-Zm06MRhL.js";
28
34
  import { VectorMetric, VectorizeIndexLike } from '@lunora/platform';
29
35
  /**
30
36
  * Minimal row-returning client (e.g. `@lunora/hyperdrive`'s `fromPostgresJs`/`fromNodePg`
@@ -33,7 +39,7 @@ import { VectorMetric, VectorizeIndexLike } from '@lunora/platform';
33
39
  */
34
40
  type RowClient = SqlClient;
35
41
  /**
36
- * Minimal `mysql2/promise` connection/pool surface — `execute` resolves to
42
+ * Minimal `mysql2/promise` connection/pool surface — `query` resolves to
37
43
  * `[rows | ResultSetHeader, fields]`. Aliases {@link Mysql2Like} so the /global
38
44
  * entry's driver surface can never drift from the main entry's, widened with the
39
45
  * (optional, structural) shape {@link assertFoundRows}'s `CLIENT_FOUND_ROWS`
@@ -60,9 +66,9 @@ type RowClient = SqlClient;
60
66
  * typings, so spelling them out made a real `createPool(...)` fail to type-check
61
67
  * (TS2345, weak-type mismatch on `config`). {@link readMysqlClientFlags} reads
62
68
  * them through the internal `FlagProbeShape` instead, and a minimal test double
63
- * (only `execute`) resolves to "undeterminable" rather than a `TypeError`.
69
+ * (only `query`) resolves to "undeterminable" rather than a `TypeError`.
64
70
  */
65
- type Mysql2Execute = Mysql2Like & {
71
+ type Mysql2Client = Mysql2Like & {
66
72
  config?: unknown;
67
73
  pool?: unknown;
68
74
  };
@@ -92,6 +98,9 @@ declare const buildPgExec: (client: RowClient) => SqlExec;
92
98
  * forward verbatim. MySQL has no `RETURNING`, so `run` surfaces `affectedRows`
93
99
  * for the store's affected-rows OCC guard.
94
100
  *
101
+ * Statements go over as text with client-side escaping, so the server must not
102
+ * run with `NO_BACKSLASH_ESCAPES` (see {@link Mysql2Like}).
103
+ *
95
104
  * **The connection MUST be created with the `CLIENT_FOUND_ROWS` flag**
96
105
  * (mysql2: `createPool({ flags: ["FOUND_ROWS"] })`). Without it, `affectedRows`
97
106
  * counts *changed* rows, so an idempotent `patch`/`replace` that re-writes the
@@ -100,12 +109,12 @@ declare const buildPgExec: (client: RowClient) => SqlExec;
100
109
  *
101
110
  * `batch` dispatches every statement concurrently (`Promise.all`), same
102
111
  * rationale as {@link buildPgExec}'s `batch` — `Mysql2Like` only exposes a
103
- * single-statement `execute`, so this is "spread across a pool's connections"
112
+ * single-statement `query`, so this is "spread across a pool's connections"
104
113
  * rather than one wire-level multi-statement command; still non-atomic,
105
114
  * at-least-once, and unordered between elements, safe only for statements
106
115
  * with no cross-effect (what every current caller batches).
107
116
  */
108
- declare const buildMysqlExec: (connection: Mysql2Execute) => SqlExec;
117
+ declare const buildMysqlExec: (connection: Mysql2Client) => SqlExec;
109
118
  /**
110
119
  * **Postgres** dialect. Differs from SQLite only in column types
111
120
  * (`DOUBLE PRECISION`/`BYTEA`/`BIGSERIAL`), the `information_schema` catalog
@@ -171,8 +180,77 @@ interface CreateHyperdriveGlobalCtxDbOptions extends Omit<SqlCtxDbOptions, "dial
171
180
  * D1 store options.
172
181
  */
173
182
  declare const createHyperdriveGlobalCtxDb: ({ engine, exec, ...rest }: CreateHyperdriveGlobalCtxDbOptions) => DatabaseWriterLike;
183
+ /** Options for {@link exportHyperdriveGlobalRows}. */
184
+ interface ExportHyperdriveGlobalRowsOptions {
185
+ engine: HyperdriveEngine;
186
+ exec: SqlExec;
187
+ schema: SchemaLike;
188
+ /** Table allowlist; empty or absent exports every `.global()` table. */
189
+ tables?: ReadonlyArray<string>;
190
+ }
191
+ /**
192
+ * The admin export's global plane on Hyperdrive: yield every requested
193
+ * `.global()` table's rows, keyset-paged. Provisions the schema's tables first,
194
+ * so a table that was never written exports as empty instead of failing.
195
+ */
196
+ declare const exportHyperdriveGlobalRows: ({ engine, exec, schema, tables }: ExportHyperdriveGlobalRowsOptions) => AsyncGenerator<ExportRow, void, undefined>;
197
+ /** Options for {@link importHyperdriveGlobalRows}. */
198
+ interface ImportHyperdriveGlobalRowsOptions {
199
+ /** Append each insert to the global CDC log, like any other write — the app's own `cdc` setting. */
200
+ cdc: boolean;
201
+ /** Validate and report shape errors only; nothing is probed or written. */
202
+ dryRun?: boolean;
203
+ engine: HyperdriveEngine;
204
+ exec: SqlExec;
205
+ /** `"replace"` overwrites a row whose `_id` already exists; `"skip"` (the default) counts it as a conflict. */
206
+ onConflict?: ImportConflictMode;
207
+ rows: ReadonlyArray<ExportRow>;
208
+ schema: SchemaLike;
209
+ startLine?: number;
210
+ }
211
+ /**
212
+ * The admin import's global plane on Hyperdrive: insert snapshot rows through
213
+ * the same store core `.global()` writes use. A row whose `_id` already exists
214
+ * is a conflict left unchanged, unless `onConflict` is `"replace"`.
215
+ */
216
+ declare const importHyperdriveGlobalRows: ({ cdc, dryRun, engine, exec, onConflict, rows, schema, startLine }: ImportHyperdriveGlobalRowsOptions) => Promise<ImportResult>;
174
217
  /** Convenience: a **Postgres** `.global()` writer from a row-client (postgres.js/pg over Hyperdrive). */
175
218
  declare const createPostgresGlobalCtxDb: (client: RowClient, options: Omit<CreateHyperdriveGlobalCtxDbOptions, "engine" | "exec">) => DatabaseWriterLike;
176
219
  /** Convenience: a **MySQL** `.global()` writer from a `mysql2/promise` connection/pool (created with `flags: ["FOUND_ROWS"]`). */
177
- declare const createMysqlGlobalCtxDb: (connection: Mysql2Execute, options: Omit<CreateHyperdriveGlobalCtxDbOptions, "engine" | "exec">) => DatabaseWriterLike;
178
- export { CreateHyperdriveGlobalCtxDbOptions, HyperdriveEngine, type Mysql2Execute, type PgVectorIndexOptions, type RowClient, buildMysqlExec, buildPgExec, createHyperdriveGlobalCtxDb, createMysqlGlobalCtxDb, createPgVectorIndex, createPostgresGlobalCtxDb, mysqlDialect, postgresDialect };
220
+ declare const createMysqlGlobalCtxDb: (connection: Mysql2Client, options: Omit<CreateHyperdriveGlobalCtxDbOptions, "engine" | "exec">) => DatabaseWriterLike;
221
+ /** The studio's global data browser over a Hyperdrive database — every table in the connection's schema, internal ones hidden. */
222
+ interface HyperdriveGlobalIntrospector {
223
+ facetColumn: (options: FacetGlobalColumnOptions) => Promise<GlobalFacetResult>;
224
+ listTables: () => Promise<GlobalTableInfo[]>;
225
+ readTablePage: (options: ReadGlobalTablePageOptions) => Promise<GlobalTablePage>;
226
+ }
227
+ /** Build the studio's global-browser introspector for a Hyperdrive `.global()` store. */
228
+ declare const createHyperdriveGlobalIntrospector: (engine: HyperdriveEngine, exec: SqlExec, schema: SchemaLike) => HyperdriveGlobalIntrospector;
229
+ /**
230
+ * The admin CDC sync's global plane on Hyperdrive: a page of the global
231
+ * `__cdc_log` past `sinceSeq`. The log only exists once a CDC-enabled writer has
232
+ * run, so its absence is "nothing has changed yet" — an empty page that leaves
233
+ * the cursor where it was — not an error.
234
+ */
235
+ declare const readHyperdriveGlobalCdc: (engine: HyperdriveEngine, exec: SqlExec, request: {
236
+ limit?: number;
237
+ now?: number;
238
+ sinceSeq: number;
239
+ }) => Promise<{
240
+ changes: CdcChange[];
241
+ cursor: number;
242
+ }>;
243
+ /**
244
+ * The admin point-in-time apply's global plane on Hyperdrive: replay CDC changes
245
+ * in order through the store core (insert, falling back to replace). The writer
246
+ * carries the app's `cdc` setting so the replay appends to the global changelog
247
+ * like any other write. Returns the number replayed.
248
+ */
249
+ declare const applyHyperdriveGlobalCdc: (options: {
250
+ cdc: boolean;
251
+ changes: ReadonlyArray<CdcChange>;
252
+ engine: HyperdriveEngine;
253
+ exec: SqlExec;
254
+ schema: SchemaLike;
255
+ }) => Promise<number>;
256
+ export { CreateHyperdriveGlobalCtxDbOptions, ExportHyperdriveGlobalRowsOptions, HyperdriveEngine, HyperdriveGlobalIntrospector, ImportHyperdriveGlobalRowsOptions, type Mysql2Client, type PgVectorIndexOptions, type RowClient, applyHyperdriveGlobalCdc, buildMysqlExec, buildPgExec, createHyperdriveGlobalCtxDb, createHyperdriveGlobalIntrospector, createMysqlGlobalCtxDb, createPgVectorIndex, createPostgresGlobalCtxDb, exportHyperdriveGlobalRows, importHyperdriveGlobalRows, mysqlDialect, postgresDialect, readHyperdriveGlobalCdc };
package/dist/global.mjs CHANGED
@@ -1 +1 @@
1
- import{createSqlCtxDb as c}from"@lunora/sql-store";import{postgresDialect as l,mysqlDialect as s}from"./packem_shared/mysqlDialect-BS1Sz60B.mjs";import{buildMysqlExec as a,buildPgExec as i}from"./packem_shared/buildMysqlExec-CjkrA5JG.mjs";import{createPgVectorIndex as d}from"./packem_shared/createPgVectorIndex-CSNnG065.mjs";const r=({engine:e,exec:t,...o})=>c({...o,dialect:e==="postgres"?l:s,exec:t}),p=(e,t)=>r({...t,engine:"postgres",exec:i(e)}),g=(e,t)=>r({...t,engine:"mysql",exec:a(e)});export{a as buildMysqlExec,i as buildPgExec,r as createHyperdriveGlobalCtxDb,g as createMysqlGlobalCtxDb,d as createPgVectorIndex,p as createPostgresGlobalCtxDb,s as mysqlDialect,l as postgresDialect};
1
+ import{applyCdcChanges as d,CDC_LOG_TABLE as b}from"@lunora/shard-engine";import{createSqlCtxDb as p,readSqlGlobalTablePage as g,listSqlGlobalTables as m,facetSqlGlobalColumn as y,exportSqlGlobalRows as C,importSqlGlobalRows as G,readSqlCdcChanges as x}from"@lunora/sql-store";import{postgresDialect as q,mysqlDialect as u}from"./packem_shared/mysqlDialect-BS1Sz60B.mjs";import{buildMysqlExec as f,buildPgExec as v}from"./packem_shared/buildMysqlExec-PqvV5h4p.mjs";import{createPgVectorIndex as A}from"./packem_shared/createPgVectorIndex-B2Wbzqns.mjs";const o=e=>e==="postgres"?q:u,c=({engine:e,exec:l,...r})=>p({...r,dialect:o(e),exec:l}),H=({engine:e,exec:l,schema:r,tables:t})=>C(l,r,o(e),t===void 0?{}:{tables:t}),P=({cdc:e,dryRun:l,engine:r,exec:t,onConflict:a,rows:s,schema:n,startLine:i})=>G(c({cdc:e,engine:r,exec:t,schema:n}),n,o(r),{dryRun:l===!0,exec:t,onConflict:a??"skip",rows:s,...i===void 0?{}:{startLine:i}}),R=(e,l)=>c({...l,engine:"postgres",exec:v(e)}),T=(e,l)=>c({...l,engine:"mysql",exec:f(e)}),E=(e,l,r)=>{const t=o(e);return{facetColumn:a=>y(l,r,t,a),listTables:()=>m(l,r,t),readTablePage:a=>g(l,r,t,a)}},I=async(e,l,r)=>{const t=o(e),{params:a,sql:s}=t.render(t.tableExists(b));return(await l.all(s,a)).length===0?{changes:[],cursor:r.sinceSeq}:x(l,r,t)},M=async e=>(await d(c({cdc:e.cdc,engine:e.engine,exec:e.exec,schema:e.schema}),e.changes),e.changes.length);export{M as applyHyperdriveGlobalCdc,f as buildMysqlExec,v as buildPgExec,c as createHyperdriveGlobalCtxDb,E as createHyperdriveGlobalIntrospector,T as createMysqlGlobalCtxDb,A as createPgVectorIndex,R as createPostgresGlobalCtxDb,H as exportHyperdriveGlobalRows,P as importHyperdriveGlobalRows,u as mysqlDialect,q as postgresDialect,I as readHyperdriveGlobalCdc};
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { H as HyperdriveLike, a as HyperdriveConnection, M as Mysql2Like, S as SqlClient, N as NodePgLike, P as PostgresJsLike } from "./packem_shared/types.d-B2JdXSms.mjs";
1
+ import { H as HyperdriveLike, a as HyperdriveConnection, M as Mysql2Like, S as SqlClient, N as NodePgLike, P as PostgresJsLike } from "./packem_shared/types.d-Zm06MRhL.mjs";
2
2
  /**
3
3
  * Surface a Cloudflare Hyperdrive binding as a connection ready to feed a
4
4
  * user-supplied SQL driver.
@@ -18,7 +18,11 @@ import { H as HyperdriveLike, a as HyperdriveConnection, M as Mysql2Like, S as S
18
18
  * // not per request. `ctx.sql` is emitted `readonly`; it is never assigned.
19
19
  * const app = defineApp<Env>()
20
20
  * .shard((env) => env.SHARD)
21
- * .hyperdrive((env) => fromPostgresJs(postgres(createHyperdrive(env.HYPERDRIVE).connectionString)))
21
+ * .hyperdrive((env) => {
22
+ * const { connectionString } = createHyperdrive(env.HYPERDRIVE);
23
+ *
24
+ * return fromPostgresJs(postgres(connectionString, { fetch_types: false, max: 5 }));
25
+ * })
22
26
  * .build();
23
27
  *
24
28
  * // Then, inside an action (never a query/mutation):
@@ -45,6 +49,13 @@ declare const createHyperdrive: (binding: HyperdriveLike) => {
45
49
  * Uses the driver's `.unsafe(text, params)` escape hatch so the caller supplies
46
50
  * a plain SQL string with `$1, $2, …` placeholders and a positional params
47
51
  * array. postgres.js's `.unsafe` resolves to a row array.
52
+ *
53
+ * `prepare: true` because `.unsafe` defaults to `prepare: false`, which
54
+ * Cloudflare warns against: under Hyperdrive's transaction pooling unnamed
55
+ * statements can hang or fail intermittently, and each one costs a Describe
56
+ * round trip before it executes. A client built with `prepare: false` still
57
+ * wins. Prepared reads are cacheable by Hyperdrive, so a `.global()` store's
58
+ * config must have caching disabled (see `@lunora/hyperdrive/global`).
48
59
  */
49
60
  declare const fromPostgresJs: (client: PostgresJsLike) => SqlClient;
50
61
  /**
@@ -56,7 +67,7 @@ declare const fromNodePg: (client: NodePgLike) => SqlClient;
56
67
  /**
57
68
  * Wrap a `mysql2/promise` connection or pool as a {@link SqlClient}.
58
69
  *
59
- * Use `?` placeholders (MySQL positional syntax). `mysql2`'s `execute` resolves
70
+ * Use `?` placeholders (MySQL positional syntax). `mysql2`'s `query` resolves
60
71
  * to a `[rows, fields]` tuple; the adapter returns the first element. For a
61
72
  * non-`SELECT` (DML), `mysql2` yields a `ResultSetHeader` object rather than a
62
73
  * row array, so the adapter normalises that to `[]` — matching the empty-array
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { H as HyperdriveLike, a as HyperdriveConnection, M as Mysql2Like, S as SqlClient, N as NodePgLike, P as PostgresJsLike } from "./packem_shared/types.d-B2JdXSms.js";
1
+ import { H as HyperdriveLike, a as HyperdriveConnection, M as Mysql2Like, S as SqlClient, N as NodePgLike, P as PostgresJsLike } from "./packem_shared/types.d-Zm06MRhL.js";
2
2
  /**
3
3
  * Surface a Cloudflare Hyperdrive binding as a connection ready to feed a
4
4
  * user-supplied SQL driver.
@@ -18,7 +18,11 @@ import { H as HyperdriveLike, a as HyperdriveConnection, M as Mysql2Like, S as S
18
18
  * // not per request. `ctx.sql` is emitted `readonly`; it is never assigned.
19
19
  * const app = defineApp<Env>()
20
20
  * .shard((env) => env.SHARD)
21
- * .hyperdrive((env) => fromPostgresJs(postgres(createHyperdrive(env.HYPERDRIVE).connectionString)))
21
+ * .hyperdrive((env) => {
22
+ * const { connectionString } = createHyperdrive(env.HYPERDRIVE);
23
+ *
24
+ * return fromPostgresJs(postgres(connectionString, { fetch_types: false, max: 5 }));
25
+ * })
22
26
  * .build();
23
27
  *
24
28
  * // Then, inside an action (never a query/mutation):
@@ -45,6 +49,13 @@ declare const createHyperdrive: (binding: HyperdriveLike) => {
45
49
  * Uses the driver's `.unsafe(text, params)` escape hatch so the caller supplies
46
50
  * a plain SQL string with `$1, $2, …` placeholders and a positional params
47
51
  * array. postgres.js's `.unsafe` resolves to a row array.
52
+ *
53
+ * `prepare: true` because `.unsafe` defaults to `prepare: false`, which
54
+ * Cloudflare warns against: under Hyperdrive's transaction pooling unnamed
55
+ * statements can hang or fail intermittently, and each one costs a Describe
56
+ * round trip before it executes. A client built with `prepare: false` still
57
+ * wins. Prepared reads are cacheable by Hyperdrive, so a `.global()` store's
58
+ * config must have caching disabled (see `@lunora/hyperdrive/global`).
48
59
  */
49
60
  declare const fromPostgresJs: (client: PostgresJsLike) => SqlClient;
50
61
  /**
@@ -56,7 +67,7 @@ declare const fromNodePg: (client: NodePgLike) => SqlClient;
56
67
  /**
57
68
  * Wrap a `mysql2/promise` connection or pool as a {@link SqlClient}.
58
69
  *
59
- * Use `?` placeholders (MySQL positional syntax). `mysql2`'s `execute` resolves
70
+ * Use `?` placeholders (MySQL positional syntax). `mysql2`'s `query` resolves
60
71
  * to a `[rows, fields]` tuple; the adapter returns the first element. For a
61
72
  * non-`SELECT` (DML), `mysql2` yields a `ResultSetHeader` object rather than a
62
73
  * row array, so the adapter normalises that to `[]` — matching the empty-array
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createHyperdrive as e,fromMysql2 as f,fromNodePg as m,fromPostgresJs as p}from"./packem_shared/createHyperdrive-BeXs5Dr6.mjs";import{projectSourceRow as t,pullSourceRows as c}from"./packem_shared/projectSourceRow-DN8ywPRl.mjs";export{e as createHyperdrive,f as fromMysql2,m as fromNodePg,p as fromPostgresJs,t as projectSourceRow,c as pullSourceRows};
1
+ import{createHyperdrive as e,fromMysql2 as f,fromNodePg as m,fromPostgresJs as p}from"./packem_shared/createHyperdrive-DWZZ_3e2.mjs";import{projectSourceRow as t,pullSourceRows as c}from"./packem_shared/projectSourceRow-DN8ywPRl.mjs";export{e as createHyperdrive,f as fromMysql2,m as fromNodePg,p as fromPostgresJs,t as projectSourceRow,c as pullSourceRows};
@@ -0,0 +1 @@
1
+ import{LunoraError as n}from"@lunora/errors";import{t as o}from"./mysql-params-D4Ijlvo5.mjs";const i=2,s=new WeakSet,c=t=>{const e=t,r=e.config?.clientFlags;if(typeof r=="number")return r;const a=e.pool?.config?.connectionConfig?.clientFlags;return typeof a=="number"?a:void 0},l=t=>{const e=c(t);if(e===void 0){if(s.has(t))return;s.add(t),console.warn('@lunora/hyperdrive: could not determine whether this MySQL connection negotiated CLIENT_FOUND_ROWS (the driver exposes no flag introspection). If it was NOT created with the `CLIENT_FOUND_ROWS` flag (mysql2: `createPool({ flags: ["FOUND_ROWS"] })`), an idempotent patch/replace that rewrites identical values may report 0 affectedRows and raise a spurious OCC ConflictError.');return}if((e&i)===0)throw new n("INTERNAL",'@lunora/hyperdrive: this MySQL connection was not created with the `CLIENT_FOUND_ROWS` flag (mysql2: `createPool({ flags: ["FOUND_ROWS"] })`). Without it, `affectedRows` counts changed (not matched) rows, so an idempotent patch/replace that rewrites identical values reports 0 and the OCC guard raises a spurious conflict.')},f=t=>({all:(e,r)=>t.query(e,r),batch:async e=>{await Promise.all(e.map(r=>t.query(r.sql,r.params)))},run:async(e,r)=>(await t.query(e,r),{rowsAffected:0})}),p=t=>(l(t),{all:async(e,r)=>{const[a]=await t.query(e,o(r));return Array.isArray(a)?a:[]},batch:async e=>{await Promise.all(e.map(r=>t.query(r.sql,o(r.params))))},run:async(e,r)=>{const[a]=await t.query(e,o(r));return{rowsAffected:a.affectedRows??0}}});export{p as buildMysqlExec,f as buildPgExec};
@@ -0,0 +1 @@
1
+ import{t as o}from"./mysql-params-D4Ijlvo5.mjs";const n=r=>({config:{database:r.database,host:r.host,password:r.password,port:r.port,user:r.user},connectionString:r.connectionString}),c=r=>({dbSystem:"postgresql",query:async(s,t=[])=>await r.unsafe(s,t,{prepare:!0})}),u=r=>({dbSystem:"postgresql",query:async(s,t=[])=>(await r.query(s,t)).rows}),y=r=>({dbSystem:"mysql",query:async(s,t=[])=>{const[e]=await r.query(s,o(t));return Array.isArray(e)?e:[]}});export{n as createHyperdrive,y as fromMysql2,u as fromNodePg,c as fromPostgresJs};
@@ -0,0 +1 @@
1
+ const F=t=>/^[A-Z_]\w*$/i.test(t);const X=new Set(["42P06","42P07","23505","42710"]),J=t=>typeof t=="object"&&t!==null&&"code"in t&&typeof t.code=="string"&&X.has(t.code),P=5,j="__vec_",v="__ann_",b={cosine:{operator:"<=>",operatorClass:"vector_cosine_ops",score:t=>1-t},"dot-product":{operator:"<#>",operatorClass:"vector_ip_ops",score:t=>t},euclidean:{operator:"<->",operatorClass:"vector_l2_ops",score:t=>t}},U=63,A=U-j.length-v.length-Math.max(...Object.values(b).map(t=>t.operatorClass.length)),C="SELECT jsonb_array_elements_text($1::text::jsonb)",R=t=>`[${[...t].join(",")}]`,D=t=>{const c=Object.getPrototypeOf(t);return c===null||c===Object.prototype},L=t=>t.constructor?.name??"non-plain object",B=(t,c)=>{const o=(e,r)=>{throw new TypeError(`@lunora/hyperdrive: pgvector index "${c}" supports equality metadata filters only, but "${r}" ${e}. Pre-compute it into an equality-shaped metadata field, or keep this index on Vectorize.`)},E=(e,r)=>{(e===void 0||typeof e=="function"||typeof e=="symbol")&&o(`has a value at "${r}" that JSON cannot represent, which would widen the filter to match every row`,r),typeof e=="number"&&!Number.isFinite(e)&&o(`has a non-finite number at "${r}", which JSON turns into null`,r),typeof e=="bigint"&&o(`has a bigint at "${r}", which JSON cannot serialise`,r)},m=(e,r)=>{if(E(e,r),Array.isArray(e)){for(const[p,l]of e.entries())m(l,`${r}[${String(p)}]`);return}if(!(typeof e!="object"||e===null)){D(e)||o(`is a ${L(e)}, whose fields JSON does not preserve`,r);for(const[p,l]of Object.entries(e))p.startsWith("$")&&o("uses a comparison operator",r),m(l,`${r}.${p}`)}};D(t)||o(`is a ${L(t)}, whose fields JSON does not preserve`,"filter");for(const[e,r]of Object.entries(t))e.startsWith("$")&&o("uses a comparison operator",e),e.includes(".")&&o("uses Vectorize's dot-addressed nested syntax, which JSONB containment cannot express",e),m(r,e)},H=t=>{if(!F(t))throw new TypeError(`@lunora/hyperdrive: pgvector \`name\` must be a bare SQL identifier (letters, digits, underscore; not starting with a digit) — got "${t}"`);if(t.length>A)throw new TypeError(`@lunora/hyperdrive: pgvector \`name\` must be at most ${String(A)} characters — got "${t}" (${String(t.length)}). Postgres truncates identifiers at 63 bytes and this index derives longer names from it.`);return t},V=t=>{const{client:c,dimensions:o,metric:E="cosine",name:m}=t,e=`${j}${H(m)}`;if(!Object.hasOwn(b,E))throw new TypeError(`@lunora/hyperdrive: pgvector index "${m}" got an unknown metric "${E}" — expected cosine, euclidean, or dot-product`);const{operator:r,operatorClass:p,score:l}=b[E];if(!Number.isInteger(o)||o<=0)throw new TypeError(`@lunora/hyperdrive: pgvector index "${m}" needs a positive integer \`dimensions\` — got ${String(o)}`);if(o>2e3)throw new TypeError(`@lunora/hyperdrive: pgvector index "${m}" declares ${String(o)} dimensions, over the HNSW limit of ${String(2e3)}. Reduce the embedding width (Vectorize itself tops out at 1536), or keep this index on Vectorize.`);let O=0;const N=()=>(O+=1,`${e}:${String(O)}`);let T;const $=async()=>(T??=(async()=>{for(const n of["CREATE EXTENSION IF NOT EXISTS vector",`CREATE TABLE IF NOT EXISTS "${e}" (id text PRIMARY KEY, namespace text, embedding vector(${String(o)}) NOT NULL, metadata jsonb)`,`CREATE INDEX IF NOT EXISTS "${e}${v}${p}" ON "${e}" USING hnsw (embedding ${p})`,`CREATE INDEX IF NOT EXISTS "${e}__ns" ON "${e}" (namespace)`,`CREATE INDEX IF NOT EXISTS "${e}__meta" ON "${e}" USING gin (metadata)`])try{await c.query(n)}catch(a){if(!J(a))throw a}})().catch(n=>{throw T=void 0,n}),T),w=async(n,a)=>{if(n.length===0)return{mutationId:N()};await $();const s=[...new Map(n.map(i=>[i.id,i])).values()],g=[],u=i=>`$${String(g.push(i))}`,h=s.map(i=>`(${u(i.id)}, ${u(i.namespace??null)}, ${u(R(i.values))}::vector, ${u(i.metadata===void 0?null:JSON.stringify(i.metadata))}::text::jsonb)`);return await c.query(`INSERT INTO "${e}" (id, namespace, embedding, metadata) VALUES ${h.join(", ")} ${a}`,g),{mutationId:N()}};return{async deleteByIds(n){return n.length===0?{count:0,mutationId:N()}:(await $(),{count:(await c.query(`DELETE FROM "${e}" WHERE id IN (${C}) RETURNING id`,[JSON.stringify(n)])).length,mutationId:N()})},async describe(){await $();const[n]=await c.query(`SELECT count(*) AS count FROM "${e}"`);return{dimensions:o,vectorsCount:Number(n?.count??0)}},async getByIds(n){return n.length===0?[]:(await $(),(await c.query(`SELECT id, namespace, metadata, embedding::text AS embedding FROM "${e}" WHERE id IN (${C})`,[JSON.stringify(n)])).map(s=>({id:s.id,...s.metadata===null?{}:{metadata:s.metadata},...s.namespace===null?{}:{namespace:s.namespace},values:JSON.parse(s.embedding)})))},async insert(n){return w(n,"ON CONFLICT (id) DO NOTHING")},async query(n,a){const s=a?.topK??P;if(!Number.isInteger(s)||s<=0)throw new TypeError(`@lunora/hyperdrive: pgvector index "${m}" needs a positive integer \`topK\` — got ${String(s)}`);a?.filter!==void 0&&B(a.filter,m),await $();const g=a?.returnValues===!0,u=a?.returnMetadata??"none",h=u!=="none"&&u!==!1,i=[],f=d=>`$${String(i.push(d))}`,y=`embedding ${r} ${f(R(n))}::vector`,S=[];a?.namespace!==void 0&&S.push(`namespace = ${f(a.namespace)}`),a?.filter!==void 0&&Object.keys(a.filter).length>0&&S.push(`metadata @> ${f(JSON.stringify(a.filter))}::text::jsonb`);const I=["id","namespace",`${y} AS distance`];h&&I.push("metadata"),g&&I.push("embedding::text AS embedding");const M=S.length>0?`(SELECT * FROM "${e}" WHERE ${S.join(" AND ")})`:`"${e}"`,x=S.length>0?`WITH filtered AS MATERIALIZED ${M} SELECT ${I.join(", ")} FROM filtered ORDER BY ${y} LIMIT ${f(s)}`:`SELECT ${I.join(", ")} FROM "${e}" ORDER BY ${y} LIMIT ${f(s)}`,_=(await c.query(x,i)).map(d=>({id:d.id,...h&&d.metadata!==null&&d.metadata!==void 0?{metadata:d.metadata}:{},...d.namespace===null?{}:{namespace:d.namespace},score:l(Number(d.distance)),...g&&d.embedding!==void 0?{values:JSON.parse(d.embedding)}:{}}));return{count:_.length,matches:_}},async upsert(n){return w(n,"ON CONFLICT (id) DO UPDATE SET embedding = EXCLUDED.embedding, namespace = EXCLUDED.namespace, metadata = EXCLUDED.metadata")}}};export{V as createPgVectorIndex};
@@ -0,0 +1 @@
1
+ const n=t=>({toSqlString:()=>`'${t.replaceAll("\\","\\\\").replaceAll("'","''")}'`,toString:()=>t}),e=t=>t.map((r,i)=>{if(r===void 0)throw new TypeError(`@lunora/hyperdrive: MySQL bind parameter ${String(i+1)} is undefined`);return typeof r=="string"?n(r):r===null||typeof r!="object"||r instanceof Date||ArrayBuffer.isView(r)?r:r instanceof ArrayBuffer?new Uint8Array(r):n(JSON.stringify(r))});export{e as t};
@@ -125,24 +125,35 @@ interface NodePgLike {
125
125
  * direction, which is all bivariance asks for.
126
126
  */
127
127
  interface PostgresJsLike {
128
- unsafe(this: void, text: string, params?: ReadonlyArray<unknown>): Promise<unknown>;
128
+ unsafe(this: void, text: string, params?: ReadonlyArray<unknown>, options?: {
129
+ prepare?: boolean;
130
+ }): Promise<unknown>;
129
131
  }
130
132
  /**
131
133
  * Structural projection of a `mysql2/promise` connection/pool. `mysql2`'s
132
- * `execute` resolves to a `[rows, fields]` tuple; the adapter takes the first
134
+ * `query` resolves to a `[rows, fields]` tuple; the adapter takes the first
133
135
  * element as the rows.
134
136
  *
137
+ * `query`, not `execute`: `execute` is a protocol-level prepared statement
138
+ * (`COM_STMT_PREPARE` + the binary protocol), which Hyperdrive does not support
139
+ * for MySQL. `query` interpolates the `?` placeholders client-side and sends
140
+ * plain text. Strings are quoted with `''` for a quote and `\\` for a
141
+ * backslash, so a server whose `sql_mode` includes `NO_BACKSLASH_ESCAPES` is
142
+ * unsupported: there a backslash is stored twice. No value can end its literal
143
+ * under either mode. mysql2 skips a `?` inside a quoted string or comment, but
144
+ * reads `??` as an identifier placeholder.
145
+ *
135
146
  * Method syntax, not an arrow property — see {@link PostgresJsLike} — but here
136
147
  * that is necessary and NOT sufficient, which is why `params` is `unknown`.
137
148
  *
138
- * mysql2 types its second argument as a single `ExecuteValues`: a union of
139
- * scalars, `ExecuteValues[]` and `{ [key: string]: ExecuteValues }`. Declared
149
+ * mysql2 types its second argument as a single `QueryValues`: a union of
150
+ * scalars, `QueryValues[]` and `{ [key: string]: QueryValues }`. Declared
140
151
  * `params?: ReadonlyArray<unknown>` this projection could never accept the
141
152
  * driver it projects — bivariance needs the two parameter types to relate in
142
153
  * ONE direction and they relate in neither (`readonly unknown[] | undefined` is
143
- * not an `ExecuteValues`, and a `string` is not an array).
154
+ * not a `QueryValues`, and a `string` is not an array).
144
155
  *
145
- * `unknown` is the narrowest supertype of `ExecuteValues` that does not restate
156
+ * `unknown` is the narrowest supertype of `QueryValues` that does not restate
146
157
  * mysql2's union here. Restating it would be the vacuous shim all over again —
147
158
  * a local copy that drifts from the package it mirrors and makes the gate pass
148
159
  * while a consumer fails.
@@ -154,6 +165,6 @@ interface PostgresJsLike {
154
165
  * values at runtime regardless.
155
166
  */
156
167
  interface Mysql2Like {
157
- execute(this: void, text: string, params?: unknown): Promise<[unknown, unknown]>;
168
+ query(this: void, text: string, params?: unknown): Promise<[unknown, unknown]>;
158
169
  }
159
170
  export { HyperdriveLike as H, Mysql2Like as M, NodePgLike as N, PostgresJsLike as P, SqlClient as S, HyperdriveConnection as a };
@@ -125,24 +125,35 @@ interface NodePgLike {
125
125
  * direction, which is all bivariance asks for.
126
126
  */
127
127
  interface PostgresJsLike {
128
- unsafe(this: void, text: string, params?: ReadonlyArray<unknown>): Promise<unknown>;
128
+ unsafe(this: void, text: string, params?: ReadonlyArray<unknown>, options?: {
129
+ prepare?: boolean;
130
+ }): Promise<unknown>;
129
131
  }
130
132
  /**
131
133
  * Structural projection of a `mysql2/promise` connection/pool. `mysql2`'s
132
- * `execute` resolves to a `[rows, fields]` tuple; the adapter takes the first
134
+ * `query` resolves to a `[rows, fields]` tuple; the adapter takes the first
133
135
  * element as the rows.
134
136
  *
137
+ * `query`, not `execute`: `execute` is a protocol-level prepared statement
138
+ * (`COM_STMT_PREPARE` + the binary protocol), which Hyperdrive does not support
139
+ * for MySQL. `query` interpolates the `?` placeholders client-side and sends
140
+ * plain text. Strings are quoted with `''` for a quote and `\\` for a
141
+ * backslash, so a server whose `sql_mode` includes `NO_BACKSLASH_ESCAPES` is
142
+ * unsupported: there a backslash is stored twice. No value can end its literal
143
+ * under either mode. mysql2 skips a `?` inside a quoted string or comment, but
144
+ * reads `??` as an identifier placeholder.
145
+ *
135
146
  * Method syntax, not an arrow property — see {@link PostgresJsLike} — but here
136
147
  * that is necessary and NOT sufficient, which is why `params` is `unknown`.
137
148
  *
138
- * mysql2 types its second argument as a single `ExecuteValues`: a union of
139
- * scalars, `ExecuteValues[]` and `{ [key: string]: ExecuteValues }`. Declared
149
+ * mysql2 types its second argument as a single `QueryValues`: a union of
150
+ * scalars, `QueryValues[]` and `{ [key: string]: QueryValues }`. Declared
140
151
  * `params?: ReadonlyArray<unknown>` this projection could never accept the
141
152
  * driver it projects — bivariance needs the two parameter types to relate in
142
153
  * ONE direction and they relate in neither (`readonly unknown[] | undefined` is
143
- * not an `ExecuteValues`, and a `string` is not an array).
154
+ * not a `QueryValues`, and a `string` is not an array).
144
155
  *
145
- * `unknown` is the narrowest supertype of `ExecuteValues` that does not restate
156
+ * `unknown` is the narrowest supertype of `QueryValues` that does not restate
146
157
  * mysql2's union here. Restating it would be the vacuous shim all over again —
147
158
  * a local copy that drifts from the package it mirrors and makes the gate pass
148
159
  * while a consumer fails.
@@ -154,6 +165,6 @@ interface PostgresJsLike {
154
165
  * values at runtime regardless.
155
166
  */
156
167
  interface Mysql2Like {
157
- execute(this: void, text: string, params?: unknown): Promise<[unknown, unknown]>;
168
+ query(this: void, text: string, params?: unknown): Promise<[unknown, unknown]>;
158
169
  }
159
170
  export { HyperdriveLike as H, Mysql2Like as M, NodePgLike as N, PostgresJsLike as P, SqlClient as S, HyperdriveConnection as a };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/hyperdrive",
3
- "version": "1.0.0-alpha.211",
3
+ "version": "1.0.0-alpha.213",
4
4
  "description": "Bring-your-own Postgres/MySQL for Lunora via Cloudflare Hyperdrive: a driver-agnostic, action-only ctx.sql",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -52,8 +52,8 @@
52
52
  "dependencies": {
53
53
  "@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0",
54
54
  "@lunora/platform": "1.0.0-alpha.75",
55
- "@lunora/shard-engine": "1.0.0-alpha.165",
56
- "@lunora/sql-store": "1.0.0-alpha.214",
55
+ "@lunora/shard-engine": "1.0.0-alpha.167",
56
+ "@lunora/sql-store": "1.0.0-alpha.216",
57
57
  "drizzle-orm": "1.0.0-rc.5"
58
58
  },
59
59
  "peerDependencies": {
@@ -1 +0,0 @@
1
- import{LunoraError as s}from"@lunora/errors";const n=2,o=new WeakSet,i=t=>{const e=t,r=e.config?.clientFlags;if(typeof r=="number")return r;const a=e.pool?.config?.connectionConfig?.clientFlags;return typeof a=="number"?a:void 0},c=t=>{const e=i(t);if(e===void 0){if(o.has(t))return;o.add(t),console.warn('@lunora/hyperdrive: could not determine whether this MySQL connection negotiated CLIENT_FOUND_ROWS (the driver exposes no flag introspection). If it was NOT created with the `CLIENT_FOUND_ROWS` flag (mysql2: `createPool({ flags: ["FOUND_ROWS"] })`), an idempotent patch/replace that rewrites identical values may report 0 affectedRows and raise a spurious OCC ConflictError.');return}if((e&n)===0)throw new s("INTERNAL",'@lunora/hyperdrive: this MySQL connection was not created with the `CLIENT_FOUND_ROWS` flag (mysql2: `createPool({ flags: ["FOUND_ROWS"] })`). Without it, `affectedRows` counts changed (not matched) rows, so an idempotent patch/replace that rewrites identical values reports 0 and the OCC guard raises a spurious conflict.')},u=t=>({all:(e,r)=>t.query(e,r),batch:async e=>{await Promise.all(e.map(r=>t.query(r.sql,r.params)))},run:async(e,r)=>(await t.query(e,r),{rowsAffected:0})}),d=t=>(c(t),{all:async(e,r)=>{const[a]=await t.execute(e,r);return Array.isArray(a)?a:[]},batch:async e=>{await Promise.all(e.map(r=>t.execute(r.sql,r.params)))},run:async(e,r)=>{const[a]=await t.execute(e,r);return{rowsAffected:a.affectedRows??0}}});export{d as buildMysqlExec,u as buildPgExec};
@@ -1 +0,0 @@
1
- const o=r=>({config:{database:r.database,host:r.host,password:r.password,port:r.port,user:r.user},connectionString:r.connectionString}),a=r=>({dbSystem:"postgresql",query:async(s,t=[])=>await r.unsafe(s,t)}),c=r=>({dbSystem:"postgresql",query:async(s,t=[])=>(await r.query(s,t)).rows}),n=r=>({dbSystem:"mysql",query:async(s,t=[])=>{const[e]=await r.execute(s,t);return Array.isArray(e)?e:[]}});export{o as createHyperdrive,n as fromMysql2,c as fromNodePg,a as fromPostgresJs};
@@ -1 +0,0 @@
1
- const x=t=>/^[A-Z_]\w*$/i.test(t);const F=new Set(["42P06","42P07","23505","42710"]),X=t=>typeof t=="object"&&t!==null&&"code"in t&&typeof t.code=="string"&&F.has(t.code),P=5,v="__vec_",L="__ann_",y={cosine:{operator:"<=>",operatorClass:"vector_cosine_ops",score:t=>1-t},"dot-product":{operator:"<#>",operatorClass:"vector_ip_ops",score:t=>t},euclidean:{operator:"<->",operatorClass:"vector_l2_ops",score:t=>t}},J=63,A=J-v.length-L.length-Math.max(...Object.values(y).map(t=>t.operatorClass.length)),C=t=>`[${[...t].join(",")}]`,R=t=>{const c=Object.getPrototypeOf(t);return c===null||c===Object.prototype},D=t=>t.constructor?.name??"non-plain object",U=(t,c)=>{const o=(e,r)=>{throw new TypeError(`@lunora/hyperdrive: pgvector index "${c}" supports equality metadata filters only, but "${r}" ${e}. Pre-compute it into an equality-shaped metadata field, or keep this index on Vectorize.`)},l=(e,r)=>{(e===void 0||typeof e=="function"||typeof e=="symbol")&&o(`has a value at "${r}" that JSON cannot represent, which would widen the filter to match every row`,r),typeof e=="number"&&!Number.isFinite(e)&&o(`has a non-finite number at "${r}", which JSON turns into null`,r),typeof e=="bigint"&&o(`has a bigint at "${r}", which JSON cannot serialise`,r)},m=(e,r)=>{if(l(e,r),Array.isArray(e)){for(const[p,E]of e.entries())m(E,`${r}[${String(p)}]`);return}if(!(typeof e!="object"||e===null)){R(e)||o(`is a ${D(e)}, whose fields JSON does not preserve`,r);for(const[p,E]of Object.entries(e))p.startsWith("$")&&o("uses a comparison operator",r),m(E,`${r}.${p}`)}};R(t)||o(`is a ${D(t)}, whose fields JSON does not preserve`,"filter");for(const[e,r]of Object.entries(t))e.startsWith("$")&&o("uses a comparison operator",e),e.includes(".")&&o("uses Vectorize's dot-addressed nested syntax, which JSONB containment cannot express",e),m(r,e)},B=t=>{if(!x(t))throw new TypeError(`@lunora/hyperdrive: pgvector \`name\` must be a bare SQL identifier (letters, digits, underscore; not starting with a digit) — got "${t}"`);if(t.length>A)throw new TypeError(`@lunora/hyperdrive: pgvector \`name\` must be at most ${String(A)} characters — got "${t}" (${String(t.length)}). Postgres truncates identifiers at 63 bytes and this index derives longer names from it.`);return t},W=t=>{const{client:c,dimensions:o,metric:l="cosine",name:m}=t,e=`${v}${B(m)}`;if(!Object.hasOwn(y,l))throw new TypeError(`@lunora/hyperdrive: pgvector index "${m}" got an unknown metric "${l}" — expected cosine, euclidean, or dot-product`);const{operator:r,operatorClass:p,score:E}=y[l];if(!Number.isInteger(o)||o<=0)throw new TypeError(`@lunora/hyperdrive: pgvector index "${m}" needs a positive integer \`dimensions\` — got ${String(o)}`);if(o>2e3)throw new TypeError(`@lunora/hyperdrive: pgvector index "${m}" declares ${String(o)} dimensions, over the HNSW limit of ${String(2e3)}. Reduce the embedding width (Vectorize itself tops out at 1536), or keep this index on Vectorize.`);let O=0;const N=()=>(O+=1,`${e}:${String(O)}`);let T;const $=async()=>(T??=(async()=>{for(const n of["CREATE EXTENSION IF NOT EXISTS vector",`CREATE TABLE IF NOT EXISTS "${e}" (id text PRIMARY KEY, namespace text, embedding vector(${String(o)}) NOT NULL, metadata jsonb)`,`CREATE INDEX IF NOT EXISTS "${e}${L}${p}" ON "${e}" USING hnsw (embedding ${p})`,`CREATE INDEX IF NOT EXISTS "${e}__ns" ON "${e}" (namespace)`,`CREATE INDEX IF NOT EXISTS "${e}__meta" ON "${e}" USING gin (metadata)`])try{await c.query(n)}catch(a){if(!X(a))throw a}})().catch(n=>{throw T=void 0,n}),T),w=async(n,a)=>{if(n.length===0)return{mutationId:N()};await $();const s=[...new Map(n.map(i=>[i.id,i])).values()],g=[],u=i=>`$${String(g.push(i))}`,h=s.map(i=>`(${u(i.id)}, ${u(i.namespace??null)}, ${u(C(i.values))}::vector, ${u(i.metadata===void 0?null:JSON.stringify(i.metadata))}::jsonb)`);return await c.query(`INSERT INTO "${e}" (id, namespace, embedding, metadata) VALUES ${h.join(", ")} ${a}`,g),{mutationId:N()}};return{async deleteByIds(n){return n.length===0?{count:0,mutationId:N()}:(await $(),{count:(await c.query(`DELETE FROM "${e}" WHERE id = ANY($1::text[]) RETURNING id`,[[...n]])).length,mutationId:N()})},async describe(){await $();const[n]=await c.query(`SELECT count(*) AS count FROM "${e}"`);return{dimensions:o,vectorsCount:Number(n?.count??0)}},async getByIds(n){return n.length===0?[]:(await $(),(await c.query(`SELECT id, namespace, metadata, embedding::text AS embedding FROM "${e}" WHERE id = ANY($1::text[])`,[[...n]])).map(s=>({id:s.id,...s.metadata===null?{}:{metadata:s.metadata},...s.namespace===null?{}:{namespace:s.namespace},values:JSON.parse(s.embedding)})))},async insert(n){return w(n,"ON CONFLICT (id) DO NOTHING")},async query(n,a){const s=a?.topK??P;if(!Number.isInteger(s)||s<=0)throw new TypeError(`@lunora/hyperdrive: pgvector index "${m}" needs a positive integer \`topK\` — got ${String(s)}`);a?.filter!==void 0&&U(a.filter,m),await $();const g=a?.returnValues===!0,u=a?.returnMetadata??"none",h=u!=="none"&&u!==!1,i=[],f=d=>`$${String(i.push(d))}`,b=`embedding ${r} ${f(C(n))}::vector`,S=[];a?.namespace!==void 0&&S.push(`namespace = ${f(a.namespace)}`),a?.filter!==void 0&&Object.keys(a.filter).length>0&&S.push(`metadata @> ${f(JSON.stringify(a.filter))}::jsonb`);const I=["id","namespace",`${b} AS distance`];h&&I.push("metadata"),g&&I.push("embedding::text AS embedding");const M=S.length>0?`(SELECT * FROM "${e}" WHERE ${S.join(" AND ")})`:`"${e}"`,j=S.length>0?`WITH filtered AS MATERIALIZED ${M} SELECT ${I.join(", ")} FROM filtered ORDER BY ${b} LIMIT ${f(s)}`:`SELECT ${I.join(", ")} FROM "${e}" ORDER BY ${b} LIMIT ${f(s)}`,_=(await c.query(j,i)).map(d=>({id:d.id,...h&&d.metadata!==null&&d.metadata!==void 0?{metadata:d.metadata}:{},...d.namespace===null?{}:{namespace:d.namespace},score:E(Number(d.distance)),...g&&d.embedding!==void 0?{values:JSON.parse(d.embedding)}:{}}));return{count:_.length,matches:_}},async upsert(n){return w(n,"ON CONFLICT (id) DO UPDATE SET embedding = EXCLUDED.embedding, namespace = EXCLUDED.namespace, metadata = EXCLUDED.metadata")}}};export{W as createPgVectorIndex};