turbine-orm 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -40
- package/dist/cjs/cli/index.js +102 -10
- package/dist/cjs/cli/migrate.js +50 -13
- package/dist/cjs/cli/studio-ui.generated.js +6 -0
- package/dist/cjs/cli/studio.js +641 -0
- package/dist/cjs/client.js +43 -5
- package/dist/cjs/errors.js +43 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/pipeline-submittable.js +403 -0
- package/dist/cjs/pipeline.js +90 -37
- package/dist/cjs/query.js +865 -141
- package/dist/cjs/schema-builder.js +23 -3
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +103 -11
- package/dist/cli/migrate.d.ts +16 -0
- package/dist/cli/migrate.js +49 -13
- package/dist/cli/studio-ui.generated.d.ts +2 -0
- package/dist/cli/studio-ui.generated.js +4 -0
- package/dist/cli/studio.d.ts +75 -0
- package/dist/cli/studio.js +627 -0
- package/dist/client.d.ts +32 -3
- package/dist/client.js +44 -6
- package/dist/errors.d.ts +44 -0
- package/dist/errors.js +41 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/pipeline-submittable.d.ts +94 -0
- package/dist/pipeline-submittable.js +397 -0
- package/dist/pipeline.d.ts +37 -9
- package/dist/pipeline.js +89 -37
- package/dist/query.d.ts +142 -6
- package/dist/query.js +863 -141
- package/dist/schema-builder.js +23 -3
- package/package.json +8 -4
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 (
|
|
131
|
+
query: async (textOrConfig, values) => {
|
|
132
132
|
try {
|
|
133
|
-
|
|
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
|
-
*
|
|
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(...
|
|
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
|