@di-framework/pglite-component 6.0.9 → 6.0.11
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-APACHE +202 -0
- package/LICENSE-MIT +23 -0
- package/README.md +164 -4
- package/dist/BUILD-INFO.txt +4 -4
- package/dist/SHA256SUMS +3 -2
- package/dist/di-framework-pglite.wasm +0 -0
- package/dist/di-framework-pglite.wit +5 -3
- package/dist/js/adapter.d.ts +171 -0
- package/dist/js/adapter.js +514 -0
- package/dist/js/index.d.ts +5 -0
- package/dist/js/index.js +5 -0
- package/package.json +45 -6
- package/wit/world.wit +25 -4
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { type SqlDatabase, type SqlRow, type WasmMaybeResult, type WasmResult } from '@di-framework/repo';
|
|
2
|
+
export declare const WASM_PGLITE_MODULE_ID = "di-framework:pglite/database@0.2.0";
|
|
3
|
+
/** Database root the provider selects when `open` receives an empty path. */
|
|
4
|
+
export declare const WASM_PGLITE_DEFAULT_ROOT = "/data/pglite";
|
|
5
|
+
export type WasmPgValue = {
|
|
6
|
+
tag: 'null';
|
|
7
|
+
val?: undefined;
|
|
8
|
+
} | {
|
|
9
|
+
tag: 'integer';
|
|
10
|
+
val: bigint | number;
|
|
11
|
+
} | {
|
|
12
|
+
tag: 'real';
|
|
13
|
+
val: number;
|
|
14
|
+
} | {
|
|
15
|
+
tag: 'text';
|
|
16
|
+
val: string;
|
|
17
|
+
} | {
|
|
18
|
+
tag: 'blob';
|
|
19
|
+
val: Uint8Array | ArrayLike<number>;
|
|
20
|
+
} | {
|
|
21
|
+
tag: 'boolean';
|
|
22
|
+
val: boolean;
|
|
23
|
+
} | {
|
|
24
|
+
tag: 'json';
|
|
25
|
+
val: string;
|
|
26
|
+
} | {
|
|
27
|
+
tag: 'timestamp';
|
|
28
|
+
val: string;
|
|
29
|
+
};
|
|
30
|
+
export type WasmPgRow = Array<[string, WasmPgValue]>;
|
|
31
|
+
/**
|
|
32
|
+
* Guest-side view of the `connection` resource.
|
|
33
|
+
*
|
|
34
|
+
* TODO(#153): members use method syntax, so `tsc` compares their parameters
|
|
35
|
+
* bivariantly and the drift guard does not notice a removed or narrowed `value`
|
|
36
|
+
* case. Declare them as property signatures (`strictFunctionTypes`) once
|
|
37
|
+
* `integer` stops allowing `number`.
|
|
38
|
+
*/
|
|
39
|
+
export interface WasmPgliteConnection {
|
|
40
|
+
exec(sql: string): WasmMaybeResult<void>;
|
|
41
|
+
run(sql: string, params: WasmPgValue[]): WasmMaybeResult<bigint | number>;
|
|
42
|
+
query(sql: string, params: WasmPgValue[]): WasmMaybeResult<WasmPgRow[]>;
|
|
43
|
+
first(sql: string, params: WasmPgValue[]): WasmMaybeResult<WasmPgRow | undefined | null>;
|
|
44
|
+
begin(): WasmMaybeResult<void>;
|
|
45
|
+
commit(): WasmMaybeResult<void>;
|
|
46
|
+
rollback(): WasmMaybeResult<void>;
|
|
47
|
+
savepoint(name: string): WasmMaybeResult<void>;
|
|
48
|
+
releaseSavepoint(name: string): WasmMaybeResult<void>;
|
|
49
|
+
rollbackToSavepoint(name: string): WasmMaybeResult<void>;
|
|
50
|
+
close(): WasmMaybeResult<void>;
|
|
51
|
+
}
|
|
52
|
+
/** The `open-options` record. Every field is optional. */
|
|
53
|
+
export interface WasmPgliteOpenOptions {
|
|
54
|
+
/** Session database. The provider currently accepts only `template1`. */
|
|
55
|
+
database?: string;
|
|
56
|
+
/** Reserved by the provider: only `0` (or unset) is accepted. */
|
|
57
|
+
statementTimeoutMs?: number;
|
|
58
|
+
}
|
|
59
|
+
/** Guest-side view of the `database` interface module. */
|
|
60
|
+
export interface WasmPgliteModule {
|
|
61
|
+
open(path: string, options?: WasmPgliteOpenOptions): WasmMaybeResult<WasmPgliteConnection>;
|
|
62
|
+
}
|
|
63
|
+
/** The `pg-error` record carried by `execution-failed`. */
|
|
64
|
+
export interface WasmPgliteErrorRecord {
|
|
65
|
+
code?: string;
|
|
66
|
+
message?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface WasmPgliteErrorPayload {
|
|
69
|
+
tag: string;
|
|
70
|
+
val?: string | WasmPgliteErrorRecord;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A provider error. `code` is the SQLSTATE for `execution-failed` (for example
|
|
74
|
+
* `23505` for a unique violation). The message then starts with
|
|
75
|
+
* `PostgreSQL <code>`, the format `isUniqueViolation` from `@di-framework/repo` reads.
|
|
76
|
+
*/
|
|
77
|
+
export declare class WasmPgliteError extends Error {
|
|
78
|
+
readonly tag: string;
|
|
79
|
+
readonly detail?: string;
|
|
80
|
+
readonly code?: string;
|
|
81
|
+
constructor(payload: WasmPgliteErrorPayload, cause?: unknown);
|
|
82
|
+
}
|
|
83
|
+
/** Normalizes a thrown binding error or an `err` payload into an `Error`. */
|
|
84
|
+
export declare function normalizeWasmPgliteError(error: unknown): Error;
|
|
85
|
+
/** Unwraps `{ tag: 'ok' | 'err' }` records; passes bare values through. */
|
|
86
|
+
export declare function unwrapWasmPgliteResult<T>(value: T | WasmResult<T>): T;
|
|
87
|
+
/**
|
|
88
|
+
* Converts a bound JavaScript parameter into a `value` variant.
|
|
89
|
+
*
|
|
90
|
+
* - `bigint` in the safe integer range becomes `integer`; a larger one is sent
|
|
91
|
+
* as `text` (OID 0), so PostgreSQL parses it for an `int8`/`numeric` column
|
|
92
|
+
* without rounding through a double.
|
|
93
|
+
* - A `number` that is an integer outside the safe range throws a `TypeError`
|
|
94
|
+
* (pass a `bigint` or string for `int8`); other non-safe numbers, including
|
|
95
|
+
* NaN and Infinity, are `real` (`float8`).
|
|
96
|
+
* - `Date` becomes `timestamp` (RFC 3339 UTC); plain objects and arrays become
|
|
97
|
+
* `json`. Arrays are not PostgreSQL arrays: pass a literal such as `'{a,b}'`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function toWasmPgValue(value: unknown): WasmPgValue;
|
|
100
|
+
/**
|
|
101
|
+
* Formats a `timestamp` variant the way the `wasmcloud:postgres` codec does:
|
|
102
|
+
* `YYYY-MM-DDTHH:MM:SS.mmmZ`. Microseconds are truncated to milliseconds.
|
|
103
|
+
* `infinity`, `-infinity`, and BC dates are returned unchanged.
|
|
104
|
+
*/
|
|
105
|
+
export declare function pgliteTimestampIso(value: string): string;
|
|
106
|
+
/** Converts a `value` variant into what the `wasmcloud:postgres` codec in `@di-framework/repo` returns. */
|
|
107
|
+
export declare function fromWasmPgValue(value: WasmPgValue): unknown;
|
|
108
|
+
export declare function toWasmPgParams(params?: readonly unknown[]): WasmPgValue[];
|
|
109
|
+
export declare function fromWasmPgRow(row: WasmPgRow): SqlRow;
|
|
110
|
+
/**
|
|
111
|
+
* Rewrites `?` placeholders to `$1`, `$2`, ... in order.
|
|
112
|
+
*
|
|
113
|
+
* Skipped: `'...'` and `E'...'` strings, `"..."` identifiers, `$tag$...$tag$`
|
|
114
|
+
* dollar quotes, `--` and nested `/* *\/` comments, and the `?|` / `?&` jsonb
|
|
115
|
+
* operators. The bare jsonb `?` (key exists) operator cannot be told apart
|
|
116
|
+
* from a placeholder: write `jsonb_exists(col, ?)` instead. The adapter does
|
|
117
|
+
* not rewrite statements bound without parameters, so a literal `?` operator
|
|
118
|
+
* still works there. Mixing `?` with explicit `$n` placeholders throws.
|
|
119
|
+
*/
|
|
120
|
+
export declare function toPgPlaceholders(sql: string): string;
|
|
121
|
+
/**
|
|
122
|
+
* Wraps an open WIT `connection` resource as a `SqlDatabase`.
|
|
123
|
+
*
|
|
124
|
+
* Every statement holds one mutex. `transaction(fn)` runs `begin` … `commit`
|
|
125
|
+
* and calls `rollback` when `fn` throws. Statements issued from inside `fn`,
|
|
126
|
+
* through the callback view or the outer handle, join the transaction; other
|
|
127
|
+
* tasks wait. A nested `transaction()` opens a savepoint: its failure rolls
|
|
128
|
+
* back to the savepoint, and its work commits only with the outer transaction.
|
|
129
|
+
* Sibling savepoints started concurrently run one at a time; a plain statement
|
|
130
|
+
* issued while a sibling savepoint is open joins that savepoint.
|
|
131
|
+
*
|
|
132
|
+
* Inside `fn`, prefer the callback view: while its transaction or savepoint is
|
|
133
|
+
* open it joins from any async context. The outer handle joins only from
|
|
134
|
+
* `fn`'s own async context (tracked with AsyncLocalStorage); from anywhere else
|
|
135
|
+
* it waits for the mutex, which deadlocks if `fn` awaits it.
|
|
136
|
+
*
|
|
137
|
+
* Sibling savepoints are queued on their parent. `transaction()` on a view,
|
|
138
|
+
* called from an async context outside an open sibling savepoint, waits for
|
|
139
|
+
* that sibling to finish; if the sibling awaits that call, it waits forever.
|
|
140
|
+
*
|
|
141
|
+
* Joining lasts only while a transaction is open. A savepoint view kept after
|
|
142
|
+
* its savepoint ends joins the nearest open ancestor, so its work commits or
|
|
143
|
+
* rolls back with that ancestor. Once the root transaction has ended, a
|
|
144
|
+
* retained view, like a timer scheduled inside `fn`, takes the mutex like any
|
|
145
|
+
* other caller.
|
|
146
|
+
*/
|
|
147
|
+
export declare function createSqlDatabaseFromWasmPgliteConnection(connection: WasmPgliteConnection): SqlDatabase;
|
|
148
|
+
export interface WasmPgliteDatabaseOptions extends WasmPgliteOpenOptions {
|
|
149
|
+
/** Pre-loaded module; skips the dynamic import of the WIT interface. */
|
|
150
|
+
module?: WasmPgliteModule;
|
|
151
|
+
/** Custom loader for the WIT interface module (primarily for tests). */
|
|
152
|
+
loadModule?: () => Promise<WasmPgliteModule>;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Imports `di-framework:pglite/database@0.2.0`, provided by the composed provider component.
|
|
156
|
+
*
|
|
157
|
+
* The module's types come from `wit.d.ts`, generated from `wit/world.wit`. The
|
|
158
|
+
* conversion below has no cast, so a WIT change that breaks the adapter's view
|
|
159
|
+
* of the interface fails the typecheck. The check covers method names, arity
|
|
160
|
+
* and result types, not the parameter variants (see the TODO on
|
|
161
|
+
* `WasmPgliteConnection`).
|
|
162
|
+
*/
|
|
163
|
+
export declare function loadWasmPgliteModule(): Promise<WasmPgliteModule>;
|
|
164
|
+
/**
|
|
165
|
+
* Opens a persistent PGlite database root through the provider component and
|
|
166
|
+
* adapts it to `SqlDatabase`. `path` is the root directory inside a writable
|
|
167
|
+
* WASI preopen (for example `/data/identity`); an empty path selects
|
|
168
|
+
* {@link WASM_PGLITE_DEFAULT_ROOT}. The provider allows one open connection per
|
|
169
|
+
* component instance, so keep one handle and share it.
|
|
170
|
+
*/
|
|
171
|
+
export declare function createWasmPgliteDatabase(path?: string, options?: WasmPgliteDatabaseOptions): Promise<SqlDatabase>;
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SqlDatabase` adapter for the `di-framework:pglite/database@0.2.0` WIT
|
|
3
|
+
* import: embedded PostgreSQL 17 bundled into a provider component. The guest
|
|
4
|
+
* exchanges `value` variants over one exclusive connection, so every call is
|
|
5
|
+
* serialized through an async mutex.
|
|
6
|
+
*
|
|
7
|
+
* Repositories and migrations write `?` placeholders; they are rewritten to
|
|
8
|
+
* PostgreSQL's `$1`, `$2`, ... (see {@link toPgPlaceholders}). Transactions use
|
|
9
|
+
* the provider's `begin`/`commit`/`rollback`, and nested `transaction()` calls
|
|
10
|
+
* open savepoints. Result values match the `wasmcloud:postgres` codec in
|
|
11
|
+
* `@di-framework/repo` (`pgScalar`): JSON is parsed, `timestamptz` becomes an ISO string with
|
|
12
|
+
* milliseconds, and an `int8` outside the safe integer range becomes a decimal
|
|
13
|
+
* string. A Postgres error keeps its SQLSTATE in `error.code`.
|
|
14
|
+
*
|
|
15
|
+
* Component bindings differ in how they surface `result<T, error>`: some
|
|
16
|
+
* return `{ tag: 'ok' | 'err', val }` records, others throw an error object
|
|
17
|
+
* carrying the variant as `payload`. Both shapes are normalized here.
|
|
18
|
+
*/
|
|
19
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
20
|
+
import { AsyncMutex, guardedStatements, isWasmResult, leaseGuard, SQL_DATABASE_BRAND, wasmBlobBytes, } from '@di-framework/repo';
|
|
21
|
+
export const WASM_PGLITE_MODULE_ID = 'di-framework:pglite/database@0.2.0';
|
|
22
|
+
/** Database root the provider selects when `open` receives an empty path. */
|
|
23
|
+
export const WASM_PGLITE_DEFAULT_ROOT = '/data/pglite';
|
|
24
|
+
/**
|
|
25
|
+
* A provider error. `code` is the SQLSTATE for `execution-failed` (for example
|
|
26
|
+
* `23505` for a unique violation). The message then starts with
|
|
27
|
+
* `PostgreSQL <code>`, the format `isUniqueViolation` from `@di-framework/repo` reads.
|
|
28
|
+
*/
|
|
29
|
+
export class WasmPgliteError extends Error {
|
|
30
|
+
tag;
|
|
31
|
+
detail;
|
|
32
|
+
code;
|
|
33
|
+
constructor(payload, cause) {
|
|
34
|
+
let detail;
|
|
35
|
+
let code;
|
|
36
|
+
if (typeof payload.val === 'string') {
|
|
37
|
+
detail = payload.val;
|
|
38
|
+
}
|
|
39
|
+
else if (typeof payload.val === 'object' && payload.val !== null) {
|
|
40
|
+
detail = payload.val.message;
|
|
41
|
+
code = payload.val.code || undefined;
|
|
42
|
+
}
|
|
43
|
+
super(wasmPgliteErrorMessage(payload.tag, code, detail), { cause });
|
|
44
|
+
this.name = 'WasmPgliteError';
|
|
45
|
+
this.tag = payload.tag;
|
|
46
|
+
this.detail = detail;
|
|
47
|
+
this.code = code;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function wasmPgliteErrorMessage(tag, code, detail) {
|
|
51
|
+
if (code)
|
|
52
|
+
return detail ? `PostgreSQL ${code} ${detail}` : `PostgreSQL ${code}`;
|
|
53
|
+
return detail ? `PGlite ${tag}: ${detail}` : `PGlite ${tag}`;
|
|
54
|
+
}
|
|
55
|
+
function isErrorPayload(value) {
|
|
56
|
+
return (typeof value === 'object' &&
|
|
57
|
+
value !== null &&
|
|
58
|
+
typeof value.tag === 'string' &&
|
|
59
|
+
value.tag !== 'ok');
|
|
60
|
+
}
|
|
61
|
+
/** Normalizes a thrown binding error or an `err` payload into an `Error`. */
|
|
62
|
+
export function normalizeWasmPgliteError(error) {
|
|
63
|
+
if (error instanceof WasmPgliteError)
|
|
64
|
+
return error;
|
|
65
|
+
if (isErrorPayload(error))
|
|
66
|
+
return new WasmPgliteError(error);
|
|
67
|
+
if (typeof error === 'object' && error !== null) {
|
|
68
|
+
const payload = error.payload;
|
|
69
|
+
if (isErrorPayload(payload))
|
|
70
|
+
return new WasmPgliteError(payload, error);
|
|
71
|
+
}
|
|
72
|
+
if (typeof error === 'string')
|
|
73
|
+
return new WasmPgliteError({ tag: 'other', val: error });
|
|
74
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
75
|
+
}
|
|
76
|
+
/** Unwraps `{ tag: 'ok' | 'err' }` records; passes bare values through. */
|
|
77
|
+
export function unwrapWasmPgliteResult(value) {
|
|
78
|
+
if (isWasmResult(value)) {
|
|
79
|
+
if (value.tag === 'err')
|
|
80
|
+
throw normalizeWasmPgliteError(value.val);
|
|
81
|
+
return value.val;
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
|
|
86
|
+
const MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
|
|
87
|
+
/**
|
|
88
|
+
* Converts a bound JavaScript parameter into a `value` variant.
|
|
89
|
+
*
|
|
90
|
+
* - `bigint` in the safe integer range becomes `integer`; a larger one is sent
|
|
91
|
+
* as `text` (OID 0), so PostgreSQL parses it for an `int8`/`numeric` column
|
|
92
|
+
* without rounding through a double.
|
|
93
|
+
* - A `number` that is an integer outside the safe range throws a `TypeError`
|
|
94
|
+
* (pass a `bigint` or string for `int8`); other non-safe numbers, including
|
|
95
|
+
* NaN and Infinity, are `real` (`float8`).
|
|
96
|
+
* - `Date` becomes `timestamp` (RFC 3339 UTC); plain objects and arrays become
|
|
97
|
+
* `json`. Arrays are not PostgreSQL arrays: pass a literal such as `'{a,b}'`.
|
|
98
|
+
*/
|
|
99
|
+
export function toWasmPgValue(value) {
|
|
100
|
+
if (value === null || value === undefined)
|
|
101
|
+
return { tag: 'null' };
|
|
102
|
+
switch (typeof value) {
|
|
103
|
+
case 'bigint':
|
|
104
|
+
// componentize-qjs lowers WIT s64 from JS numbers (f64), not BigInt (#153).
|
|
105
|
+
if (value >= MIN_SAFE && value <= MAX_SAFE) {
|
|
106
|
+
return { tag: 'integer', val: Number(value) };
|
|
107
|
+
}
|
|
108
|
+
return { tag: 'text', val: value.toString() };
|
|
109
|
+
case 'number':
|
|
110
|
+
if (Number.isSafeInteger(value)) {
|
|
111
|
+
return { tag: 'integer', val: value };
|
|
112
|
+
}
|
|
113
|
+
if (Number.isInteger(value)) {
|
|
114
|
+
// An integral double past 2^53 has already lost its low bits, and the
|
|
115
|
+
// column is most likely int8; sending float8 would fail or round silently.
|
|
116
|
+
throw new TypeError(`Cannot bind ${value} as a PostgreSQL parameter: it is an integer outside the safe range. Pass a bigint or a decimal string for int8 (or a string such as '1e300' for float8)`);
|
|
117
|
+
}
|
|
118
|
+
// Fractions, NaN and +/-Infinity are valid float8 values in PostgreSQL.
|
|
119
|
+
return { tag: 'real', val: value };
|
|
120
|
+
case 'boolean':
|
|
121
|
+
return { tag: 'boolean', val: value };
|
|
122
|
+
case 'string':
|
|
123
|
+
return { tag: 'text', val: value };
|
|
124
|
+
case 'object':
|
|
125
|
+
return toWasmPgObject(value);
|
|
126
|
+
}
|
|
127
|
+
throw new TypeError(`Cannot bind ${typeof value} as a PostgreSQL parameter; use null, number, bigint, boolean, string, Date, Uint8Array, or a JSON object`);
|
|
128
|
+
}
|
|
129
|
+
function toWasmPgObject(value) {
|
|
130
|
+
const bytes = wasmBlobBytes(value);
|
|
131
|
+
if (bytes)
|
|
132
|
+
return { tag: 'blob', val: bytes };
|
|
133
|
+
if (!(value instanceof Date))
|
|
134
|
+
return { tag: 'json', val: JSON.stringify(value) };
|
|
135
|
+
if (Number.isNaN(value.getTime())) {
|
|
136
|
+
throw new TypeError('Cannot bind an invalid Date as a PostgreSQL parameter');
|
|
137
|
+
}
|
|
138
|
+
return { tag: 'timestamp', val: value.toISOString() };
|
|
139
|
+
}
|
|
140
|
+
const RFC3339_UTC = /^(\d{4,}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?Z$/;
|
|
141
|
+
/**
|
|
142
|
+
* Formats a `timestamp` variant the way the `wasmcloud:postgres` codec does:
|
|
143
|
+
* `YYYY-MM-DDTHH:MM:SS.mmmZ`. Microseconds are truncated to milliseconds.
|
|
144
|
+
* `infinity`, `-infinity`, and BC dates are returned unchanged.
|
|
145
|
+
*/
|
|
146
|
+
export function pgliteTimestampIso(value) {
|
|
147
|
+
const match = RFC3339_UTC.exec(value);
|
|
148
|
+
if (!match)
|
|
149
|
+
return value;
|
|
150
|
+
const millis = (match[2] ?? '').padEnd(3, '0').slice(0, 3);
|
|
151
|
+
return `${match[1]}.${millis}Z`;
|
|
152
|
+
}
|
|
153
|
+
/** Converts a `value` variant into what the `wasmcloud:postgres` codec in `@di-framework/repo` returns. */
|
|
154
|
+
export function fromWasmPgValue(value) {
|
|
155
|
+
switch (value.tag) {
|
|
156
|
+
case 'null':
|
|
157
|
+
return null;
|
|
158
|
+
case 'integer': {
|
|
159
|
+
if (typeof value.val === 'number')
|
|
160
|
+
return value.val;
|
|
161
|
+
return value.val >= MIN_SAFE && value.val <= MAX_SAFE
|
|
162
|
+
? Number(value.val)
|
|
163
|
+
: value.val.toString();
|
|
164
|
+
}
|
|
165
|
+
case 'real':
|
|
166
|
+
case 'text':
|
|
167
|
+
case 'boolean':
|
|
168
|
+
return value.val;
|
|
169
|
+
case 'blob':
|
|
170
|
+
return value.val instanceof Uint8Array ? value.val : Uint8Array.from(value.val);
|
|
171
|
+
case 'json':
|
|
172
|
+
try {
|
|
173
|
+
return JSON.parse(value.val);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return value.val;
|
|
177
|
+
}
|
|
178
|
+
case 'timestamp':
|
|
179
|
+
return pgliteTimestampIso(value.val);
|
|
180
|
+
default:
|
|
181
|
+
throw new WasmPgliteError({
|
|
182
|
+
tag: 'value-conversion-failed',
|
|
183
|
+
val: `Unknown value tag ${String(value.tag)}`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
export function toWasmPgParams(params = []) {
|
|
188
|
+
return params.map((param) => toWasmPgValue(param));
|
|
189
|
+
}
|
|
190
|
+
export function fromWasmPgRow(row) {
|
|
191
|
+
const result = {};
|
|
192
|
+
for (const [column, value] of row) {
|
|
193
|
+
result[column] = fromWasmPgValue(value);
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
const IDENTIFIER_CHAR = /[\w$]/;
|
|
198
|
+
const DOLLAR_TAG = /^\$(?:[A-Za-z_]\w*)?\$/;
|
|
199
|
+
const POSITIONAL = /^\$\d+/;
|
|
200
|
+
function skipQuoted(sql, start, quote, backslashes) {
|
|
201
|
+
let index = start + 1;
|
|
202
|
+
while (index < sql.length) {
|
|
203
|
+
const char = sql[index];
|
|
204
|
+
if (backslashes && char === '\\') {
|
|
205
|
+
index += 2;
|
|
206
|
+
}
|
|
207
|
+
else if (char === quote) {
|
|
208
|
+
if (sql[index + 1] !== quote)
|
|
209
|
+
return index + 1;
|
|
210
|
+
index += 2;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
index++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return sql.length;
|
|
217
|
+
}
|
|
218
|
+
function skipBlockComment(sql, start) {
|
|
219
|
+
let depth = 0;
|
|
220
|
+
let index = start;
|
|
221
|
+
while (index < sql.length) {
|
|
222
|
+
if (sql.startsWith('/*', index)) {
|
|
223
|
+
depth++;
|
|
224
|
+
index += 2;
|
|
225
|
+
}
|
|
226
|
+
else if (sql.startsWith('*/', index)) {
|
|
227
|
+
depth--;
|
|
228
|
+
index += 2;
|
|
229
|
+
if (depth === 0)
|
|
230
|
+
return index;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
index++;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return sql.length;
|
|
237
|
+
}
|
|
238
|
+
function isEscapeString(sql, quote) {
|
|
239
|
+
const prefix = sql[quote - 1];
|
|
240
|
+
return (prefix === 'E' || prefix === 'e') && !IDENTIFIER_CHAR.test(sql[quote - 2] ?? '');
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Rewrites `?` placeholders to `$1`, `$2`, ... in order.
|
|
244
|
+
*
|
|
245
|
+
* Skipped: `'...'` and `E'...'` strings, `"..."` identifiers, `$tag$...$tag$`
|
|
246
|
+
* dollar quotes, `--` and nested `/* *\/` comments, and the `?|` / `?&` jsonb
|
|
247
|
+
* operators. The bare jsonb `?` (key exists) operator cannot be told apart
|
|
248
|
+
* from a placeholder: write `jsonb_exists(col, ?)` instead. The adapter does
|
|
249
|
+
* not rewrite statements bound without parameters, so a literal `?` operator
|
|
250
|
+
* still works there. Mixing `?` with explicit `$n` placeholders throws.
|
|
251
|
+
*/
|
|
252
|
+
export function toPgPlaceholders(sql) {
|
|
253
|
+
let out = '';
|
|
254
|
+
let index = 0;
|
|
255
|
+
let count = 0;
|
|
256
|
+
let positional = false;
|
|
257
|
+
while (index < sql.length) {
|
|
258
|
+
let end = skippedTokenEnd(sql, index);
|
|
259
|
+
if (end === undefined && sql[index] === '$') {
|
|
260
|
+
const dollar = dollarTokenEnd(sql, index);
|
|
261
|
+
end = dollar.end;
|
|
262
|
+
positional ||= dollar.positional;
|
|
263
|
+
}
|
|
264
|
+
if (end === undefined && sql[index] === '?') {
|
|
265
|
+
out += `$${++count}`;
|
|
266
|
+
index++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
end ??= index + 1;
|
|
270
|
+
out += sql.slice(index, end);
|
|
271
|
+
index = end;
|
|
272
|
+
}
|
|
273
|
+
if (count > 0 && positional) {
|
|
274
|
+
throw new TypeError('Cannot mix ? placeholders with $n placeholders in one statement');
|
|
275
|
+
}
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
/** End of the string, identifier, comment, or `?|`/`?&` operator at `index`. */
|
|
279
|
+
function skippedTokenEnd(sql, index) {
|
|
280
|
+
const char = sql[index];
|
|
281
|
+
const next = sql[index + 1];
|
|
282
|
+
if (char === "'")
|
|
283
|
+
return skipQuoted(sql, index, "'", isEscapeString(sql, index));
|
|
284
|
+
if (char === '"')
|
|
285
|
+
return skipQuoted(sql, index, '"', false);
|
|
286
|
+
if (char === '-' && next === '-') {
|
|
287
|
+
const newline = sql.indexOf('\n', index);
|
|
288
|
+
return newline < 0 ? sql.length : newline;
|
|
289
|
+
}
|
|
290
|
+
if (char === '/' && next === '*')
|
|
291
|
+
return skipBlockComment(sql, index);
|
|
292
|
+
if (char === '?' && (next === '|' || next === '&'))
|
|
293
|
+
return index + 2;
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
/** End of the `$tag$...$tag$` body or `$n` placeholder that starts at `index`. */
|
|
297
|
+
function dollarTokenEnd(sql, index) {
|
|
298
|
+
if (IDENTIFIER_CHAR.test(sql[index - 1] ?? ''))
|
|
299
|
+
return { end: index + 1, positional: false };
|
|
300
|
+
const rest = sql.slice(index);
|
|
301
|
+
const tag = DOLLAR_TAG.exec(rest)?.[0];
|
|
302
|
+
if (tag) {
|
|
303
|
+
const close = sql.indexOf(tag, index + tag.length);
|
|
304
|
+
return { end: close < 0 ? sql.length : close + tag.length, positional: false };
|
|
305
|
+
}
|
|
306
|
+
const number = POSITIONAL.exec(rest)?.[0];
|
|
307
|
+
if (number)
|
|
308
|
+
return { end: index + number.length, positional: true };
|
|
309
|
+
return { end: index + 1, positional: false };
|
|
310
|
+
}
|
|
311
|
+
async function call(fn) {
|
|
312
|
+
try {
|
|
313
|
+
return unwrapWasmPgliteResult(await fn());
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
throw normalizeWasmPgliteError(error);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function descendsFrom(scope, ancestor) {
|
|
320
|
+
for (let current = scope.parent; current; current = current.parent) {
|
|
321
|
+
if (current === ancestor)
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Wraps an open WIT `connection` resource as a `SqlDatabase`.
|
|
328
|
+
*
|
|
329
|
+
* Every statement holds one mutex. `transaction(fn)` runs `begin` … `commit`
|
|
330
|
+
* and calls `rollback` when `fn` throws. Statements issued from inside `fn`,
|
|
331
|
+
* through the callback view or the outer handle, join the transaction; other
|
|
332
|
+
* tasks wait. A nested `transaction()` opens a savepoint: its failure rolls
|
|
333
|
+
* back to the savepoint, and its work commits only with the outer transaction.
|
|
334
|
+
* Sibling savepoints started concurrently run one at a time; a plain statement
|
|
335
|
+
* issued while a sibling savepoint is open joins that savepoint.
|
|
336
|
+
*
|
|
337
|
+
* Inside `fn`, prefer the callback view: while its transaction or savepoint is
|
|
338
|
+
* open it joins from any async context. The outer handle joins only from
|
|
339
|
+
* `fn`'s own async context (tracked with AsyncLocalStorage); from anywhere else
|
|
340
|
+
* it waits for the mutex, which deadlocks if `fn` awaits it.
|
|
341
|
+
*
|
|
342
|
+
* Sibling savepoints are queued on their parent. `transaction()` on a view,
|
|
343
|
+
* called from an async context outside an open sibling savepoint, waits for
|
|
344
|
+
* that sibling to finish; if the sibling awaits that call, it waits forever.
|
|
345
|
+
*
|
|
346
|
+
* Joining lasts only while a transaction is open. A savepoint view kept after
|
|
347
|
+
* its savepoint ends joins the nearest open ancestor, so its work commits or
|
|
348
|
+
* rolls back with that ancestor. Once the root transaction has ended, a
|
|
349
|
+
* retained view, like a timer scheduled inside `fn`, takes the mutex like any
|
|
350
|
+
* other caller.
|
|
351
|
+
*/
|
|
352
|
+
export function createSqlDatabaseFromWasmPgliteConnection(connection) {
|
|
353
|
+
const mutex = new AsyncMutex();
|
|
354
|
+
// The outer handle joins a running root transaction from its own async
|
|
355
|
+
// context for as long as that transaction's lease is active. The lease ends
|
|
356
|
+
// before COMMIT/ROLLBACK is issued (see `enter`), unlike `contextLock`, which
|
|
357
|
+
// would keep it until the holder returns.
|
|
358
|
+
const held = new AsyncLocalStorage();
|
|
359
|
+
const locked = (fn) => (held.getStore()?.active ? fn() : mutex.run(fn));
|
|
360
|
+
// Statements a transaction's callers issue concurrently reach the connection
|
|
361
|
+
// one at a time, even when the binding returns promises (see `leaseGuard`).
|
|
362
|
+
const wire = new AsyncMutex();
|
|
363
|
+
const invoke = (fn) => wire.run(() => call(fn));
|
|
364
|
+
const activeScope = new AsyncLocalStorage();
|
|
365
|
+
let savepoints = 0;
|
|
366
|
+
const bind = (sql, params) => {
|
|
367
|
+
const values = toWasmPgParams(params);
|
|
368
|
+
return { sql: values.length > 0 ? toPgPlaceholders(sql) : sql, values };
|
|
369
|
+
};
|
|
370
|
+
const calls = {
|
|
371
|
+
run: async (sql, params) => {
|
|
372
|
+
const bound = bind(sql, params);
|
|
373
|
+
const changes = await invoke(() => connection.run(bound.sql, bound.values));
|
|
374
|
+
return { changes: Number(changes ?? 0) };
|
|
375
|
+
},
|
|
376
|
+
query: async (sql, params) => {
|
|
377
|
+
const bound = bind(sql, params);
|
|
378
|
+
const rows = await invoke(() => connection.query(bound.sql, bound.values));
|
|
379
|
+
return rows.map((row) => fromWasmPgRow(row));
|
|
380
|
+
},
|
|
381
|
+
first: async (sql, params) => {
|
|
382
|
+
const bound = bind(sql, params);
|
|
383
|
+
const row = await invoke(() => connection.first(bound.sql, bound.values));
|
|
384
|
+
return row ? fromWasmPgRow(row) : null;
|
|
385
|
+
},
|
|
386
|
+
exec: async (sql) => {
|
|
387
|
+
await invoke(() => connection.exec(sql));
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
const statementsVia = (guard) => ({
|
|
391
|
+
[SQL_DATABASE_BRAND]: true,
|
|
392
|
+
...guardedStatements(guard, calls),
|
|
393
|
+
});
|
|
394
|
+
/** The innermost transaction or savepoint of this context that is still open. */
|
|
395
|
+
const liveScope = () => {
|
|
396
|
+
let scope = activeScope.getStore();
|
|
397
|
+
while (scope && !scope.lease.active)
|
|
398
|
+
scope = scope.parent;
|
|
399
|
+
return scope;
|
|
400
|
+
};
|
|
401
|
+
const enter = async (scope, fn, open, close, undo) => {
|
|
402
|
+
try {
|
|
403
|
+
await invoke(open);
|
|
404
|
+
try {
|
|
405
|
+
const result = await activeScope.run(scope, () => fn(scope.view));
|
|
406
|
+
// End the lease before COMMIT / RELEASE is issued: a statement that
|
|
407
|
+
// arrives late waits for the mutex and runs after the transaction,
|
|
408
|
+
// instead of reaching the wire between the close and the end of the lease.
|
|
409
|
+
scope.lease.active = false;
|
|
410
|
+
await invoke(close);
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
scope.lease.active = false;
|
|
415
|
+
try {
|
|
416
|
+
await invoke(undo);
|
|
417
|
+
}
|
|
418
|
+
catch { }
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
scope.lease.active = false;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
const nest = (parent, fn) => {
|
|
427
|
+
const current = liveScope();
|
|
428
|
+
if (current && descendsFrom(current, parent))
|
|
429
|
+
return nest(current, fn);
|
|
430
|
+
return parent.children.run(() => {
|
|
431
|
+
const name = `di_sp_${++savepoints}`;
|
|
432
|
+
return enter(createScope(parent), fn, () => connection.savepoint(name), () => connection.releaseSavepoint(name), () => connection.rollbackToSavepoint(name));
|
|
433
|
+
});
|
|
434
|
+
};
|
|
435
|
+
// A view runs directly while its scope is open, from any async context: the
|
|
436
|
+
// scope's lease is cleared before the mutex is released. Once a savepoint has
|
|
437
|
+
// ended, its view falls back to the parent scope, which is still open and
|
|
438
|
+
// may be awaiting the caller; taking the root mutex there would deadlock. A
|
|
439
|
+
// root view, whose transaction holds that mutex, takes it like the outer handle.
|
|
440
|
+
const createScope = (parent, lease = { active: true }) => {
|
|
441
|
+
const guard = leaseGuard(lease, parent ? parent.guard : locked);
|
|
442
|
+
const scope = {
|
|
443
|
+
parent,
|
|
444
|
+
children: new AsyncMutex(),
|
|
445
|
+
lease,
|
|
446
|
+
guard,
|
|
447
|
+
view: {
|
|
448
|
+
...statementsVia(guard),
|
|
449
|
+
// A view kept past its scope opens its transaction on the parent scope
|
|
450
|
+
// (or, for a root view, a fresh transaction).
|
|
451
|
+
transaction: (fn) => lease.active
|
|
452
|
+
? nest(scope, fn)
|
|
453
|
+
: parent
|
|
454
|
+
? parent.view.transaction(fn)
|
|
455
|
+
: database.transaction(fn),
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
return scope;
|
|
459
|
+
};
|
|
460
|
+
const database = {
|
|
461
|
+
...statementsVia(locked),
|
|
462
|
+
transaction: async (fn) => {
|
|
463
|
+
const current = liveScope();
|
|
464
|
+
if (current)
|
|
465
|
+
return nest(current, fn);
|
|
466
|
+
return mutex.run(() => {
|
|
467
|
+
// The root scope's lease doubles as the outer handle's join lease.
|
|
468
|
+
const lease = { active: true };
|
|
469
|
+
return held.run(lease, () => enter(createScope(undefined, lease), fn, () => connection.begin(), () => connection.commit(), () => connection.rollback()));
|
|
470
|
+
});
|
|
471
|
+
},
|
|
472
|
+
close: () => locked(() => invoke(() => connection.close())),
|
|
473
|
+
};
|
|
474
|
+
return database;
|
|
475
|
+
}
|
|
476
|
+
let cachedModule;
|
|
477
|
+
/**
|
|
478
|
+
* Imports `di-framework:pglite/database@0.2.0`, provided by the composed provider component.
|
|
479
|
+
*
|
|
480
|
+
* The module's types come from `wit.d.ts`, generated from `wit/world.wit`. The
|
|
481
|
+
* conversion below has no cast, so a WIT change that breaks the adapter's view
|
|
482
|
+
* of the interface fails the typecheck. The check covers method names, arity
|
|
483
|
+
* and result types, not the parameter variants (see the TODO on
|
|
484
|
+
* `WasmPgliteConnection`).
|
|
485
|
+
*/
|
|
486
|
+
export function loadWasmPgliteModule() {
|
|
487
|
+
cachedModule ??= import('di-framework:pglite/database@0.2.0')
|
|
488
|
+
.then((module) => module)
|
|
489
|
+
.catch((error) => {
|
|
490
|
+
cachedModule = undefined;
|
|
491
|
+
throw new Error(`The ${WASM_PGLITE_MODULE_ID} import is unavailable in this runtime; compose the component with the di-framework PGlite provider`, { cause: error });
|
|
492
|
+
});
|
|
493
|
+
return cachedModule;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Opens a persistent PGlite database root through the provider component and
|
|
497
|
+
* adapts it to `SqlDatabase`. `path` is the root directory inside a writable
|
|
498
|
+
* WASI preopen (for example `/data/identity`); an empty path selects
|
|
499
|
+
* {@link WASM_PGLITE_DEFAULT_ROOT}. The provider allows one open connection per
|
|
500
|
+
* component instance, so keep one handle and share it.
|
|
501
|
+
*/
|
|
502
|
+
export async function createWasmPgliteDatabase(path = '', options = {}) {
|
|
503
|
+
const { module: providedModule, loadModule, ...openOptions } = options;
|
|
504
|
+
const module = providedModule ?? (await (loadModule ?? loadWasmPgliteModule)());
|
|
505
|
+
const resolvedOpenOptions = {
|
|
506
|
+
...(openOptions.database !== undefined ? { database: openOptions.database } : {}),
|
|
507
|
+
...(openOptions.statementTimeoutMs !== undefined
|
|
508
|
+
? { statementTimeoutMs: openOptions.statementTimeoutMs }
|
|
509
|
+
: {}),
|
|
510
|
+
};
|
|
511
|
+
const hasOptions = Object.keys(resolvedOpenOptions).length > 0;
|
|
512
|
+
const connection = await call(() => module.open(path, hasOptions ? resolvedOpenOptions : undefined));
|
|
513
|
+
return createSqlDatabaseFromWasmPgliteConnection(connection);
|
|
514
|
+
}
|