turbine-orm 0.21.0 → 0.22.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/dist/cli/ui.d.ts CHANGED
@@ -40,7 +40,7 @@ export declare const symbols: {
40
40
  readonly bottomLeft: "+" | "╰";
41
41
  readonly bottomRight: "+" | "╯";
42
42
  readonly tee: "|" | "├";
43
- readonly teeEnd: "" | "\\";
43
+ readonly teeEnd: "\\" | "";
44
44
  };
45
45
  export declare function box(content: string, options?: {
46
46
  title?: string;
package/dist/client.js CHANGED
@@ -113,7 +113,9 @@ export class TransactionClient {
113
113
  // We use a proxy pool that routes queries through the transaction client
114
114
  const txPool = this.createTxPool();
115
115
  const txOpts = { ...this.queryOptions, _txScoped: true };
116
- qi = new QueryInterface(txPool, name, this.schema, this.middlewares, txOpts);
116
+ qi = txOpts.queryInterfaceFactory
117
+ ? txOpts.queryInterfaceFactory(txPool, name, this.schema, this.middlewares, txOpts)
118
+ : new QueryInterface(txPool, name, this.schema, this.middlewares, txOpts);
117
119
  this.tableCache.set(name, qi);
118
120
  }
119
121
  return qi;
@@ -254,6 +256,11 @@ export class TurbineClient {
254
256
  preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? !config.pool),
255
257
  sqlCache: config.sqlCache ?? true,
256
258
  dialect: config.dialect,
259
+ // Non-SQL backends (PowDB) inject a factory that builds their own query
260
+ // interface (PowqlInterface) instead of the SQL QueryInterface. SQL engines
261
+ // never set this, so `table()` keeps constructing `new QueryInterface`.
262
+ queryInterfaceFactory: config
263
+ .queryInterfaceFactory,
257
264
  _onQuery: (event) => {
258
265
  if (this.queryListeners.size === 0)
259
266
  return;
@@ -405,7 +412,9 @@ export class TurbineClient {
405
412
  table(name) {
406
413
  let qi = this.tableCache.get(name);
407
414
  if (!qi) {
408
- qi = new QueryInterface(this.pool, name, this.schema, this.middlewares, this.queryOptions);
415
+ qi = this.queryOptions?.queryInterfaceFactory
416
+ ? this.queryOptions.queryInterfaceFactory(this.pool, name, this.schema, this.middlewares, this.queryOptions)
417
+ : new QueryInterface(this.pool, name, this.schema, this.middlewares, this.queryOptions);
409
418
  this.tableCache.set(name, qi);
410
419
  }
411
420
  return qi;
@@ -0,0 +1,338 @@
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 { type PgCompatPool, type PgCompatPoolClient, TurbineClient, type TurbineConfig } from './client.js';
45
+ import { type Dialect } from './dialect.js';
46
+ import type { ColumnMetadata, SchemaMetadata, TableMetadata } from './schema.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 declare const powdbDialect: Dialect;
68
+ /** A single value PowDB accepts as a positional `$N` parameter. */
69
+ type PowdbParam = string | number | bigint | boolean | null;
70
+ /**
71
+ * Marker wrapper for a value bound to a `float` column. The networked driver
72
+ * unwraps it to the plain number (the wire param is unchanged), but the
73
+ * *embedded* literal encoder reads it to emit a float-form PowQL literal (`42`
74
+ * → `42.0`) so an integer-valued float column stays unambiguously a float.
75
+ * Constructed in {@link PowqlInterface.param}.
76
+ */
77
+ export declare class PowdbFloatParam {
78
+ readonly value: number;
79
+ constructor(value: number);
80
+ }
81
+ /** The four shapes a PowQL result takes over the wire. */
82
+ type PowdbResult = {
83
+ kind: 'rows';
84
+ columns: string[];
85
+ rows: string[][];
86
+ } | {
87
+ kind: 'scalar';
88
+ value: string;
89
+ } | {
90
+ kind: 'ok';
91
+ affected: bigint;
92
+ } | {
93
+ kind: 'message';
94
+ message: string;
95
+ };
96
+ interface PowdbClient {
97
+ readonly serverVersion: string;
98
+ query(query: string, params?: PowdbParam[], opts?: {
99
+ signal?: AbortSignal;
100
+ }): Promise<PowdbResult>;
101
+ close(): Promise<void>;
102
+ }
103
+ interface PowdbClientPool {
104
+ acquire(): Promise<PowdbClient>;
105
+ release(c: PowdbClient): void;
106
+ destroy(c: PowdbClient): void;
107
+ withClient<T>(fn: (c: PowdbClient) => Promise<T>): Promise<T>;
108
+ close(): Promise<void>;
109
+ }
110
+ /** Connection options for {@link turbinePowDB} — host/port, not a connection string. */
111
+ export interface PowdbConnOptions {
112
+ host: string;
113
+ port: number;
114
+ dbName?: string;
115
+ user?: string;
116
+ password?: string | null;
117
+ connectTimeoutMs?: number;
118
+ tls?: boolean;
119
+ }
120
+ /** Minimum PowDB server version the networked transport requires. */
121
+ export declare const MIN_POWDB_VERSION = "0.7.0";
122
+ /**
123
+ * Parse a `powdb://[user[:pass]@]host[:port][/db]` connection string into
124
+ * {@link PowdbConnOptions} (consistency with `turbineMysql`/`turbineMssql`,
125
+ * which accept a URL). Defaults: host `127.0.0.1`, port `5433`.
126
+ */
127
+ export declare function parsePowdbUrl(connectionString: string): PowdbConnOptions;
128
+ /**
129
+ * Fail fast if a networked PowDB server is older than {@link MIN_POWDB_VERSION}.
130
+ * Turbine's write path relies on the trailing `returning` keyword and the
131
+ * int→float coercion fix, both of which landed in 0.7.0. Embedded exposes no
132
+ * version method, so this is networked-only (the embedded peer is pinned ^0.7.0
133
+ * at install time). A non-semver / empty version string is tolerated (we cannot
134
+ * prove it is too old).
135
+ */
136
+ export declare function assertSupportedPowdbVersion(version: string | undefined): void;
137
+ /** PowQL column types Turbine is willing to emit (the four writable scalars). */
138
+ export type PowqlType = 'str' | 'int' | 'float' | 'bool';
139
+ /**
140
+ * Map a Turbine column to the PowQL DDL type used in `defineSchema` →
141
+ * `type T { … }`. Turbine never emits PowDB's `uuid`/`datetime`/`bytes` types,
142
+ * which cannot hold client-supplied values on the wire (no literal, no cast):
143
+ * - `Date` → `int` (epoch micros) - `boolean` → `bool`
144
+ * - integral `number`/`bigint` → `int` - fractional `number` → `float`
145
+ * - everything else (incl. UUID/PK strings) → `str`
146
+ * Array / JSON / bytes columns throw — they have no PowDB equivalent.
147
+ */
148
+ export declare function powqlColumnType(col: ColumnMetadata): PowqlType;
149
+ /**
150
+ * Generate PowQL DDL (`type T { … }`) for every table in a schema. Used to
151
+ * provision a PowDB database from a code-first `defineSchema`/`SchemaMetadata`
152
+ * (PowDB has no migration runner yet). The primary key column is declared
153
+ * `required unique`; non-nullable columns are `required`.
154
+ */
155
+ export declare function powqlSchemaDDL(schema: SchemaMetadata): string[];
156
+ /**
157
+ * Coerce a single PowDB wire string into the JS value its column type implies.
158
+ * Every PowDB value arrives as a string; NULL arrives as the bareword `"null"`.
159
+ * Metadata resolves the `"null"` ambiguity for nullable non-string columns.
160
+ */
161
+ export declare function coerceValue(raw: string, col: ColumnMetadata): unknown;
162
+ /**
163
+ * Map one raw PowDB row (snake-cased columns → raw wire strings, as produced by
164
+ * {@link PowdbPool}) into a typed entity (camelCase fields, coerced values).
165
+ * Only the columns present in `raw` are emitted, so partial `select` projections
166
+ * round-trip unchanged.
167
+ */
168
+ export declare function rowToEntity(raw: Record<string, unknown>, meta: TableMetadata): Record<string, unknown>;
169
+ /**
170
+ * Translate a PowDB error into a typed Turbine error. Handles BOTH transports,
171
+ * whose error shapes differ:
172
+ * - **networked** (`@zvndev/powdb-client`) tags errors with a *semantic*
173
+ * `.code` (`connect_failed`, `timeout`, `query_failed`, …);
174
+ * - **embedded** (`@zvndev/powdb-embedded` napi addon) tags EVERY error
175
+ * `code:'GenericFailure'`, so the class can only be recovered from the
176
+ * message text (`Execution("column 'email' is required …")`,
177
+ * `Execution("type mismatch …")`, `Parse(…)`, `StorageError(…)`).
178
+ *
179
+ * So we always run the unique-constraint and message-shape checks first (they
180
+ * fire for both transports), then fall through to the networked `.code` switch.
181
+ */
182
+ export declare function wrapPowdbError(err: unknown): Error;
183
+ type QueryArg = string | {
184
+ name?: string;
185
+ text: string;
186
+ values?: unknown[];
187
+ };
188
+ /**
189
+ * A {@link PgCompatPool} backed by a `@zvndev/powdb-client` `Pool`. The query
190
+ * `text` is **PowQL**, not SQL — {@link PowqlInterface} generates it. Rows come
191
+ * back as raw strings here; per-column JS coercion happens in `PowqlInterface`
192
+ * (it owns the schema metadata).
193
+ */
194
+ export declare class PowdbPool implements PgCompatPool {
195
+ readonly pool: PowdbClientPool;
196
+ private readonly toParam;
197
+ private closed;
198
+ /**
199
+ * Pool-level single-writer guard. PowDB holds one global write lock, so at
200
+ * most one transaction may be open across the whole pool. A `begin` issued
201
+ * while this is `true` is rejected (it would otherwise check out a second
202
+ * connection and block on the lock forever — the networked re-entrant hang).
203
+ */
204
+ private activeTransaction;
205
+ constructor(pool: PowdbClientPool, toParam?: (v: unknown, i: number) => PowdbParam);
206
+ /**
207
+ * Enforce the single-writer model on a transaction-control statement. Throws
208
+ * (before any query runs) if a `begin` arrives while a transaction is open;
209
+ * otherwise flips the pool-level flag. Returns the control kind so the caller
210
+ * can decide whether it even needs to hit the engine.
211
+ */
212
+ private guardTxControl;
213
+ query(text: QueryArg, values?: unknown[]): Promise<any>;
214
+ connect(): Promise<PgCompatPoolClient>;
215
+ end(): Promise<void>;
216
+ }
217
+ /** The embedded addon's result shape (matches {@link PowdbResult} but with optional fields). */
218
+ interface EmbeddedQueryResult {
219
+ kind: string;
220
+ columns?: string[];
221
+ rows?: string[][];
222
+ value?: string;
223
+ affected?: bigint;
224
+ message?: string;
225
+ }
226
+ /** A single in-process embedded database handle (`@zvndev/powdb-embedded`). */
227
+ interface EmbeddedDatabase {
228
+ query(powql: string): EmbeddedQueryResult;
229
+ querySql(sql: string): EmbeddedQueryResult;
230
+ queryReadonly(powql: string): EmbeddedQueryResult;
231
+ isPoisoned(): boolean;
232
+ /** WAL durability selector — `@zvndev/powdb-embedded` ≥ 0.7.1. */
233
+ setSyncMode?(mode: string): void;
234
+ }
235
+ /**
236
+ * Encode a JS value as a **PowQL literal** for the embedded driver, which takes
237
+ * no params array — `$N` placeholders must be materialized into the query text.
238
+ *
239
+ * This is the single place Turbine builds PowQL text from a value, so it is the
240
+ * security-critical surface. String encoding matches PowDB's lexer
241
+ * (`crates/query/src/lexer.rs`) exactly: a string literal is `"…"`, and inside
242
+ * it the lexer recognizes only the escapes `\"`, `\\`, `\n`, `\t` (any other
243
+ * `\x` drops the backslash and keeps `x`; every non-`\`/non-`"` char — raw
244
+ * newlines, CR, unicode — is taken literally). So we escape `\` → `\\` and
245
+ * `"` → `\"` (the only breakout vectors), render `\n`/`\t` as their recognized
246
+ * escapes, and leave everything else raw. Verified against the real engine:
247
+ * quotes, backslashes, `$N`, `"); drop … --`, raw CR, and emoji all round-trip
248
+ * as data and cannot break out of the literal or inject a second statement.
249
+ */
250
+ export declare function encodePowqlLiteral(value: unknown): string;
251
+ /**
252
+ * Substitute every `$N` placeholder in a generator-produced PowQL template with
253
+ * the encoded literal of `params[N-1]`. Safe because the template is produced by
254
+ * {@link PowqlInterface} and contains **no** user string literals — the only
255
+ * `$<digits>` tokens are genuine positional placeholders, so a single scan
256
+ * cannot accidentally rewrite a `$N` that is itself part of a value (values are
257
+ * params, never inlined into the template by the generator).
258
+ */
259
+ export declare function materializePowql(powql: string, params: unknown[]): string;
260
+ /**
261
+ * A {@link PgCompatPool} backed by an in-process `@zvndev/powdb-embedded`
262
+ * `Database`. The embedded addon takes **no params array** — its `query(powql)`
263
+ * accepts only a string — so this pool materializes each positional `$N` into a
264
+ * PowQL literal via {@link materializePowql} before handing the text to the
265
+ * engine. One handle, single connection: transaction keywords (`begin`/`commit`/
266
+ * `rollback`) are issued serially as ordinary queries.
267
+ */
268
+ export declare class PowdbEmbeddedPool implements PgCompatPool {
269
+ private readonly db;
270
+ private closed;
271
+ /**
272
+ * Single-writer guard. The embedded engine is one handle with one global
273
+ * write lock — only one transaction may be open at a time. A re-entrant
274
+ * `begin` (a fresh top-level `db.$transaction` opened inside an open one)
275
+ * would otherwise hit PowDB's raw "already in a transaction" parse error;
276
+ * this surfaces a typed error instead. (Nested `tx.$transaction` is caught
277
+ * earlier still, by the savepoint override in {@link powdbDialect}.)
278
+ */
279
+ private activeTransaction;
280
+ constructor(db: EmbeddedDatabase);
281
+ /** Enforce the single-writer model on a transaction-control statement. */
282
+ private guardTxControl;
283
+ private run;
284
+ query(text: QueryArg, values?: unknown[]): Promise<any>;
285
+ connect(): Promise<PgCompatPoolClient>;
286
+ end(): Promise<void>;
287
+ }
288
+ export { PowqlInterface } from './powql.js';
289
+ /** Options for {@link turbinePowDB}. */
290
+ export interface TurbinePowdbOptions extends Pick<TurbineConfig, 'logging' | 'defaultLimit' | 'warnOnUnlimited'> {
291
+ /** Max pooled connections (default 10). Networked transport only. */
292
+ connectionLimit?: number;
293
+ }
294
+ /**
295
+ * Selects the **embedded** transport — an in-process `@zvndev/powdb-embedded`
296
+ * database at the given data directory (no server, no socket). The value is the
297
+ * data dir path. Preview: Full-durability checkpoint-bound; built binaries ship
298
+ * for macOS (arm64/x64) and Unix-glibc only (Intel-mac/musl/Windows fall back to
299
+ * a from-source `npm run build`).
300
+ *
301
+ * @example
302
+ * ```ts
303
+ * const db = await turbinePowDB({ embedded: '/var/data/app.powdb' }, SCHEMA);
304
+ * // Faster writes (fsync off the commit path, bounded-loss) — requires addon >= 0.7.1:
305
+ * const fast = await turbinePowDB({ embedded: '/var/data/app.powdb', syncMode: 'normal' }, SCHEMA);
306
+ * ```
307
+ */
308
+ export interface TurbinePowdbEmbeddedTarget {
309
+ embedded: string;
310
+ /**
311
+ * WAL durability for the embedded engine (requires `@zvndev/powdb-embedded` ≥ 0.7.1):
312
+ * `'full'` (default — fsync per commit), `'normal'` (fsync off the commit path,
313
+ * ~15–40× faster writes, bounded loss on OS crash/power loss), `'off'` (bench-only).
314
+ */
315
+ syncMode?: 'full' | 'normal' | 'off';
316
+ /** Per-query memory budget in bytes (requires `@zvndev/powdb-embedded` ≥ 0.7.1). */
317
+ memoryLimit?: number;
318
+ }
319
+ /**
320
+ * Bind Turbine to PowDB. `target` is one of:
321
+ * - a `powdb://[user[:pass]@]host[:port][/db]` connection string → a
322
+ * **networked** `@zvndev/powdb-client` pool (consistency with
323
+ * `turbineMysql`/`turbineMssql`);
324
+ * - a host/port options object → a **networked** `@zvndev/powdb-client` pool;
325
+ * - an `{ embedded: <data-dir> }` object → an in-process
326
+ * `@zvndev/powdb-embedded` database (no server);
327
+ * - an already-constructed `@zvndev/powdb-client` `Pool` or {@link PowdbPool}
328
+ * (injection — you own its lifecycle and `disconnect()` is a no-op).
329
+ *
330
+ * On the networked transport the server version is probed and a clear
331
+ * {@link ConnectionError} is thrown if it is older than {@link MIN_POWDB_VERSION}
332
+ * (the `returning` keyword / int->float coercion fix Turbine relies on).
333
+ *
334
+ * Resolves to a `TurbineClient` whose `table()` accessors generate **PowQL** via
335
+ * {@link PowqlInterface}. The SQL `Dialect` is not involved.
336
+ */
337
+ export declare function turbinePowDB(target: string | PowdbConnOptions | PowdbClientPool | PowdbPool | TurbinePowdbEmbeddedTarget, schema: SchemaMetadata, options?: TurbinePowdbOptions): Promise<TurbineClient>;
338
+ //# sourceMappingURL=powdb.d.ts.map