turbine-orm 0.21.0 → 0.23.0
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 +19 -2
- package/dist/cjs/client.js +11 -2
- package/dist/cjs/generate.js +4 -0
- package/dist/cjs/introspect.js +6 -0
- package/dist/cjs/powdb.js +865 -0
- package/dist/cjs/powql.js +1262 -0
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.js +11 -2
- package/dist/generate.js +4 -0
- package/dist/introspect.js +6 -0
- package/dist/powdb.d.ts +341 -0
- package/dist/powdb.js +816 -0
- package/dist/powql.d.ts +205 -0
- package/dist/powql.js +1226 -0
- package/dist/query/builder.d.ts +8 -0
- package/dist/schema.d.ts +10 -0
- package/package.json +16 -1
package/dist/powdb.js
ADDED
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm/powdb — Turbine's PowDB / PowQL backend.
|
|
3
|
+
*
|
|
4
|
+
* PowDB is a single-node embedded database with its own query language, **PowQL**
|
|
5
|
+
* (not SQL), reached over `@zvndev/powdb-client`'s binary TCP protocol. PowDB is a
|
|
6
|
+
* different shape than the SQL engines, so this module does NOT route through the
|
|
7
|
+
* SQL `Dialect` / `QueryInterface`: it ships a parallel {@link PowqlInterface} that
|
|
8
|
+
* generates PowQL, plugged into `TurbineClient` via the `queryInterfaceFactory`
|
|
9
|
+
* seam. The four SQL engines are untouched.
|
|
10
|
+
*
|
|
11
|
+
* PowDB realities shape the design (all verified firsthand against a live
|
|
12
|
+
* `powdb-server` / the embedded addon, see `docs/strategy/powdb-parity-matrix.md`):
|
|
13
|
+
* - **`RETURNING` (since 0.7.0)** — `create/createMany/update/delete` append the
|
|
14
|
+
* trailing `returning` keyword (`RETURNING *`, all columns) and read the
|
|
15
|
+
* affected rows back in one round-trip. `upsert` is the lone exception (its
|
|
16
|
+
* statement rejects `returning`) and reselects by primary key.
|
|
17
|
+
* - **No generated IDs** — the app must supply every value → Turbine generates a
|
|
18
|
+
* client-side UUID for the primary key when it has a default.
|
|
19
|
+
* - **`uuid`/`datetime`/`bytes` columns can't hold client-supplied values** (no
|
|
20
|
+
* literal, no working cast on the wire) → Turbine maps everything onto the four
|
|
21
|
+
* writable types (`str`/`int`/`float`/`bool`); `Date` → `int` epoch micros;
|
|
22
|
+
* `string` PKs hold UUID strings.
|
|
23
|
+
* - **No JSON aggregation / link navigation** — single-query nested `with` is
|
|
24
|
+
* impossible → it degrades to batched N+1 loaders (Phase B).
|
|
25
|
+
* - **Single global write lock; no savepoints/isolation/pipelining** — nested
|
|
26
|
+
* transactions / isolation / vector / LISTEN-NOTIFY / RLS throw.
|
|
27
|
+
*
|
|
28
|
+
* `@zvndev/powdb-client` is an **optional peer dependency** loaded by dynamic
|
|
29
|
+
* import; `npm i turbine-orm` still pulls only `pg`.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { turbinePowDB } from 'turbine-orm/powdb';
|
|
34
|
+
* import { SCHEMA } from './generated/turbine/metadata.js';
|
|
35
|
+
*
|
|
36
|
+
* const db = await turbinePowDB({ host: '127.0.0.1', port: 5433 }, SCHEMA);
|
|
37
|
+
* const user = await db.table('users').create({ data: { name: 'Ada' } }); // UUID id auto-generated
|
|
38
|
+
* const found = await db.table('users').findMany({ where: { name: 'Ada' }, limit: 10 });
|
|
39
|
+
* await db.disconnect();
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @module
|
|
43
|
+
*/
|
|
44
|
+
import { TurbineClient, } from './client.js';
|
|
45
|
+
import { postgresDialect } from './dialect.js';
|
|
46
|
+
import { ConnectionError, NotNullViolationError, TimeoutError, UniqueConstraintError, UnsupportedFeatureError, ValidationError, } from './errors.js';
|
|
47
|
+
/**
|
|
48
|
+
* Capability descriptor for PowDB. PowQL generation is owned by
|
|
49
|
+
* {@link PowqlInterface} (not the SQL `Dialect`), so this dialect exists only to
|
|
50
|
+
* drive `TurbineClient`'s capability gating and transaction keywords:
|
|
51
|
+
* - `supports*` flags are all `false` for the Postgres-only features, so
|
|
52
|
+
* `$listen`/`$notify`, RLS `sessionContext`/`$withSession`, and pgvector
|
|
53
|
+
* throw a clear {@link UnsupportedFeatureError} (E017) at the client surface
|
|
54
|
+
* instead of emitting SQL that PowDB cannot parse.
|
|
55
|
+
* - `begin`/`commit`/`rollback` are lowercase PowQL keywords (verified on the
|
|
56
|
+
* wire) so a single-level `$transaction` works.
|
|
57
|
+
* - PowDB has a single global write lock and supports neither savepoints nor
|
|
58
|
+
* nested/concurrent transactions. The `savepoint*` keywords therefore throw
|
|
59
|
+
* {@link UnsupportedFeatureError} (E017): a nested `tx.$transaction` emits a
|
|
60
|
+
* savepoint synchronously (before any DB call) and so fails fast with a
|
|
61
|
+
* clear typed error instead of leaking PowDB's cryptic `Parse(... 'sp_1')`.
|
|
62
|
+
* The pool-level begin-while-active guard (see {@link PowdbPool}) catches the
|
|
63
|
+
* other re-entrant shape — a fresh top-level `db.$transaction` opened inside
|
|
64
|
+
* an already-open one — before it can deadlock on the write lock.
|
|
65
|
+
* Isolation levels remain Phase B.
|
|
66
|
+
*/
|
|
67
|
+
export const powdbDialect = {
|
|
68
|
+
...postgresDialect,
|
|
69
|
+
name: 'powdb',
|
|
70
|
+
// `resultStrategy` is decorative for PowDB — PowqlInterface owns its own write
|
|
71
|
+
// path and never reads it. Set to 'returning' for honesty: writes use PowDB
|
|
72
|
+
// 0.7.0's trailing `returning` keyword (upsert excepted — see PowqlInterface).
|
|
73
|
+
resultStrategy: 'returning',
|
|
74
|
+
supportsReturning: true,
|
|
75
|
+
supportsVector: false,
|
|
76
|
+
supportsListenNotify: false,
|
|
77
|
+
supportsRLS: false,
|
|
78
|
+
supportsAdvisoryLock: false,
|
|
79
|
+
supportsILike: false,
|
|
80
|
+
beginStatement: () => 'begin',
|
|
81
|
+
commitStatement: () => 'commit',
|
|
82
|
+
rollbackStatement: () => 'rollback',
|
|
83
|
+
// PowDB has no savepoints — a nested `tx.$transaction` would emit one and PowDB
|
|
84
|
+
// rejects it with a cryptic parse error. Throw a clear typed error instead.
|
|
85
|
+
// These run synchronously in TransactionClient.$transaction before any query,
|
|
86
|
+
// so the nested call fails fast with no partial DB state.
|
|
87
|
+
savepointStatement: throwNoNestedTransaction,
|
|
88
|
+
releaseSavepointStatement: throwNoNestedTransaction,
|
|
89
|
+
rollbackToSavepointStatement: throwNoNestedTransaction,
|
|
90
|
+
};
|
|
91
|
+
/** Reject any savepoint (nested-transaction) operation — PowDB is single-writer. */
|
|
92
|
+
function throwNoNestedTransaction() {
|
|
93
|
+
throw new UnsupportedFeatureError('nested transactions', 'powdb', 'PowDB is single-writer — it has one global write lock and no savepoints. ' +
|
|
94
|
+
'Complete the open transaction before starting another; do not nest `$transaction` calls.');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Marker wrapper for a value bound to a `float` column. The networked driver
|
|
98
|
+
* unwraps it to the plain number (the wire param is unchanged), but the
|
|
99
|
+
* *embedded* literal encoder reads it to emit a float-form PowQL literal (`42`
|
|
100
|
+
* → `42.0`) so an integer-valued float column stays unambiguously a float.
|
|
101
|
+
* Constructed in {@link PowqlInterface.param}.
|
|
102
|
+
*/
|
|
103
|
+
export class PowdbFloatParam {
|
|
104
|
+
value;
|
|
105
|
+
constructor(value) {
|
|
106
|
+
this.value = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Minimum PowDB server version the networked transport requires. */
|
|
110
|
+
export const MIN_POWDB_VERSION = '0.7.0';
|
|
111
|
+
/**
|
|
112
|
+
* Parse a `powdb://[user[:pass]@]host[:port][/db]` connection string into
|
|
113
|
+
* {@link PowdbConnOptions} (consistency with `turbineMysql`/`turbineMssql`,
|
|
114
|
+
* which accept a URL). Defaults: host `127.0.0.1`, port `5433`.
|
|
115
|
+
*/
|
|
116
|
+
export function parsePowdbUrl(connectionString) {
|
|
117
|
+
let u;
|
|
118
|
+
try {
|
|
119
|
+
u = new URL(connectionString);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new ConnectionError(`[turbine] Invalid PowDB connection string: "${connectionString}"`);
|
|
123
|
+
}
|
|
124
|
+
if (u.protocol !== 'powdb:') {
|
|
125
|
+
throw new ConnectionError(`[turbine] PowDB connection string must use the powdb:// scheme (got "${u.protocol}//…").`);
|
|
126
|
+
}
|
|
127
|
+
const opts = {
|
|
128
|
+
host: u.hostname || '127.0.0.1',
|
|
129
|
+
port: u.port ? Number(u.port) : 5433,
|
|
130
|
+
};
|
|
131
|
+
if (u.username)
|
|
132
|
+
opts.user = decodeURIComponent(u.username);
|
|
133
|
+
if (u.password)
|
|
134
|
+
opts.password = decodeURIComponent(u.password);
|
|
135
|
+
const db = u.pathname.replace(/^\//, '');
|
|
136
|
+
if (db)
|
|
137
|
+
opts.dbName = decodeURIComponent(db);
|
|
138
|
+
return opts;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Fail fast if a networked PowDB server is older than {@link MIN_POWDB_VERSION}.
|
|
142
|
+
* Turbine's write path relies on the trailing `returning` keyword and the
|
|
143
|
+
* int→float coercion fix, both of which landed in 0.7.0. Embedded exposes no
|
|
144
|
+
* version method, so this is networked-only (the embedded peer is pinned ^0.7.0
|
|
145
|
+
* at install time). A non-semver / empty version string is tolerated (we cannot
|
|
146
|
+
* prove it is too old).
|
|
147
|
+
*/
|
|
148
|
+
export function assertSupportedPowdbVersion(version) {
|
|
149
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(String(version ?? '').trim());
|
|
150
|
+
if (!m)
|
|
151
|
+
return; // unknown / non-semver — don't block
|
|
152
|
+
const [major, minor] = [Number(m[1]), Number(m[2])];
|
|
153
|
+
// 0.7.0 is the floor; >= 0.7 (or any 1.x+) passes.
|
|
154
|
+
if (major > 0 || (major === 0 && minor >= 7))
|
|
155
|
+
return;
|
|
156
|
+
throw new ConnectionError(`[turbine] turbine-orm/powdb requires PowDB >= ${MIN_POWDB_VERSION}; the server reports "${version}". ` +
|
|
157
|
+
'Upgrade the PowDB server (0.7.0 added the `returning` keyword and the int->float coercion fix Turbine relies on).');
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Map a Turbine column to the PowQL DDL type used in `defineSchema` →
|
|
161
|
+
* `type T { … }`. Turbine never emits PowDB's `uuid`/`datetime`/`bytes` types,
|
|
162
|
+
* which cannot hold client-supplied values on the wire (no literal, no cast):
|
|
163
|
+
* - `Date` → `int` (epoch micros) - `boolean` → `bool`
|
|
164
|
+
* - integral `number`/`bigint` → `int` - fractional `number` → `float`
|
|
165
|
+
* - everything else (incl. UUID/PK strings) → `str`
|
|
166
|
+
* Array / JSON / bytes columns throw — they have no PowDB equivalent.
|
|
167
|
+
*/
|
|
168
|
+
export function powqlColumnType(col) {
|
|
169
|
+
if (col.isArray) {
|
|
170
|
+
throw new ValidationError(`[turbine] Column "${col.name}" is an array — PowDB has no array type. Arrays are unsupported on the PowDB backend.`);
|
|
171
|
+
}
|
|
172
|
+
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
173
|
+
if (ts === 'Date')
|
|
174
|
+
return 'int'; // epoch micros
|
|
175
|
+
if (ts === 'boolean')
|
|
176
|
+
return 'bool';
|
|
177
|
+
if (ts === 'number')
|
|
178
|
+
return isFloatColumn(col) ? 'float' : 'int';
|
|
179
|
+
if (ts === 'bigint')
|
|
180
|
+
return 'int';
|
|
181
|
+
if (ts === 'string')
|
|
182
|
+
return 'str';
|
|
183
|
+
if (ts === 'Buffer' || ts === 'Uint8Array') {
|
|
184
|
+
throw new ValidationError(`[turbine] Column "${col.name}" is binary — PowDB cannot store client-supplied bytes on the wire. Use a string (e.g. base64) instead.`);
|
|
185
|
+
}
|
|
186
|
+
if (/Record<|object|unknown|\[\]|\{/.test(ts)) {
|
|
187
|
+
throw new ValidationError(`[turbine] Column "${col.name}" (${col.tsType}) maps to JSON/object, which PowDB has no type for. Flatten it or store a JSON string.`);
|
|
188
|
+
}
|
|
189
|
+
return 'str';
|
|
190
|
+
}
|
|
191
|
+
/** Heuristic: does this numeric column hold fractional values (→ PowQL `float`)? */
|
|
192
|
+
function isFloatColumn(col) {
|
|
193
|
+
const t = (col.dialectType ?? col.pgType ?? '').toLowerCase();
|
|
194
|
+
return /float|double|real|numeric|decimal|money/.test(t);
|
|
195
|
+
}
|
|
196
|
+
/** Is a column stored as `int` epoch micros but surfaced as a JS `Date`? */
|
|
197
|
+
function isDateColumn(col) {
|
|
198
|
+
return col.tsType.replace(/\s*\|\s*null$/i, '').trim() === 'Date';
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Generate PowQL DDL (`type T { … }`) for every table in a schema. Used to
|
|
202
|
+
* provision a PowDB database from a code-first `defineSchema`/`SchemaMetadata`
|
|
203
|
+
* (PowDB has no migration runner yet). The primary key column is declared
|
|
204
|
+
* `required unique`; non-nullable columns are `required`. A server-generated
|
|
205
|
+
* column ({@link ColumnMetadata.isGenerated}) that maps to PowQL `int` gets the
|
|
206
|
+
* `auto` modifier, so PowDB assigns a monotonic id on insert and Turbine stops
|
|
207
|
+
* synthesizing a client-side value for it.
|
|
208
|
+
*/
|
|
209
|
+
export function powqlSchemaDDL(schema) {
|
|
210
|
+
const stmts = [];
|
|
211
|
+
for (const meta of Object.values(schema.tables)) {
|
|
212
|
+
const pkSet = new Set(meta.primaryKey);
|
|
213
|
+
// PowDB's `unique` is single-column only — there is no composite-unique
|
|
214
|
+
// constraint (`add unique` takes one `.column`). So the per-field `unique`
|
|
215
|
+
// modifier is emitted only for a single-column PK; a composite PK (e.g. a
|
|
216
|
+
// m2m junction's `(source_id, target_id)`) marks its columns `required` but
|
|
217
|
+
// cannot enforce the tuple's uniqueness at the engine level.
|
|
218
|
+
const pkIsSingle = meta.primaryKey.length === 1;
|
|
219
|
+
const fields = meta.columns.map((col) => {
|
|
220
|
+
const mods = [];
|
|
221
|
+
if (!col.nullable || pkSet.has(col.name))
|
|
222
|
+
mods.push('required');
|
|
223
|
+
if (pkSet.has(col.name) && pkIsSingle)
|
|
224
|
+
mods.push('unique');
|
|
225
|
+
// `auto` = server-generated monotonic int. PowDB requires it be `int` and
|
|
226
|
+
// rejects it alongside a `default`; non-int generated columns fall back to
|
|
227
|
+
// a plain typed column (Turbine assigns the value client-side instead).
|
|
228
|
+
if (col.isGenerated && powqlColumnType(col) === 'int')
|
|
229
|
+
mods.push('auto');
|
|
230
|
+
return ` ${mods.join(' ')}${mods.length ? ' ' : ''}${col.name}: ${powqlColumnType(col)}`;
|
|
231
|
+
});
|
|
232
|
+
stmts.push(`type ${meta.name} {\n${fields.join(',\n')}\n}`);
|
|
233
|
+
// Secondary unique constraints (beyond the PK) become unique indexes.
|
|
234
|
+
for (const uniq of meta.uniqueColumns) {
|
|
235
|
+
if (uniq.length === 1 && !pkSet.has(uniq[0])) {
|
|
236
|
+
stmts.push(`alter ${meta.name} add unique .${uniq[0]}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return stmts;
|
|
241
|
+
}
|
|
242
|
+
/** Coerce a JS value into a PowDB positional param (the write side). */
|
|
243
|
+
function toPowdbParam(value, col) {
|
|
244
|
+
if (value instanceof PowdbFloatParam)
|
|
245
|
+
return value.value; // wire-side: a float column takes the plain number
|
|
246
|
+
if (value === undefined || value === null)
|
|
247
|
+
return null;
|
|
248
|
+
if (value instanceof Date)
|
|
249
|
+
return BigInt(value.getTime()) * 1000n; // ms → micros (int column)
|
|
250
|
+
if (col && isDateColumn(col) && typeof value === 'number')
|
|
251
|
+
return BigInt(value) * 1000n;
|
|
252
|
+
if (typeof value === 'boolean' ||
|
|
253
|
+
typeof value === 'number' ||
|
|
254
|
+
typeof value === 'bigint' ||
|
|
255
|
+
typeof value === 'string') {
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
// Objects/arrays have no PowDB representation.
|
|
259
|
+
throw new ValidationError(`[turbine] Value of type ${typeof value} cannot be bound as a PowDB parameter.`);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Coerce a single PowDB wire string into the JS value its column type implies.
|
|
263
|
+
* Every PowDB value arrives as a string; NULL arrives as the bareword `"null"`.
|
|
264
|
+
* Metadata resolves the `"null"` ambiguity for nullable non-string columns.
|
|
265
|
+
*/
|
|
266
|
+
export function coerceValue(raw, col) {
|
|
267
|
+
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
268
|
+
// NULL bareword: unambiguous for non-string columns; for `str` we cannot tell a
|
|
269
|
+
// literal "null" from SQL NULL, so a nullable str of value "null" reads as null.
|
|
270
|
+
if (raw === 'null' && (ts !== 'string' || col.nullable))
|
|
271
|
+
return null;
|
|
272
|
+
if (ts === 'Date') {
|
|
273
|
+
const micros = Number(raw);
|
|
274
|
+
return Number.isFinite(micros) ? new Date(micros / 1000) : null;
|
|
275
|
+
}
|
|
276
|
+
if (ts === 'boolean')
|
|
277
|
+
return raw === 'true';
|
|
278
|
+
if (ts === 'number') {
|
|
279
|
+
const n = Number(raw);
|
|
280
|
+
// int8 policy: keep precision-losing big integers as strings.
|
|
281
|
+
return Number.isSafeInteger(n) || !Number.isInteger(n) ? n : raw;
|
|
282
|
+
}
|
|
283
|
+
if (ts === 'bigint')
|
|
284
|
+
return BigInt(raw);
|
|
285
|
+
return raw; // string / uuid-as-string
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Map one raw PowDB row (snake-cased columns → raw wire strings, as produced by
|
|
289
|
+
* {@link PowdbPool}) into a typed entity (camelCase fields, coerced values).
|
|
290
|
+
* Only the columns present in `raw` are emitted, so partial `select` projections
|
|
291
|
+
* round-trip unchanged.
|
|
292
|
+
*/
|
|
293
|
+
export function rowToEntity(raw, meta) {
|
|
294
|
+
const byName = new Map(meta.columns.map((c) => [c.name, c]));
|
|
295
|
+
const out = {};
|
|
296
|
+
for (const snake of Object.keys(raw)) {
|
|
297
|
+
const col = byName.get(snake);
|
|
298
|
+
const field = meta.reverseColumnMap[snake] ?? snake;
|
|
299
|
+
const value = raw[snake];
|
|
300
|
+
out[field] = col && typeof value === 'string' ? coerceValue(value, col) : value;
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// Error translation
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
/**
|
|
308
|
+
* Translate a PowDB error into a typed Turbine error. Handles BOTH transports,
|
|
309
|
+
* whose error shapes differ:
|
|
310
|
+
* - **networked** (`@zvndev/powdb-client`) tags errors with a *semantic*
|
|
311
|
+
* `.code` (`connect_failed`, `timeout`, `query_failed`, …);
|
|
312
|
+
* - **embedded** (`@zvndev/powdb-embedded` napi addon) tags EVERY error
|
|
313
|
+
* `code:'GenericFailure'`, so the class can only be recovered from the
|
|
314
|
+
* message text (`Execution("column 'email' is required …")`,
|
|
315
|
+
* `Execution("type mismatch …")`, `Parse(…)`, `StorageError(…)`).
|
|
316
|
+
*
|
|
317
|
+
* So we always run the unique-constraint and message-shape checks first (they
|
|
318
|
+
* fire for both transports), then fall through to the networked `.code` switch.
|
|
319
|
+
*/
|
|
320
|
+
export function wrapPowdbError(err) {
|
|
321
|
+
if (!err || typeof err !== 'object')
|
|
322
|
+
return new ConnectionError(`[turbine] PowDB error: ${String(err)}`);
|
|
323
|
+
const e = err;
|
|
324
|
+
const msg = e.message ?? 'unknown PowDB error';
|
|
325
|
+
// Unique-constraint — message-based on both transports.
|
|
326
|
+
if (/unique constraint violation/i.test(msg)) {
|
|
327
|
+
const m = /on\s+\S+\.(\w+)/i.exec(msg);
|
|
328
|
+
return new UniqueConstraintError({ constraint: m?.[1], cause: err });
|
|
329
|
+
}
|
|
330
|
+
// NOT NULL — "column 'x' is required but no value was provided". Map on BOTH
|
|
331
|
+
// transports (the networked path used to collapse this into E003).
|
|
332
|
+
if (/required|not[- ]?null|no value/i.test(msg)) {
|
|
333
|
+
const m = /column ['"]?(\w+)['"]?/i.exec(msg);
|
|
334
|
+
return new NotNullViolationError({ column: m?.[1], cause: err });
|
|
335
|
+
}
|
|
336
|
+
// Type mismatch / parse / execution / storage / unexpected → validation
|
|
337
|
+
// (E003). On the embedded transport these are the only signal we get
|
|
338
|
+
// (code is always 'GenericFailure'); on the networked path they are a
|
|
339
|
+
// safety net before the .code switch.
|
|
340
|
+
if (/type mismatch|\bParse\b|\bExecution\b|StorageError|unexpected|row too large/i.test(msg)) {
|
|
341
|
+
return new ValidationError(`[turbine] PowDB query rejected: ${msg}`);
|
|
342
|
+
}
|
|
343
|
+
switch (e.code) {
|
|
344
|
+
case 'connect_failed':
|
|
345
|
+
case 'closed':
|
|
346
|
+
return new ConnectionError(`[turbine] PowDB connection failed: ${msg}`);
|
|
347
|
+
case 'timeout':
|
|
348
|
+
case 'aborted':
|
|
349
|
+
return new TimeoutError(0, 'PowDB query');
|
|
350
|
+
case 'query_failed':
|
|
351
|
+
case 'type_coercion_failed':
|
|
352
|
+
case 'protocol_error':
|
|
353
|
+
case 'size_exceeded':
|
|
354
|
+
return new ValidationError(`[turbine] PowDB query rejected: ${msg}`);
|
|
355
|
+
default:
|
|
356
|
+
return err instanceof Error ? err : new ConnectionError(`[turbine] PowDB error: ${msg}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function normalizeQueryArgs(arg, values) {
|
|
360
|
+
if (typeof arg === 'string')
|
|
361
|
+
return { text: arg, params: values ?? [] };
|
|
362
|
+
return { text: arg.text, params: arg.values ?? values ?? [] };
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Classify a transaction-control statement so the single-writer guard can track
|
|
366
|
+
* whether a transaction is open. Matches the lowercase keywords
|
|
367
|
+
* {@link powdbDialect} emits (`begin`/`commit`/`rollback`) plus the common SQL
|
|
368
|
+
* spellings, case-insensitively. Returns `null` for ordinary queries.
|
|
369
|
+
*/
|
|
370
|
+
function txControl(powql) {
|
|
371
|
+
const head = powql.trim().toLowerCase();
|
|
372
|
+
if (head === 'begin' || head === 'begin transaction' || head === 'start transaction')
|
|
373
|
+
return 'begin';
|
|
374
|
+
if (head === 'commit' || head === 'commit transaction' || head === 'end')
|
|
375
|
+
return 'commit';
|
|
376
|
+
if (head === 'rollback' || head === 'rollback transaction')
|
|
377
|
+
return 'rollback';
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* The error a pool throws when a `begin` arrives while a transaction is already
|
|
382
|
+
* open. PowDB has ONE global write lock and supports neither concurrent nor
|
|
383
|
+
* nested transactions: on the networked transport a second `begin` checks out a
|
|
384
|
+
* fresh pooled connection and blocks forever on the lock the open transaction
|
|
385
|
+
* holds. This guard converts that hang into a fast, typed error.
|
|
386
|
+
*/
|
|
387
|
+
function reentrantTransactionError() {
|
|
388
|
+
return new UnsupportedFeatureError('concurrent or nested transactions', 'powdb', 'PowDB is single-writer — it has one global write lock. A second transaction would block on it forever; ' +
|
|
389
|
+
'complete the open transaction first.');
|
|
390
|
+
}
|
|
391
|
+
/** Adapt a PowDB result into the pg-compat `{ rows, rowCount, fields }` shape. */
|
|
392
|
+
function adaptResult(r) {
|
|
393
|
+
switch (r.kind) {
|
|
394
|
+
case 'rows': {
|
|
395
|
+
const rows = r.rows.map((row) => {
|
|
396
|
+
const o = {};
|
|
397
|
+
r.columns.forEach((c, i) => {
|
|
398
|
+
o[c] = row[i];
|
|
399
|
+
});
|
|
400
|
+
return o;
|
|
401
|
+
});
|
|
402
|
+
return { rows, rowCount: rows.length, fields: r.columns.map((name) => ({ name, dataTypeID: 0 })) };
|
|
403
|
+
}
|
|
404
|
+
case 'ok':
|
|
405
|
+
return { rows: [], rowCount: Number(r.affected), fields: [] };
|
|
406
|
+
case 'scalar':
|
|
407
|
+
return { rows: [{ value: r.value }], rowCount: 1, fields: [{ name: 'value', dataTypeID: 0 }] };
|
|
408
|
+
default:
|
|
409
|
+
return { rows: [], rowCount: 0, fields: [] };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* A {@link PgCompatPool} backed by a `@zvndev/powdb-client` `Pool`. The query
|
|
414
|
+
* `text` is **PowQL**, not SQL — {@link PowqlInterface} generates it. Rows come
|
|
415
|
+
* back as raw strings here; per-column JS coercion happens in `PowqlInterface`
|
|
416
|
+
* (it owns the schema metadata).
|
|
417
|
+
*/
|
|
418
|
+
export class PowdbPool {
|
|
419
|
+
pool;
|
|
420
|
+
toParam;
|
|
421
|
+
closed = false;
|
|
422
|
+
/**
|
|
423
|
+
* Pool-level single-writer guard. PowDB holds one global write lock, so at
|
|
424
|
+
* most one transaction may be open across the whole pool. A `begin` issued
|
|
425
|
+
* while this is `true` is rejected (it would otherwise check out a second
|
|
426
|
+
* connection and block on the lock forever — the networked re-entrant hang).
|
|
427
|
+
*/
|
|
428
|
+
activeTransaction = false;
|
|
429
|
+
constructor(pool, toParam = (v) => toPowdbParam(v)) {
|
|
430
|
+
this.pool = pool;
|
|
431
|
+
this.toParam = toParam;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Enforce the single-writer model on a transaction-control statement. Throws
|
|
435
|
+
* (before any query runs) if a `begin` arrives while a transaction is open;
|
|
436
|
+
* otherwise flips the pool-level flag. Returns the control kind so the caller
|
|
437
|
+
* can decide whether it even needs to hit the engine.
|
|
438
|
+
*/
|
|
439
|
+
guardTxControl(powql) {
|
|
440
|
+
const ctl = txControl(powql);
|
|
441
|
+
if (ctl === 'begin') {
|
|
442
|
+
if (this.activeTransaction)
|
|
443
|
+
throw reentrantTransactionError();
|
|
444
|
+
this.activeTransaction = true;
|
|
445
|
+
}
|
|
446
|
+
else if (ctl === 'commit' || ctl === 'rollback') {
|
|
447
|
+
this.activeTransaction = false;
|
|
448
|
+
}
|
|
449
|
+
return ctl;
|
|
450
|
+
}
|
|
451
|
+
// biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape.
|
|
452
|
+
async query(text, values) {
|
|
453
|
+
const { text: powql, params } = normalizeQueryArgs(text, values);
|
|
454
|
+
this.guardTxControl(powql);
|
|
455
|
+
try {
|
|
456
|
+
const result = await this.pool.withClient((c) => c.query(powql, params.map(this.toParam)));
|
|
457
|
+
return adaptResult(result);
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
throw wrapPowdbError(err);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async connect() {
|
|
464
|
+
const client = await this.pool.acquire();
|
|
465
|
+
let broken = false;
|
|
466
|
+
return {
|
|
467
|
+
// biome-ignore lint/suspicious/noExplicitAny: see query() above.
|
|
468
|
+
query: async (text, values) => {
|
|
469
|
+
const { text: powql, params } = normalizeQueryArgs(text, values);
|
|
470
|
+
// Guard BEFORE acquiring the engine — a re-entrant begin throws fast
|
|
471
|
+
// instead of blocking on the global write lock the open tx holds.
|
|
472
|
+
this.guardTxControl(powql);
|
|
473
|
+
try {
|
|
474
|
+
return adaptResult(await client.query(powql, params.map(this.toParam)));
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
broken = true;
|
|
478
|
+
throw wrapPowdbError(err);
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
release: () => {
|
|
482
|
+
// Releasing this connection ends its transaction scope. Clear the flag
|
|
483
|
+
// as a safety net so a tx torn down without an explicit commit/rollback
|
|
484
|
+
// (e.g. a timeout that destroys the connection) never leaves the pool
|
|
485
|
+
// permanently believing a transaction is still open.
|
|
486
|
+
this.activeTransaction = false;
|
|
487
|
+
return broken ? this.pool.destroy(client) : this.pool.release(client);
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
async end() {
|
|
492
|
+
if (this.closed)
|
|
493
|
+
return;
|
|
494
|
+
this.closed = true;
|
|
495
|
+
await this.pool.close();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/** Normalize the embedded addon's loosely-typed result into a {@link PowdbResult}. */
|
|
499
|
+
function normalizeEmbeddedResult(r) {
|
|
500
|
+
switch (r.kind) {
|
|
501
|
+
case 'rows':
|
|
502
|
+
return { kind: 'rows', columns: r.columns ?? [], rows: r.rows ?? [] };
|
|
503
|
+
case 'scalar':
|
|
504
|
+
return { kind: 'scalar', value: r.value ?? 'null' };
|
|
505
|
+
case 'ok':
|
|
506
|
+
return { kind: 'ok', affected: r.affected ?? 0n };
|
|
507
|
+
default:
|
|
508
|
+
return { kind: 'message', message: r.message ?? '' };
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Encode a JS value as a **PowQL literal** for the embedded driver, which takes
|
|
513
|
+
* no params array — `$N` placeholders must be materialized into the query text.
|
|
514
|
+
*
|
|
515
|
+
* This is the single place Turbine builds PowQL text from a value, so it is the
|
|
516
|
+
* security-critical surface. String encoding matches PowDB's lexer
|
|
517
|
+
* (`crates/query/src/lexer.rs`) exactly: a string literal is `"…"`, and inside
|
|
518
|
+
* it the lexer recognizes only the escapes `\"`, `\\`, `\n`, `\t` (any other
|
|
519
|
+
* `\x` drops the backslash and keeps `x`; every non-`\`/non-`"` char — raw
|
|
520
|
+
* newlines, CR, unicode — is taken literally). So we escape `\` → `\\` and
|
|
521
|
+
* `"` → `\"` (the only breakout vectors), render `\n`/`\t` as their recognized
|
|
522
|
+
* escapes, and leave everything else raw. Verified against the real engine:
|
|
523
|
+
* quotes, backslashes, `$N`, `"); drop … --`, raw CR, and emoji all round-trip
|
|
524
|
+
* as data and cannot break out of the literal or inject a second statement.
|
|
525
|
+
*/
|
|
526
|
+
export function encodePowqlLiteral(value) {
|
|
527
|
+
if (value instanceof PowdbFloatParam) {
|
|
528
|
+
const n = value.value;
|
|
529
|
+
if (!Number.isFinite(n))
|
|
530
|
+
throw new ValidationError(`[turbine] Non-finite float cannot be encoded for PowDB.`);
|
|
531
|
+
// Force a float-form literal so an integer-valued float column stays a float.
|
|
532
|
+
return Number.isInteger(n) ? `${n}.0` : String(n);
|
|
533
|
+
}
|
|
534
|
+
if (value === undefined || value === null)
|
|
535
|
+
return 'null';
|
|
536
|
+
if (value instanceof Date)
|
|
537
|
+
return `${BigInt(value.getTime()) * 1000n}`; // epoch micros (int column)
|
|
538
|
+
if (typeof value === 'boolean')
|
|
539
|
+
return value ? 'true' : 'false';
|
|
540
|
+
if (typeof value === 'bigint')
|
|
541
|
+
return value.toString();
|
|
542
|
+
if (typeof value === 'number') {
|
|
543
|
+
if (!Number.isFinite(value))
|
|
544
|
+
throw new ValidationError(`[turbine] Non-finite number cannot be encoded for PowDB.`);
|
|
545
|
+
// `String(n)` renders an integer as an int literal (`42`) and a fractional
|
|
546
|
+
// number as a float literal (`4.2`) — PowQL distinguishes them by the dot.
|
|
547
|
+
return String(value);
|
|
548
|
+
}
|
|
549
|
+
if (typeof value === 'string')
|
|
550
|
+
return encodePowqlString(value);
|
|
551
|
+
throw new ValidationError(`[turbine] Value of type ${typeof value} cannot be encoded as a PowDB literal.`);
|
|
552
|
+
}
|
|
553
|
+
/** Escape a string into a PowQL `"…"` literal, matching the engine lexer's escape rules. */
|
|
554
|
+
function encodePowqlString(s) {
|
|
555
|
+
let out = '"';
|
|
556
|
+
for (const ch of s) {
|
|
557
|
+
if (ch === '\\')
|
|
558
|
+
out += '\\\\';
|
|
559
|
+
else if (ch === '"')
|
|
560
|
+
out += '\\"';
|
|
561
|
+
else if (ch === '\n')
|
|
562
|
+
out += '\\n';
|
|
563
|
+
else if (ch === '\t')
|
|
564
|
+
out += '\\t';
|
|
565
|
+
else
|
|
566
|
+
out += ch; // raw — the lexer takes any other char literally (incl. CR, unicode)
|
|
567
|
+
}
|
|
568
|
+
return `${out}"`;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Substitute every `$N` placeholder in a generator-produced PowQL template with
|
|
572
|
+
* the encoded literal of `params[N-1]`. Safe because the template is produced by
|
|
573
|
+
* {@link PowqlInterface} and contains **no** user string literals — the only
|
|
574
|
+
* `$<digits>` tokens are genuine positional placeholders, so a single scan
|
|
575
|
+
* cannot accidentally rewrite a `$N` that is itself part of a value (values are
|
|
576
|
+
* params, never inlined into the template by the generator).
|
|
577
|
+
*/
|
|
578
|
+
export function materializePowql(powql, params) {
|
|
579
|
+
return powql.replace(/\$(\d+)/g, (_m, n) => {
|
|
580
|
+
const idx = Number(n) - 1;
|
|
581
|
+
if (idx < 0 || idx >= params.length) {
|
|
582
|
+
throw new ValidationError(`[turbine] PowQL placeholder $${n} has no bound parameter (have ${params.length}).`);
|
|
583
|
+
}
|
|
584
|
+
return encodePowqlLiteral(params[idx]);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* A {@link PgCompatPool} backed by an in-process `@zvndev/powdb-embedded`
|
|
589
|
+
* `Database`. The embedded addon takes **no params array** — its `query(powql)`
|
|
590
|
+
* accepts only a string — so this pool materializes each positional `$N` into a
|
|
591
|
+
* PowQL literal via {@link materializePowql} before handing the text to the
|
|
592
|
+
* engine. One handle, single connection: transaction keywords (`begin`/`commit`/
|
|
593
|
+
* `rollback`) are issued serially as ordinary queries.
|
|
594
|
+
*/
|
|
595
|
+
export class PowdbEmbeddedPool {
|
|
596
|
+
db;
|
|
597
|
+
closed = false;
|
|
598
|
+
/**
|
|
599
|
+
* Single-writer guard. The embedded engine is one handle with one global
|
|
600
|
+
* write lock — only one transaction may be open at a time. A re-entrant
|
|
601
|
+
* `begin` (a fresh top-level `db.$transaction` opened inside an open one)
|
|
602
|
+
* would otherwise hit PowDB's raw "already in a transaction" parse error;
|
|
603
|
+
* this surfaces a typed error instead. (Nested `tx.$transaction` is caught
|
|
604
|
+
* earlier still, by the savepoint override in {@link powdbDialect}.)
|
|
605
|
+
*/
|
|
606
|
+
activeTransaction = false;
|
|
607
|
+
constructor(db) {
|
|
608
|
+
this.db = db;
|
|
609
|
+
}
|
|
610
|
+
/** Enforce the single-writer model on a transaction-control statement. */
|
|
611
|
+
guardTxControl(powql) {
|
|
612
|
+
const ctl = txControl(powql);
|
|
613
|
+
if (ctl === 'begin') {
|
|
614
|
+
if (this.activeTransaction)
|
|
615
|
+
throw reentrantTransactionError();
|
|
616
|
+
this.activeTransaction = true;
|
|
617
|
+
}
|
|
618
|
+
else if (ctl === 'commit' || ctl === 'rollback') {
|
|
619
|
+
this.activeTransaction = false;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
run(powql, params) {
|
|
623
|
+
this.guardTxControl(powql);
|
|
624
|
+
try {
|
|
625
|
+
const materialized = materializePowql(powql, params);
|
|
626
|
+
return adaptResult(normalizeEmbeddedResult(this.db.query(materialized)));
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
throw wrapPowdbError(err);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape.
|
|
633
|
+
async query(text, values) {
|
|
634
|
+
const { text: powql, params } = normalizeQueryArgs(text, values);
|
|
635
|
+
return this.run(powql, params);
|
|
636
|
+
}
|
|
637
|
+
async connect() {
|
|
638
|
+
// Single in-process handle — the "client" shares the one Database; tx
|
|
639
|
+
// keywords run serially on it.
|
|
640
|
+
return {
|
|
641
|
+
// biome-ignore lint/suspicious/noExplicitAny: see query() above.
|
|
642
|
+
query: async (text, values) => {
|
|
643
|
+
const { text: powql, params } = normalizeQueryArgs(text, values);
|
|
644
|
+
return this.run(powql, params);
|
|
645
|
+
},
|
|
646
|
+
release: () => {
|
|
647
|
+
// End-of-scope safety net (see PowdbPool.connect()): a tx torn down
|
|
648
|
+
// without an explicit commit/rollback must not wedge the handle.
|
|
649
|
+
this.activeTransaction = false;
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
async end() {
|
|
654
|
+
if (this.closed)
|
|
655
|
+
return;
|
|
656
|
+
// The addon exposes no explicit close — drop the reference and let GC /
|
|
657
|
+
// the engine's checkpoint flush. Caveat: durability is checkpoint-bound, so
|
|
658
|
+
// hold the process open long enough for the final WAL flush in short scripts.
|
|
659
|
+
this.closed = true;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// ---------------------------------------------------------------------------
|
|
663
|
+
// PowqlInterface — the PowQL query generator (Phase A: flat CRUD via returning)
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
export { PowqlInterface } from './powql.js';
|
|
666
|
+
/**
|
|
667
|
+
* Dynamically load `@zvndev/powdb-client`. Kept out of the static import graph so
|
|
668
|
+
* `import 'turbine-orm/powdb'` never throws when the optional peer is absent.
|
|
669
|
+
*/
|
|
670
|
+
async function loadPowdb() {
|
|
671
|
+
try {
|
|
672
|
+
return (await import('@zvndev/powdb-client'));
|
|
673
|
+
}
|
|
674
|
+
catch (err) {
|
|
675
|
+
throw new ConnectionError("[turbine] turbine-orm/powdb requires the optional peer dependency '@zvndev/powdb-client'. Install it: npm i @zvndev/powdb-client. " +
|
|
676
|
+
`(${err.message})`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Dynamically load `@zvndev/powdb-embedded` (the in-process napi addon). Kept out
|
|
681
|
+
* of the static import graph — `import 'turbine-orm/powdb'` never pulls it. A
|
|
682
|
+
* missing package or an unsupported platform (Intel-mac/musl/Windows ship no
|
|
683
|
+
* prebuilt binary) throws a clear {@link ConnectionError} pointing at the
|
|
684
|
+
* from-source `npm run build` fallback.
|
|
685
|
+
*/
|
|
686
|
+
async function loadPowdbEmbedded() {
|
|
687
|
+
let mod;
|
|
688
|
+
try {
|
|
689
|
+
mod = (await import('@zvndev/powdb-embedded'));
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
throw new ConnectionError("[turbine] turbine-orm/powdb embedded mode requires the optional peer '@zvndev/powdb-embedded'. " +
|
|
693
|
+
'Install it: npm i @zvndev/powdb-embedded. If install succeeded but loading failed, your platform has no ' +
|
|
694
|
+
'prebuilt binary (prebuilts ship for macOS arm64/x64 and Linux glibc x64/arm64; Intel-mac/musl/Windows ' +
|
|
695
|
+
'build from source) — build it with `npm run build` in the addon, then retry. ' +
|
|
696
|
+
`(${err.message})`);
|
|
697
|
+
}
|
|
698
|
+
if (!mod || typeof mod.Database?.open !== 'function') {
|
|
699
|
+
throw new ConnectionError("[turbine] '@zvndev/powdb-embedded' loaded but did not export Database.open — the installed version is " +
|
|
700
|
+
'likely incompatible (turbine-orm/powdb embedded requires @zvndev/powdb-embedded ^0.7.0).');
|
|
701
|
+
}
|
|
702
|
+
return mod;
|
|
703
|
+
}
|
|
704
|
+
/** Open an embedded database handle, wrapping engine open failures (corrupt dir, etc.). */
|
|
705
|
+
async function openEmbeddedPool(target) {
|
|
706
|
+
const mod = await loadPowdbEmbedded();
|
|
707
|
+
const { embedded: dir, syncMode, memoryLimit } = target;
|
|
708
|
+
let db;
|
|
709
|
+
try {
|
|
710
|
+
if (memoryLimit !== undefined) {
|
|
711
|
+
if (typeof mod.Database.openWithMemoryLimit !== 'function') {
|
|
712
|
+
throw new ConnectionError('[turbine] embedded `memoryLimit` requires @zvndev/powdb-embedded ≥ 0.7.1.');
|
|
713
|
+
}
|
|
714
|
+
db = mod.Database.openWithMemoryLimit(dir, memoryLimit);
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
db = mod.Database.open(dir);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
catch (err) {
|
|
721
|
+
if (err instanceof ConnectionError)
|
|
722
|
+
throw err;
|
|
723
|
+
throw new ConnectionError(`[turbine] PowDB embedded could not open data dir "${dir}": ${err.message}`);
|
|
724
|
+
}
|
|
725
|
+
if (syncMode !== undefined) {
|
|
726
|
+
if (typeof db.setSyncMode !== 'function') {
|
|
727
|
+
throw new ConnectionError('[turbine] embedded `syncMode` requires @zvndev/powdb-embedded ≥ 0.7.1 (the installed addon has no setSyncMode).');
|
|
728
|
+
}
|
|
729
|
+
db.setSyncMode(syncMode);
|
|
730
|
+
}
|
|
731
|
+
return new PowdbEmbeddedPool(db);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Bind Turbine to PowDB. `target` is one of:
|
|
735
|
+
* - a `powdb://[user[:pass]@]host[:port][/db]` connection string → a
|
|
736
|
+
* **networked** `@zvndev/powdb-client` pool (consistency with
|
|
737
|
+
* `turbineMysql`/`turbineMssql`);
|
|
738
|
+
* - a host/port options object → a **networked** `@zvndev/powdb-client` pool;
|
|
739
|
+
* - an `{ embedded: <data-dir> }` object → an in-process
|
|
740
|
+
* `@zvndev/powdb-embedded` database (no server);
|
|
741
|
+
* - an already-constructed `@zvndev/powdb-client` `Pool` or {@link PowdbPool}
|
|
742
|
+
* (injection — you own its lifecycle and `disconnect()` is a no-op).
|
|
743
|
+
*
|
|
744
|
+
* On the networked transport the server version is probed and a clear
|
|
745
|
+
* {@link ConnectionError} is thrown if it is older than {@link MIN_POWDB_VERSION}
|
|
746
|
+
* (the `returning` keyword / int->float coercion fix Turbine relies on).
|
|
747
|
+
*
|
|
748
|
+
* Resolves to a `TurbineClient` whose `table()` accessors generate **PowQL** via
|
|
749
|
+
* {@link PowqlInterface}. The SQL `Dialect` is not involved.
|
|
750
|
+
*/
|
|
751
|
+
export async function turbinePowDB(target, schema, options = {}) {
|
|
752
|
+
let pool;
|
|
753
|
+
let owns = false;
|
|
754
|
+
if (typeof target === 'string') {
|
|
755
|
+
const mod = await loadPowdb();
|
|
756
|
+
const clientPool = new mod.Pool({ ...parsePowdbUrl(target), max: options.connectionLimit ?? 10 });
|
|
757
|
+
await assertNetworkedVersion(clientPool);
|
|
758
|
+
pool = new PowdbPool(clientPool);
|
|
759
|
+
owns = true;
|
|
760
|
+
}
|
|
761
|
+
else if (target instanceof PowdbPool) {
|
|
762
|
+
pool = target;
|
|
763
|
+
}
|
|
764
|
+
else if (isEmbeddedTarget(target)) {
|
|
765
|
+
pool = await openEmbeddedPool(target);
|
|
766
|
+
owns = true;
|
|
767
|
+
}
|
|
768
|
+
else if (isPowdbClientPool(target)) {
|
|
769
|
+
pool = new PowdbPool(target);
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
const mod = await loadPowdb();
|
|
773
|
+
const clientPool = new mod.Pool({ ...target, max: options.connectionLimit ?? 10 });
|
|
774
|
+
await assertNetworkedVersion(clientPool);
|
|
775
|
+
pool = new PowdbPool(clientPool);
|
|
776
|
+
owns = true;
|
|
777
|
+
}
|
|
778
|
+
// The PowQL generator is loaded here to keep client.ts free of any PowDB import.
|
|
779
|
+
const { PowqlInterface } = await import('./powql.js');
|
|
780
|
+
const queryInterfaceFactory = (p, table, sch, middlewares, opts) => new PowqlInterface(p, table, sch, middlewares, opts);
|
|
781
|
+
const client = new TurbineClient({
|
|
782
|
+
pool,
|
|
783
|
+
preparedStatements: false,
|
|
784
|
+
dialect: powdbDialect,
|
|
785
|
+
logging: options.logging,
|
|
786
|
+
defaultLimit: options.defaultLimit,
|
|
787
|
+
warnOnUnlimited: options.warnOnUnlimited,
|
|
788
|
+
queryInterfaceFactory,
|
|
789
|
+
}, schema);
|
|
790
|
+
if (!owns) {
|
|
791
|
+
// Injected pool — the caller owns its lifecycle.
|
|
792
|
+
client.disconnect = async () => { };
|
|
793
|
+
}
|
|
794
|
+
return client;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Probe a networked pool's `serverVersion` (declared on {@link PowdbClient}) and
|
|
798
|
+
* fail fast if the server is older than {@link MIN_POWDB_VERSION}. Best-effort:
|
|
799
|
+
* a driver that does not surface a version is left untouched (we cannot prove it
|
|
800
|
+
* too old). Errors from the probe itself surface as the normal connect failure.
|
|
801
|
+
*/
|
|
802
|
+
async function assertNetworkedVersion(clientPool) {
|
|
803
|
+
await clientPool.withClient(async (c) => {
|
|
804
|
+
assertSupportedPowdbVersion(c.serverVersion);
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
function isPowdbClientPool(x) {
|
|
808
|
+
return (!!x &&
|
|
809
|
+
typeof x === 'object' &&
|
|
810
|
+
typeof x.acquire === 'function' &&
|
|
811
|
+
typeof x.withClient === 'function');
|
|
812
|
+
}
|
|
813
|
+
function isEmbeddedTarget(x) {
|
|
814
|
+
return !!x && typeof x === 'object' && typeof x.embedded === 'string';
|
|
815
|
+
}
|
|
816
|
+
//# sourceMappingURL=powdb.js.map
|