turbine-orm 0.7.1 → 0.8.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/client.d.ts CHANGED
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import pg from 'pg';
25
25
  import { type ErrorMessageMode } from './errors.js';
26
- import { type PipelineResults } from './pipeline.js';
26
+ import { type PipelineOptions, type PipelineResults } from './pipeline.js';
27
27
  import { type DeferredQuery, QueryInterface, type QueryInterfaceOptions } from './query.js';
28
28
  import type { SchemaMetadata } from './schema.js';
29
29
  /**
@@ -125,6 +125,23 @@ export interface TurbineConfig {
125
125
  * programmatic access regardless of mode.
126
126
  */
127
127
  errorMessages?: ErrorMessageMode;
128
+ /**
129
+ * Enable prepared statements. Queries are submitted with `{ name, text, values }`
130
+ * to the pg driver, which caches the parse+plan on the server per connection.
131
+ *
132
+ * Default: `true` for Turbine-owned pools, `false` for external pools (serverless
133
+ * drivers may not support named statements).
134
+ *
135
+ * Override with `TURBINE_DISABLE_PREPARED=1` env var.
136
+ */
137
+ preparedStatements?: boolean;
138
+ /**
139
+ * Enable the SQL template cache. Repeated queries with the same shape reuse
140
+ * cached SQL text instead of rebuilding from scratch.
141
+ *
142
+ * Default: `true`. Set to `false` as a nuclear kill switch.
143
+ */
144
+ sqlCache?: boolean;
128
145
  }
129
146
  /** Parameters passed to middleware functions */
