turbine-orm 0.7.0 → 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/errors.js CHANGED
@@ -17,6 +17,9 @@ export const TurbineErrorCode = {
17
17
  FOREIGN_KEY_VIOLATION: 'TURBINE_E009',
18
18
  NOT_NULL_VIOLATION: 'TURBINE_E010',
19
19
  CHECK_VIOLATION: 'TURBINE_E011',
20
+ DEADLOCK_DETECTED: 'TURBINE_E012',
21
+ SERIALIZATION_FAILURE: 'TURBINE_E013',
22
+ PIPELINE: 'TURBINE_E014',
20
23
  };
21
24
  /** Base error class for all Turbine errors */
22
25
  export class TurbineError extends Error {
@@ -27,6 +30,47 @@ export class TurbineError extends Error {
27
30
  this.code = code;
28
31
  }
29
32
  }
33
+ let errorMessageMode = 'safe';
34
+ /**
35
+ * Set the global NotFoundError message mode. Called from the TurbineClient
36
+ * constructor when `TurbineConfig.errorMessages` is provided.
37
+ *
38
+ * - `'safe'` (default): the message includes only the keys of the where
39
+ * clause (e.g. `where: { id, email }`). Values are redacted.
40
+ * - `'verbose'`: the message includes the full JSON-serialized where
41
+ * clause (e.g. `where: {"id":1,"email":"alice@x.com"}`).
42
+ */
43
+ export function setErrorMessageMode(mode) {
44
+ errorMessageMode = mode;
45
+ }
46
+ /** Returns the current NotFoundError message mode. Exported for tests. */
47
+ export function getErrorMessageMode() {
48
+ return errorMessageMode;
49
+ }
50
+ /**
51
+ * Render a `where` clause for error messages. In 'safe' mode (the default),
52
+ * only the keys are shown; values are stripped to avoid leaking PII into logs.
53
+ * Nested AND/OR/NOT combinators are recursively rendered.
54
+ */
55
+ function renderWhereForMessage(where, mode) {
56
+ if (mode === 'verbose') {
57
+ try {
58
+ return JSON.stringify(where);
59
+ }
60
+ catch {
61
+ return '[unserializable]';
62
+ }
63
+ }
64
+ // safe mode: keys only
65
+ if (where === null || where === undefined)
66
+ return '';
67
+ if (typeof where !== 'object')
68
+ return '';
69
+ const keys = Object.keys(where);
70
+ if (keys.length === 0)
71
+ return '{}';
72
+ return `{ ${keys.join(', ')} }`;
73
+ }
30
74
  /**
31
75
  * Thrown when a record is not found (findUniqueOrThrow, findFirstOrThrow,
32
76
  * update/delete against a non-matching row, etc.)
@@ -36,8 +80,16 @@ export class TurbineError extends Error {
36
80
  * - `new NotFoundError({ table, where, operation, cause, message })`
37
81
  *
38
82
  * When called with an options object and no explicit `message`, a Prisma-style
39
- * message is built automatically, e.g.:
83
+ * message is built automatically. By default, only the where-clause keys are
84
+ * shown to avoid leaking PII into logs:
85
+ * `[turbine] findUniqueOrThrow on "users" found no record matching where: { id }`
86
+ *
87
+ * Set `setErrorMessageMode('verbose')` (or pass `errorMessages: 'verbose'` to
88
+ * the TurbineClient constructor) to include the full where values:
40
89
  * `[turbine] findUniqueOrThrow on "users" found no record matching where: {"id":1}`
90
+ *
91
+ * The full `where` object, `table`, and `operation` are always available as
92
+ * structured properties on the error instance regardless of mode.
41
93
  */
