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