130
147
  export interface MiddlewareParams {
@@ -236,9 +253,21 @@ export declare class TurbineClient {
236
253
  /**
237
254
  * Execute multiple queries in a single database round-trip.
238
255
  *
239
- * Pass the result of any `.build*()` method on a table accessor.
256
+ * Two call styles:
257
+ * - `db.pipeline(q1, q2, q3)` — rest params (backward-compatible)
258
+ * - `db.pipeline([q1, q2, q3], { transactional: false })` — array + options
259
+ *
260
+ * On pg.Pool-backed connections with TCP, this uses the real Postgres
261
+ * extended-query pipeline protocol (one TCP flush, one round-trip).
262
+ * On HTTP-based drivers it falls back to sequential execution.
263
+ */
264
+ pipeline<T extends readonly DeferredQuery<unknown>[]>(...args: T | [T, PipelineOptions?]): Promise<PipelineResults<T>>;
265
+ /**
266
+ * Check whether the underlying pool supports the real pipeline protocol.
267
+ * Returns `true` for standard pg.Pool TCP connections, `false` for HTTP
268
+ * drivers (Neon HTTP, Vercel Postgres, etc.) and mock pools.
240
269
  */
241
- pipeline<T extends readonly DeferredQuery<unknown>[]>(...queries: T): Promise<PipelineResults<T>>;
270
+ pipelineSupported(): Promise<boolean>;
242
271
  /**
243
272
  * Execute a raw SQL query with parameter interpolation via tagged templates.
244
273
  *
package/dist/client.js CHANGED
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import pg from 'pg';
25
25
  import { setErrorMessageMode, TimeoutError, wrapPgError } from './errors.js';
26
- import { executePipeline } from './pipeline.js';
26
+ import { executePipeline, pipelineSupported } from './pipeline.js';
27
27
  import { QueryInterface } from './query.js';
28
28
  /** Maps isolation level names to SQL */
29
29
  const ISOLATION_LEVELS = {
@@ -128,9 +128,15 @@ export class TransactionClient {
128
128
  // Return a minimal pool-compatible object that routes queries
129
129
  // through the transaction client
130
130
  return {
131
- query: async (text, values) => {
131
+ query: async (textOrConfig, values) => {
132
132
  try {
133
- return await client.query(text, values);
133
+ if (typeof textOrConfig === 'string') {
134
+ return await client.query(textOrConfig, values);
135
+ }
136
+ // Object form for prepared statements: { name, text, values }
137
+ // pg.PoolClient.query accepts QueryConfig but the overloads make TS
138
+ // unhappy with the union, so we cast through unknown.
139
+ return await client.query(textOrConfig);
134
140
  }
135
141
  catch (err) {
136
142
  throw wrapPgError(err);
@@ -184,9 +190,13 @@ export class TurbineClient {
184
190
  }
185
191
  this.logging = config.logging ?? false;
186
192
  this.schema = schema;
193
+ // Respect env var kill switch
194
+ const envDisablePrepared = typeof process !== 'undefined' && process.env?.TURBINE_DISABLE_PREPARED === '1';
187
195
  this.queryOptions = {
188
196
  defaultLimit: config.defaultLimit,
189
197
  warnOnUnlimited: config.warnOnUnlimited,
198
+ preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? !config.pool),
199
+ sqlCache: config.sqlCache ?? true,
190
200
  };
191
201
  // Apply NotFoundError message redaction mode (default: safe — values are
192
202
  // stripped from messages to avoid leaking PII into error logs).
@@ -300,13 +310,41 @@ export class TurbineClient {
300
310
  /**
301
311
  * Execute multiple queries in a single database round-trip.
302
312
  *
303
- * Pass the result of any `.build*()` method on a table accessor.
313
+ * Two call styles:
314
+ * - `db.pipeline(q1, q2, q3)` — rest params (backward-compatible)
315
+ * - `db.pipeline([q1, q2, q3], { transactional: false })` — array + options
316
+ *
317
+ * On pg.Pool-backed connections with TCP, this uses the real Postgres
318
+ * extended-query pipeline protocol (one TCP flush, one round-trip).
319
+ * On HTTP-based drivers it falls back to sequential execution.
304
320
  */
305
- async pipeline(...queries) {
321
+ async pipeline(...args) {
322
+ let queries;
323
+ let options;
324
+ // Detect which overload was used
325
+ if (args.length > 0 &&
326
+ Array.isArray(args[0]) &&
327
+ args[0].every((item) => item && typeof item === 'object' && 'sql' in item)) {
328
+ // Array form: pipeline([q1, q2], opts?)
329
+ queries = args[0];
330
+ options = args[1];
331
+ }
332
+ else {
333
+ // Rest-param form: pipeline(q1, q2, q3)
334
+ queries = args;
335
+ }
306
336
  if (this.logging) {
307
337
  console.log(`[turbine] Pipeline: ${queries.length} queries — ${queries.map((q) => q.tag).join(', ')}`);
308
338
  }
309
- return executePipeline(this.pool, queries);
339
+ return executePipeline(this.pool, queries, options);
340
+ }
341
+ /**
342
+ * Check whether the underlying pool supports the real pipeline protocol.
343
+ * Returns `true` for standard pg.Pool TCP connections, `false` for HTTP
344
+ * drivers (Neon HTTP, Vercel Postgres, etc.) and mock pools.
345
+ */
346
+ async pipelineSupported() {
347
+ return pipelineSupported(this.pool);
310
348
  }
311
349
  // -------------------------------------------------------------------------
312
350
  // Raw SQL — tagged template literal escape hatch
package/dist/errors.d.ts CHANGED
@@ -19,6 +19,7 @@ export declare const TurbineErrorCode: {
19
19
  readonly CHECK_VIOLATION: "TURBINE_E011";
20
20
  readonly DEADLOCK_DETECTED: "TURBINE_E012";
21
21
  readonly SERIALIZATION_FAILURE: "TURBINE_E013";
22
+ readonly PIPELINE: "TURBINE_E014";
22
23
  };
23
24
  export type TurbineErrorCode = (typeof TurbineErrorCode)[keyof typeof TurbineErrorCode];
24
25
  /** Base error class for all Turbine errors */
@@ -207,6 +208,49 @@ export declare class CheckConstraintError extends TurbineError {
207
208
  cause?: unknown;
208
209
  });
209
210
  }
211
+ /** Result slot for a single query in a non-transactional pipeline */
212
+ export type PipelineResultSlot = {
213
+ status: 'ok';
214
+ value: unknown;
215
+ } | {
216
+ status: 'error';
217
+ error: Error;
218
+ };
219
+ /**
220
+ * Thrown when a non-transactional pipeline has partial failures.
221
+ *
222
+ * In non-transactional mode (`{ transactional: false }`), each query executes
223
+ * independently. If one or more queries fail, the pipeline rejects with a
224
+ * `PipelineError` that carries per-query results so callers can inspect which
225
+ * succeeded and which failed.
226
+ *
227
+ * ```ts
228
+ * try {
229
+ * await db.pipeline([q1, q2, q3], { transactional: false });
230
+ * } catch (err) {
231
+ * if (err instanceof PipelineError) {
232
+ * for (const slot of err.results) {
233
+ * if (slot.status === 'error') console.error(slot.error);
234
+ * }
235
+ * }
236
+ * }
237
+ * ```
238
+ */
239
+ export declare class PipelineError extends TurbineError {
240
+ /** Per-query results: each slot is either `{status:'ok', value}` or `{status:'error', error}` */
241
+ readonly results: PipelineResultSlot[];
242
+ /** Zero-based index of the first query that failed */
243
+ readonly failedIndex?: number;
244
+ /** Tag of the first query that failed (from DeferredQuery.tag) */
245
+ readonly failedTag?: string;
246
+ constructor(opts: {
247
+ message?: string;
248
+ results: PipelineResultSlot[];
249
+ failedIndex?: number;
250
+ failedTag?: string;
251
+ cause?: unknown;
252
+ });
253
+ }
210
254
  /**
211
255
  * Translate a pg driver error into a typed Turbine error.
212
256
  * If the error doesn't match a known constraint code, returns it unchanged.
package/dist/errors.js CHANGED
@@ -19,6 +19,7 @@ export const TurbineErrorCode = {
19
19
  CHECK_VIOLATION: 'TURBINE_E011',
20
20
  DEADLOCK_DETECTED: 'TURBINE_E012',
21
21
  SERIALIZATION_FAILURE: 'TURBINE_E013',
22
+ PIPELINE: 'TURBINE_E014',
22
23
  };
23
24
  /** Base error class for all Turbine errors */
24
25
  export class TurbineError extends Error {
@@ -330,6 +331,46 @@ export class CheckConstraintError extends TurbineError {
330
331
  this.table = table;
331
332
  }
332
333
  }
334
+ /**
335
+ * Thrown when a non-transactional pipeline has partial failures.
336
+ *
337
+ * In non-transactional mode (`{ transactional: false }`), each query executes
338
+ * independently. If one or more queries fail, the pipeline rejects with a
339
+ * `PipelineError` that carries per-query results so callers can inspect which
340
+ * succeeded and which failed.
341
+ *
342
+ * ```ts
343
+ * try {
344
+ * await db.pipeline([q1, q2, q3], { transactional: false });
345
+ * } catch (err) {
346
+ * if (err instanceof PipelineError) {
347
+ * for (const slot of err.results) {
348
+ * if (slot.status === 'error') console.error(slot.error);
349
+ * }
350
+ * }
351
+ * }
352
+ * ```
353
+ */
354
+ export class PipelineError extends TurbineError {
355
+ /** Per-query results: each slot is either `{status:'ok', value}` or `{status:'error', error}` */
356
+ results;
357
+ /** Zero-based index of the first query that failed */
358
+ failedIndex;
359
+ /** Tag of the first query that failed (from DeferredQuery.tag) */
360
+ failedTag;
361
+ constructor(opts) {
362
+ const { results, failedIndex, failedTag, cause } = opts;
363
+ const failedCount = results.filter((r) => r.status === 'error').length;
364
+ const message = opts.message ??
365
+ `[turbine] Pipeline completed with ${failedCount} error(s) out of ${results.length} queries` +
366
+ (failedTag ? ` (first failure: ${failedTag} at index ${failedIndex})` : '');
367
+ super(TurbineErrorCode.PIPELINE, message, { cause });
368
+ this.name = 'PipelineError';
369
+ this.results = results;
370
+ this.failedIndex = failedIndex;
371
+ this.failedTag = failedTag;
372
+ }
373
+ }
333
374
  /**
334
375
  * Parse column names out of a pg `detail` string like:
335
376
  * "Key (email)=(foo@bar) already exists."
package/dist/index.d.ts CHANGED
@@ -33,10 +33,10 @@
33
33
  * ```
34
34
  */
35
35
  export { type Middleware, type MiddlewareNext, type MiddlewareParams, type PgCompatPool, type PgCompatPoolClient, type PgCompatQueryResult, TransactionClient, type TransactionOptions, TurbineClient, type TurbineConfig, } from './client.js';
36
- export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, type ErrorMessageMode, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
36
+ export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, type ErrorMessageMode, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, PipelineError, type PipelineResultSlot, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
37
37
  export { type GenerateOptions, generate } from './generate.js';
38
38
  export { type IntrospectOptions, introspect } from './introspect.js';
39
- export { executePipeline, type PipelineResults } from './pipeline.js';
39
+ export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
40
40
  export { type AggregateArgs, type AggregateResult, type ArrayFilter, type CountArgs, type CreateArgs, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GroupByArgs, type JsonFilter, type OrderDirection, QueryInterface, type RelationDescriptor, type RelationFilter, type TypedWithClause, type UpdateArgs, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type WithClause, type WithOptions, type WithResult, } from './query.js';
41
41
  export type { ColumnMetadata, IndexMetadata, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
42
42
  export { camelToSnake, isDateType, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
package/dist/index.js CHANGED
@@ -35,13 +35,13 @@
35
35
  // Client
36
36
  export { TransactionClient, TurbineClient, } from './client.js';
37
37
  // Error types
38
- export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
38
+ export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, PipelineError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, ValidationError, wrapPgError, } from './errors.js';
39
39
  // Code generation
40
40
  export { generate } from './generate.js';
41
41
  // Introspection
42
42
  export { introspect } from './introspect.js';
43
43
  // Pipeline
44
- export { executePipeline } from './pipeline.js';
44
+ export { executePipeline, pipelineSupported } from './pipeline.js';
45
45
  // Query builder
46
46
  export { QueryInterface, } from './query.js';
47
47
  // Schema utilities
@@ -0,0 +1,94 @@
1
+ /**
2
+ * turbine-orm — Real Postgres pipeline protocol implementation
3
+ *
4
+ * Uses the pg extended-query protocol wire methods (parse/bind/describe/execute/sync)
5
+ * exposed on pg.Client's Connection object to send multiple queries in a single
6
+ * TCP flush. This achieves true 1-RTT pipeline execution instead of the sequential
7
+ * await-per-query approach.
8
+ *
9
+ * The approach (listener-swap):
10
+ * 1. Detach the pg.Client's event listeners from the Connection
11
+ * 2. Attach our own state-machine listeners
12
+ * 3. Cork the TCP stream, push all protocol messages, uncork (one TCP write)
13
+ * 4. Drive a state machine over backend response events
14
+ * 5. Restore original listeners and release the client
15
+ *
16
+ * This is the same pattern used by pg-cursor and pg-query-stream, but extended
17
+ * to handle N queries in a single pipeline.
18
+ */
19
+ import type { EventEmitter } from 'node:events';
20
+ import type { DeferredQuery } from './query.js';
21
+ /** The pg Connection object — an EventEmitter with wire-protocol methods */
22
+ export interface PgConnection extends EventEmitter {
23
+ stream: {
24
+ cork?: () => void;
25
+ uncork?: () => void;
26
+ writable?: boolean;
27
+ destroy?: (err?: Error) => void;
28
+ write?: (...args: unknown[]) => boolean;
29
+ };
30
+ parse(query: {
31
+ text: string;
32
+ name?: string;
33
+ types?: number[];
34
+ }): void;
35
+ bind(config: {
36
+ portal?: string;
37
+ statement?: string;
38
+ values?: unknown[];
39
+ binary?: boolean;
40
+ valueMapper?: (val: unknown, index: number) => unknown;
41
+ }): void;
42
+ describe(msg: {
43
+ type: 'S' | 'P';
44
+ name?: string;
45
+ }): void;
46
+ execute(config: {
47
+ portal?: string;
48
+ rows?: number;
49
+ }): void;
50
+ sync(): void;
51
+ }
52
+ /** A pg PoolClient with the internal fields we need */
53
+ export interface PgPoolClient {
54
+ connection: PgConnection;
55
+ /** pg.Client sets this to control query queue draining */
56
+ readyForQuery: boolean;
57
+ /** Type parser overrides (if the client has custom type parsers) */
58
+ _types?: unknown;
59
+ release(err?: Error | boolean): void;
60
+ }
61
+ export interface PipelineRunOptions {
62
+ /**
63
+ * Whether to wrap the pipeline in BEGIN/COMMIT (default: true).
64
+ * When true, all queries execute atomically. On error, ROLLBACK is sent.
65
+ * When false, each query gets its own Sync message for error isolation.
66
+ */
67
+ transactional?: boolean;
68
+ /** Timeout in milliseconds. If exceeded, the connection is destroyed. */
69
+ timeout?: number;
70
+ }
71
+ /**
72
+ * Execute multiple queries using the Postgres extended-query pipeline protocol.
73
+ *
74
+ * All protocol messages are buffered into a single TCP write via cork/uncork.
75
+ * The backend processes them in order and sends back results which our state
76
+ * machine collects.
77
+ *
78
+ * @param client - A pg PoolClient with an accessible Connection
79
+ * @param queries - Array of DeferredQuery descriptors
80
+ * @param options - Pipeline options (transactional, timeout)
81
+ * @returns Array of transformed results in the same order as queries
82
+ */
83
+ export declare function runPipelined<T extends readonly DeferredQuery<unknown>[]>(client: PgPoolClient, queries: T, options?: PipelineRunOptions): Promise<unknown[]>;
84
+ /**
85
+ * Check whether a pool client supports the extended-query pipeline protocol.
86
+ *
87
+ * Returns true if the client has a Connection object with the required wire
88
+ * protocol methods (parse, bind, describe, execute, sync) and is an EventEmitter.
89
+ *
90
+ * Returns false for HTTP-based drivers (Neon HTTP, Vercel Postgres), mock pools,
91
+ * and any pool that doesn't expose pg internals.
92
+ */
93
+ export declare function supportsExtendedPipeline(poolClient: unknown): poolClient is PgPoolClient;
94
+ //# sourceMappingURL=pipeline-submittable.d.ts.map