42
94
  export class NotFoundError extends TurbineError {
43
95
  table;
@@ -54,11 +106,11 @@ export class NotFoundError extends TurbineError {
54
106
  let message = input.message;
55
107
  if (!message) {
56
108
  if (operation && table) {
57
- const wherePart = where !== undefined ? ` matching where: ${JSON.stringify(where)}` : '';
109
+ const wherePart = where !== undefined ? ` matching where: ${renderWhereForMessage(where, errorMessageMode)}` : '';
58
110
  message = `[turbine] ${operation} on "${table}" found no record${wherePart}`;
59
111
  }
60
112
  else if (table) {
61
- const wherePart = where !== undefined ? ` matching where ${JSON.stringify(where)}` : '';
113
+ const wherePart = where !== undefined ? ` matching where ${renderWhereForMessage(where, errorMessageMode)}` : '';
62
114
  message = `[turbine] No record found in "${table}"${wherePart}`;
63
115
  }
64
116
  else {
@@ -194,6 +246,71 @@ export class NotNullViolationError extends TurbineError {
194
246
  this.table = table;
195
247
  }
196
248
  }
249
+ /**
250
+ * Thrown when Postgres detects a deadlock (pg code 40P01).
251
+ *
252
+ * This error is **retryable** — when caught, callers can safely retry the
253
+ * transaction (typically with backoff). Catch it explicitly:
254
+ *
255
+ * ```ts
256
+ * try {
257
+ * await db.$transaction(async (tx) => { ... });
258
+ * } catch (err) {
259
+ * if (err instanceof DeadlockError) {
260
+ * // safe to retry
261
+ * }
262
+ * }
263
+ * ```
264
+ */
265
+ export class DeadlockError extends TurbineError {
266
+ /** Marks this error as safe to retry */
267
+ isRetryable = true;
268
+ constraint;
269
+ constructor(opts = {}) {
270
+ const { constraint, cause } = opts;
271
+ let message = opts.message;
272
+ if (!message) {
273
+ const pgMessage = cause?.message;
274
+ message = pgMessage ? `[turbine] Deadlock detected: ${pgMessage}` : '[turbine] Deadlock detected';
275
+ }
276
+ super(TurbineErrorCode.DEADLOCK_DETECTED, message, { cause });
277
+ this.name = 'DeadlockError';
278
+ this.constraint = constraint;
279
+ }
280
+ }
281
+ /**
282
+ * Thrown when a Serializable transaction fails due to a serialization
283
+ * conflict (pg code 40001 — `could not serialize access due to ...`).
284
+ *
285
+ * This error is **retryable** — by Postgres documentation, the recommended
286
+ * response is to re-run the entire transaction. Catch it explicitly:
287
+ *
288
+ * ```ts
289
+ * try {
290
+ * await db.$transaction(async (tx) => { ... }, { isolationLevel: 'Serializable' });
291
+ * } catch (err) {
292
+ * if (err instanceof SerializationFailureError) {
293
+ * // safe to retry the whole transaction
294
+ * }
295
+ * }
296
+ * ```
297
+ */
298
+ export class SerializationFailureError extends TurbineError {
299
+ /** Marks this error as safe to retry */
300
+ isRetryable = true;
301
+ constructor(opts = {}) {
302
+ const { cause } = opts;
303
+ let message = opts.message;
304
+ if (!message) {
305
+ const pgMessage = cause?.message;
306
+ message = pgMessage
307
+ ? `[turbine] Serializable transaction conflict: ${pgMessage}`
308
+ : '[turbine] Serializable transaction conflict';
309
+ }
310
+ super(TurbineErrorCode.SERIALIZATION_FAILURE, message, { cause });
311
+ this.name = 'SerializationFailureError';
312
+ }
313
+ }
197
314
  /** Thrown when a CHECK constraint is violated (pg code 23514) */
198
315
  export class CheckConstraintError extends TurbineError {
199
316
  constraint;
@@ -214,6 +331,46 @@ export class CheckConstraintError extends TurbineError {
214
331
  this.table = table;
215
332
  }
216
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
+ }
217
374
  /**
218
375
  * Parse column names out of a pg `detail` string like:
219
376
  * "Key (email)=(foo@bar) already exists."
@@ -234,6 +391,8 @@ function parseColumnsFromDetail(detail) {
234
391
  * 23503 (foreign_key_violation) -> ForeignKeyError
235
392
  * 23502 (not_null_violation) -> NotNullViolationError
236
393
  * 23514 (check_violation) -> CheckConstraintError
394
+ * 40P01 (deadlock_detected) -> DeadlockError (retryable)
395
+ * 40001 (serialization_failure) -> SerializationFailureError (retryable)
237
396
  *
238
397
  * The original pg error is preserved as `.cause` on the wrapped error.
239
398
  */
@@ -271,6 +430,15 @@ export function wrapPgError(err) {
271
430
  table: e.table,
272
431
  cause: err,
273
432
  });
433
+ case '40P01':
434
+ return new DeadlockError({
435
+ constraint: e.constraint,
436
+ cause: err,
437
+ });
438
+ case '40001':
439
+ return new SerializationFailureError({
440
+ cause: err,
441
+ });
274
442
  default:
275
443
  return err;
276
444
  }
@@ -21,4 +21,10 @@ export declare function generate(options: GenerateOptions): {
21
21
  outDir: string;
22
22
  files: string[];
23
23
  };
24
+ /**
25
+ * Generate the contents of `types.ts` (entity interfaces, *Create / *Update,
26
+ * and *Relations brand-field interfaces). Exported so tests can pin the
27
+ * generator output without writing files to disk.
28
+ */
29
+ export declare function generateTypes(schema: SchemaMetadata): string;
24
30
  //# sourceMappingURL=generate.d.ts.map
package/dist/generate.js CHANGED
@@ -60,8 +60,33 @@ function generatedFileHeader() {
60
60
  '',
61
61
  ];
62
62
  }
63
- function generateTypes(schema) {
63
+ /**
64
+ * Generate the contents of `types.ts` (entity interfaces, *Create / *Update,
65
+ * and *Relations brand-field interfaces). Exported so tests can pin the
66
+ * generator output without writing files to disk.
67
+ */
68
+ export function generateTypes(schema) {
64
69
  const lines = [...generatedFileHeader()];
70
+ // We import UpdateOperatorInput so generated *Update types can express
71
+ // atomic increment / decrement / multiply / divide / set operators on
72
+ // numeric columns (TASK-3.4).
73
+ //
74
+ // RelationDescriptor is the brand-field interface that lets `WithResult`
75
+ // recurse through nested `with` clauses at any depth. The generator emits
76
+ // each `*Relations` member as a `RelationDescriptor<Target, Cardinality,
77
+ // TargetRelations>` so users get full deep `with`-clause type inference
78
+ // out of the box (TASK-2.1).
79
+ lines.push("import type { RelationDescriptor, UpdateOperatorInput } from 'turbine-orm';");
80
+ lines.push('');
81
+ // Pre-compute which tables have relations so we know whether to thread
82
+ // `${TargetType}Relations` (for deep inference) or `{}` (the no-relations
83
+ // default) into each `RelationDescriptor`. Built once up-front because
84
+ // relations can point at tables we haven't iterated to yet.
85
+ const tablesWithRelations = new Set();
86
+ for (const t of Object.values(schema.tables)) {
87
+ if (Object.keys(t.relations).length > 0)
88
+ tablesWithRelations.add(t.name);
89
+ }
65
90
  // Generate enum types
66
91
  for (const [enumName, labels] of Object.entries(schema.enums)) {
67
92
  const typeName = snakeToPascal(enumName);
@@ -103,27 +128,33 @@ function generateTypes(schema) {
103
128
  lines.push('};');
104
129
  lines.push('');
105
130
  // --- Update input type (all fields optional except PK) ---
131
+ // Numeric columns additionally accept `UpdateOperatorInput<number>` so
132
+ // users can write `{ viewCount: { increment: 1 } }` without an `as any`.
106
133
  const nonPkCols = table.columns.filter((c) => !table.primaryKey.includes(c.name));
107
134
  lines.push(`/** Input type for updating a row in \`${table.name}\` */`);
108
135
  lines.push(`export type ${typeName}Update = {`);
109
136
  for (const col of nonPkCols) {
110
- lines.push(` ${col.field}?: ${col.tsType};`);
137
+ lines.push(` ${col.field}?: ${updateFieldType(col.tsType)};`);
111
138
  }
112
139
  lines.push('};');
113
140
  lines.push('');
114
141
  // --- Relations map (for type-safe `with` clauses) ---
142
+ //
143
+ // Each relation is emitted as a `RelationDescriptor<Target, Cardinality,
144
+ // TargetRelations>` brand-field interface. This is what enables the
145
+ // recursive `WithResult` type to walk through nested `with` clauses at
146
+ // any depth — `RelationRelations<R[K]>` reads the third type parameter
147
+ // and threads it into the next recursion step. If the target table has
148
+ // no relations of its own, the descriptor uses `{}` (the default).
115
149
  const hasRelations = Object.keys(table.relations).length > 0;
116
150
  if (hasRelations) {
117
151
  lines.push(`/** Available relations for the \`${table.name}\` table */`);
118
152
  lines.push(`export interface ${typeName}Relations {`);
119
153
  for (const [relName, rel] of Object.entries(table.relations)) {
120
154
  const targetType = entityName(rel.to);
121
- if (rel.type === 'hasMany') {
122
- lines.push(` ${relName}: ${targetType}[];`);
123
- }
124
- else {
125
- lines.push(` ${relName}: ${targetType} | null;`);
126
- }
155
+ const cardinality = rel.type === 'hasMany' ? "'many'" : "'one'";
156
+ const targetRelations = tablesWithRelations.has(rel.to) ? `${targetType}Relations` : '{}';
157
+ lines.push(` ${relName}: RelationDescriptor<${targetType}, ${cardinality}, ${targetRelations}>;`);
127
158
  }
128
159
  lines.push('}');
129
160
  lines.push('');
@@ -227,8 +258,8 @@ function generateIndex(schema) {
227
258
  const tableEntries = Object.values(schema.tables);
228
259
  const lines = [
229
260
  ...generatedFileHeader(),
230
- "import { TurbineClient as BaseTurbineClient, QueryInterface } from 'turbine-orm';",
231
- "import type { TurbineConfig } from 'turbine-orm';",
261
+ "import { TurbineClient as BaseTurbineClient, TransactionClient as BaseTransactionClient, QueryInterface } from 'turbine-orm';",
262
+ "import type { TurbineConfig, TransactionOptions } from 'turbine-orm';",
232
263
  "import { SCHEMA } from './metadata.js';",
233
264
  ];
234
265
  // Import all entity types and relations maps
@@ -241,6 +272,41 @@ function generateIndex(schema) {
241
272
  }
242
273
  lines.push(`import type { ${typeImports.join(', ')} } from './types.js';`);
243
274
  lines.push('');
275
+ // -------------------------------------------------------------------------
276
+ // TypedTransactionClient — same typed table accessors as TurbineClient,
277
+ // but scoped to a single transaction connection. The runtime instance is
278
+ // an ordinary `TransactionClient` from turbine-orm; this declaration just
279
+ // teaches TypeScript about the auto-attached accessors so users get
280
+ // autocomplete inside `db.$transaction(async (tx) => tx.users.create(...))`.
281
+ // -------------------------------------------------------------------------
282
+ lines.push('/**');
283
+ lines.push(' * Transaction-scoped client with the same typed table accessors as TurbineClient.');
284
+ lines.push(' * Created automatically by `db.$transaction(async (tx) => ...)` — never instantiate');
285
+ lines.push(' * directly. All queries run on a dedicated connection within a BEGIN/COMMIT block.');
286
+ lines.push(' */');
287
+ lines.push('export class TypedTransactionClient extends BaseTransactionClient {');
288
+ for (const table of tableEntries) {
289
+ const typeName = entityName(table.name);
290
+ const accessor = snakeToCamelStr(table.name);
291
+ const hasRelations = Object.keys(table.relations).length > 0;
292
+ const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
293
+ lines.push(` /** Query interface for the \`${table.name}\` table (transaction-scoped) */`);
294
+ lines.push(` declare readonly ${accessor}: QueryInterface<${genericArgs}>;`);
295
+ }
296
+ lines.push('}');
297
+ lines.push('');
298
+ // Augment the class with a typed `$transaction` overload via interface
299
+ // merging. This adds an additional callable signature whose callback
300
+ // parameter is narrowed to `TypedTransactionClient`, while the base
301
+ // signature (callback parameter `BaseTransactionClient`) remains valid.
302
+ lines.push('export interface TypedTransactionClient {');
303
+ lines.push(' /**');
304
+ lines.push(' * Nested transaction via SAVEPOINT. The callback receives a typed');
305
+ lines.push(' * `TypedTransactionClient` so all table accessors auto-complete.');
306
+ lines.push(' */');
307
+ lines.push(' $transaction<R>(fn: (tx: TypedTransactionClient) => Promise<R>): Promise<R>;');
308
+ lines.push('}');
309
+ lines.push('');
244
310
  // Generate the client class with JSDoc
245
311
  lines.push('/**');
246
312
  lines.push(' * Generated Turbine client with typed table accessors.');
@@ -275,6 +341,22 @@ function generateIndex(schema) {
275
341
  lines.push(' }');
276
342
  lines.push('}');
277
343
  lines.push('');
344
+ // Augment TurbineClient via interface merging with a typed $transaction
345
+ // overload. The callback parameter is narrowed to `TypedTransactionClient`
346
+ // so users get autocomplete on `tx.users`, `tx.posts`, etc. The base
347
+ // signature (callback parameter `BaseTransactionClient`) remains valid as
348
+ // an overload, so prior usage continues to typecheck.
349
+ lines.push('export interface TurbineClient {');
350
+ lines.push(' /**');
351
+ lines.push(' * Run a callback inside a transaction. The callback receives a typed');
352
+ lines.push(' * `TypedTransactionClient` with autocompletion for every table accessor.');
353
+ lines.push(' */');
354
+ lines.push(' $transaction<R>(');
355
+ lines.push(' fn: (tx: TypedTransactionClient) => Promise<R>,');
356
+ lines.push(' options?: TransactionOptions,');
357
+ lines.push(' ): Promise<R>;');
358
+ lines.push('}');
359
+ lines.push('');
278
360
  // Factory function with JSDoc
279
361
  lines.push('/**');
280
362
  lines.push(' * Create a new Turbine client instance.');
@@ -316,4 +398,32 @@ function quoteIfNeeded(s) {
316
398
  function snakeToCamelStr(s) {
317
399
  return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
318
400
  }
401
+ /**
402
+ * Build the update-input field type for a column. Numeric columns become
403
+ * `T | UpdateOperatorInput<number> | null?` so users can write atomic
404
+ * operators (`{ increment: 1 }`, `{ multiply: 2 }`, etc.) without casts.
405
+ *
406
+ * The check is purely structural — if the column's TS type contains
407
+ * `'number'` (e.g. `number`, `number | null`), it's eligible. Other
408
+ * scalar types (`string`, `boolean`, `Date`, `unknown`, `Buffer`,
409
+ * `Date | null`, etc.) pass through unchanged.
410
+ */
411
+ function updateFieldType(tsType) {
412
+ // Strip parens for the regex check; preserve the original string in the output.
413
+ if (containsNumberType(tsType)) {
414
+ return `${tsType} | UpdateOperatorInput<number>`;
415
+ }
416
+ return tsType;
417
+ }
418
+ /**
419
+ * Detect whether a TypeScript type expression contains the `number` primitive
420
+ * as a top-level union member. Conservative on purpose — only matches
421
+ * `number`, `number | null`, `null | number`, etc., not `number[]` or
422
+ * `Record<string, number>`.
423
+ */
424
+ function containsNumberType(tsType) {
425
+ // Tokenize on `|` and check each member.
426
+ const parts = tsType.split('|').map((p) => p.trim());
427
+ return parts.some((p) => p === 'number');
428
+ }
319
429
  //# sourceMappingURL=generate.js.map
package/dist/index.d.ts CHANGED
@@ -33,11 +33,11 @@
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, ForeignKeyError, MigrationError, NotFoundError, NotNullViolationError, RelationError, 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';
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 RelationFilter, type TypedWithClause, type UpdateArgs, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type WithClause, type WithOptions, type WithResult, } from './query.js';
39
+ export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
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';
43
43
  export { ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnType, type ColumnTypeName, column, defineSchema, type SchemaDef, type TableDef, table, } from './schema-builder.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, ForeignKeyError, MigrationError, NotFoundError, NotNullViolationError, RelationError, 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