turbine-orm 0.28.2 → 0.29.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.
@@ -911,14 +911,48 @@ class TurbineClient {
911
911
  * Execute a batch of {@link DeferredQuery} objects atomically inside one
912
912
  * transaction. Backs the `$transaction([...])` array overload. Reuses the raw
913
913
  * {@link transaction} machinery (BEGIN/COMMIT/ROLLBACK + connection release);
914
- * queries run sequentially on the single transaction connection and each
915
- * result is passed through its query's `transform`.
914
+ * each result is passed through its query's `transform`.
915
+ *
916
+ * Execution strategy on the single transaction connection:
917
+ * - **Sequential (default).** Await each statement's reply before sending
918
+ * the next. Safe on every driver; on a networked driver a batch of N
919
+ * costs N round trips.
920
+ * - **Pipelined.** When the checked-out connection advertises
921
+ * {@link PgCompatPoolClient.supportsPipelining} (its `query()` accepts
922
+ * concurrent calls and completes them in FIFO submission order), all
923
+ * statements are dispatched in one write burst and the replies are
924
+ * collected in order — ~1 round trip plus server time. Only taken when
925
+ * the dialect's writes surface rows directly (`resultStrategy` !==
926
+ * 'reselect'): a reselect plan is itself a sequential write+read pair.
927
+ *
928
+ * The two paths share one failure contract: the first (lowest-index) failed
929
+ * statement's error is thrown (wrapped via {@link wrapPgError}) and the
930
+ * surrounding transaction rolls back, so no statement's effect survives. The
931
+ * pipelined path drains every in-flight reply (`Promise.allSettled`) before
932
+ * rethrowing, which keeps the connection's request/reply pairing intact and
933
+ * means ROLLBACK is only issued once no statement is still in flight.
916
934
  */
917
935
  async transactionBatch(queries) {
918
936
  if (queries.length === 0) {
919
937
  return [];
920
938
  }
921
939
  return this.transaction(async (client) => {
940
+ const pipelined = client.supportsPipelining === true &&
941
+ this.dialect.resultStrategy !== 'reselect';
942
+ if (pipelined) {
943
+ // Dispatch every statement before awaiting any reply. The driver's
944
+ // FIFO guarantee makes settled[i] the reply to queries[i].
945
+ const settled = await Promise.allSettled(queries.map((dq) => client.query(dq.sql, dq.params)));
946
+ const results = [];
947
+ for (let i = 0; i < settled.length; i++) {
948
+ const outcome = settled[i];
949
+ if (outcome.status === 'rejected') {
950
+ throw (0, errors_js_1.wrapPgError)(outcome.reason);
951
+ }
952
+ results.push(queries[i].transform(outcome.value));
953
+ }
954
+ return results;
955
+ }
922
956
  const results = [];
923
957
  for (const dq of queries) {
924
958
  let raw;
@@ -523,7 +523,7 @@ function generateIndex(schema) {
523
523
  const lines = [
524
524
  ...generatedFileHeader(),
525
525
  "import { TurbineClient as BaseTurbineClient, TransactionClient as BaseTransactionClient, QueryInterface } from 'turbine-orm';",
526
- "import type { TurbineConfig, TransactionOptions } from 'turbine-orm';",
526
+ "import type { TurbineConfig, TransactionOptions, DeferredQuery, PipelineResults } from 'turbine-orm';",
527
527
  "import { SCHEMA } from './metadata.js';",
528
528
  ];
529
529
  // Import all entity types and relations maps
@@ -607,9 +607,13 @@ function generateIndex(schema) {
607
607
  lines.push('');
608
608
  // Augment TurbineClient via interface merging with a typed $transaction
609
609
  // overload. The callback parameter is narrowed to `TypedTransactionClient`
610
- // so users get autocomplete on `tx.users`, `tx.posts`, etc. The base
611
- // signature (callback parameter `BaseTransactionClient`) remains valid as
612
- // an overload, so prior usage continues to typecheck.
610
+ // so users get autocomplete on `tx.users`, `tx.posts`, etc.
611
+ //
612
+ // IMPORTANT: the merged member must be compatible with the base class's
613
+ // $transaction ON ITS OWN (TS2415) — since v0.26 the base method also has a
614
+ // batch-array overload (`$transaction([...queries])`), so the merged
615
+ // interface must redeclare BOTH signatures. Emitting only the callback form
616
+ // makes every generated client fail `tsc` with "incorrectly extends".
613
617
  lines.push('export interface TurbineClient {');
614
618
  lines.push(' /**');
615
619
  lines.push(' * Run a callback inside a transaction. The callback receives a typed');
@@ -619,6 +623,13 @@ function generateIndex(schema) {
619
623
  lines.push(' fn: (tx: TypedTransactionClient) => Promise<R>,');
620
624
  lines.push(' options?: TransactionOptions,');
621
625
  lines.push(' ): Promise<R>;');
626
+ lines.push(' /**');
627
+ lines.push(' * Batch form: run several deferred queries in one transaction and get');
628
+ lines.push(' * their results as a tuple (same as the base client).');
629
+ lines.push(' */');
630
+ lines.push(' $transaction<T extends readonly DeferredQuery<unknown>[]>(');
631
+ lines.push(' queries: readonly [...T],');
632
+ lines.push(' ): Promise<PipelineResults<T>>;');
622
633
  lines.push('}');
623
634
  lines.push('');
624
635
  // Factory function with JSDoc
package/dist/cjs/powdb.js CHANGED
@@ -23,8 +23,14 @@
23
23
  * `string` PKs hold UUID strings.
24
24
  * - **No JSON aggregation / link navigation** — single-query nested `with` is
25
25
  * impossible → it degrades to batched N+1 loaders (Phase B).
26
- * - **Single global write lock; no savepoints/isolation/pipelining** — nested
26
+ * - **Single global write lock; no savepoints/isolation** — nested
27
27
  * transactions / isolation / vector / LISTEN-NOTIFY / RLS throw.
28
+ * - **The wire protocol pipelines** — `@zvndev/powdb-client` writes each
29
+ * request frame immediately and matches replies FIFO, so multiple queries
30
+ * may be in flight on one connection. {@link PowdbPool}'s checked-out
31
+ * clients advertise `supportsPipelining`, which lets the batch
32
+ * `$transaction([...])` overload dispatch all statements in one write
33
+ * burst (~1 round trip) instead of one round trip per statement.
28
34
  *
29
35
  * `@zvndev/powdb-client` is an **optional peer dependency** loaded by dynamic
30
36
  * import; `npm i turbine-orm` still pulls only `pg`.
@@ -511,6 +517,16 @@ class PowdbPool {
511
517
  const client = await this.pool.acquire();
512
518
  let broken = false;
513
519
  return {
520
+ // The networked client's query() supports concurrent in-flight calls on
521
+ // one connection: every request frame is written to the socket
522
+ // immediately and replies are matched to callers in FIFO order. That
523
+ // lets the batch `$transaction([...])` path dispatch all statements in
524
+ // one write burst instead of paying a round trip per statement. Safe
525
+ // for the batch's rollback contract because a failed statement leaves
526
+ // the engine's transaction open (no aborted state, no auto-rollback) —
527
+ // later pipelined statements execute inside the same still-open
528
+ // transaction and the final `rollback` discards every effect.
529
+ supportsPipelining: true,
514
530
  // biome-ignore lint/suspicious/noExplicitAny: see query() above.
515
531
  query: async (text, values) => {
516
532
  const { text: powql, params } = normalizeQueryArgs(text, values);
package/dist/client.d.ts CHANGED
@@ -57,6 +57,17 @@ export interface PgCompatQueryResult<R = Record<string, unknown>> {
57
57
  export interface PgCompatPoolClient {
58
58
  query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<PgCompatQueryResult<R>>;
59
59
  release(err?: Error | boolean): void;
60
+ /**
61
+ * Optional driver capability: `true` when `query()` may be called again on
62
+ * this connection while earlier calls are still in flight, with replies
63
+ * delivered to callers in FIFO submission order. Drivers that set this let
64
+ * the batch `$transaction([...])` overload dispatch every statement in one
65
+ * write burst (~1 network round trip plus server time) instead of awaiting
66
+ * each reply before sending the next (N round trips). Leave unset for
67
+ * drivers (node-postgres included) whose batch path must stay strictly
68
+ * sequential.
69
+ */
70
+ readonly supportsPipelining?: boolean;
60
71
  }
61
72
  /**
62
73
  * Minimal pg-compatible pool. Pass any driver that satisfies this interface
@@ -564,8 +575,13 @@ export declare class TurbineClient {
564
575
  * of each query's transformed result; any failure rolls the whole batch back.
565
576
  *
566
577
  * Unlike {@link pipeline}, this never uses the extended-query pipeline
567
- * protocol it executes sequentially on the transaction connection, so it is
568
- * safe on every driver (including HTTP/serverless pools).
578
+ * protocol. Statements run on the single transaction connection: strictly
579
+ * sequentially by default (safe on every driver, including HTTP/serverless
580
+ * pools), or — when the checked-out connection advertises
581
+ * {@link PgCompatPoolClient.supportsPipelining} — dispatched in one write
582
+ * burst with replies collected in order, saving a network round trip per
583
+ * statement. Either way the failure contract is identical: the first
584
+ * (lowest-index) failure aborts the batch and rolls everything back.
569
585
  *
570
586
  * @example
571
587
  * ```ts
@@ -580,8 +596,26 @@ export declare class TurbineClient {
580
596
  * Execute a batch of {@link DeferredQuery} objects atomically inside one
581
597
  * transaction. Backs the `$transaction([...])` array overload. Reuses the raw
582
598
  * {@link transaction} machinery (BEGIN/COMMIT/ROLLBACK + connection release);
583
- * queries run sequentially on the single transaction connection and each
584
- * result is passed through its query's `transform`.
599
+ * each result is passed through its query's `transform`.
600
+ *
601
+ * Execution strategy on the single transaction connection:
602
+ * - **Sequential (default).** Await each statement's reply before sending
603
+ * the next. Safe on every driver; on a networked driver a batch of N
604
+ * costs N round trips.
605
+ * - **Pipelined.** When the checked-out connection advertises
606
+ * {@link PgCompatPoolClient.supportsPipelining} (its `query()` accepts
607
+ * concurrent calls and completes them in FIFO submission order), all
608
+ * statements are dispatched in one write burst and the replies are
609
+ * collected in order — ~1 round trip plus server time. Only taken when
610
+ * the dialect's writes surface rows directly (`resultStrategy` !==
611
+ * 'reselect'): a reselect plan is itself a sequential write+read pair.
612
+ *
613
+ * The two paths share one failure contract: the first (lowest-index) failed
614
+ * statement's error is thrown (wrapped via {@link wrapPgError}) and the
615
+ * surrounding transaction rolls back, so no statement's effect survives. The
616
+ * pipelined path drains every in-flight reply (`Promise.allSettled`) before
617
+ * rethrowing, which keeps the connection's request/reply pairing intact and
618
+ * means ROLLBACK is only issued once no statement is still in flight.
585
619
  */
586
620
  private transactionBatch;
587
621
  /**
package/dist/client.js CHANGED
@@ -903,14 +903,48 @@ export class TurbineClient {
903
903
  * Execute a batch of {@link DeferredQuery} objects atomically inside one
904
904
  * transaction. Backs the `$transaction([...])` array overload. Reuses the raw
905
905
  * {@link transaction} machinery (BEGIN/COMMIT/ROLLBACK + connection release);
906
- * queries run sequentially on the single transaction connection and each
907
- * result is passed through its query's `transform`.
906
+ * each result is passed through its query's `transform`.
907
+ *
908
+ * Execution strategy on the single transaction connection:
909
+ * - **Sequential (default).** Await each statement's reply before sending
910
+ * the next. Safe on every driver; on a networked driver a batch of N
911
+ * costs N round trips.
912
+ * - **Pipelined.** When the checked-out connection advertises
913
+ * {@link PgCompatPoolClient.supportsPipelining} (its `query()` accepts
914
+ * concurrent calls and completes them in FIFO submission order), all
915
+ * statements are dispatched in one write burst and the replies are
916
+ * collected in order — ~1 round trip plus server time. Only taken when
917
+ * the dialect's writes surface rows directly (`resultStrategy` !==
918
+ * 'reselect'): a reselect plan is itself a sequential write+read pair.
919
+ *
920
+ * The two paths share one failure contract: the first (lowest-index) failed
921
+ * statement's error is thrown (wrapped via {@link wrapPgError}) and the
922
+ * surrounding transaction rolls back, so no statement's effect survives. The
923
+ * pipelined path drains every in-flight reply (`Promise.allSettled`) before
924
+ * rethrowing, which keeps the connection's request/reply pairing intact and
925
+ * means ROLLBACK is only issued once no statement is still in flight.
908
926
  */
909
927
  async transactionBatch(queries) {
910
928
  if (queries.length === 0) {
911
929
  return [];
912
930
  }
913
931
  return this.transaction(async (client) => {
932
+ const pipelined = client.supportsPipelining === true &&
933
+ this.dialect.resultStrategy !== 'reselect';
934
+ if (pipelined) {
935
+ // Dispatch every statement before awaiting any reply. The driver's
936
+ // FIFO guarantee makes settled[i] the reply to queries[i].
937
+ const settled = await Promise.allSettled(queries.map((dq) => client.query(dq.sql, dq.params)));
938
+ const results = [];
939
+ for (let i = 0; i < settled.length; i++) {
940
+ const outcome = settled[i];
941
+ if (outcome.status === 'rejected') {
942
+ throw wrapPgError(outcome.reason);
943
+ }
944
+ results.push(queries[i].transform(outcome.value));
945
+ }
946
+ return results;
947
+ }
914
948
  const results = [];
915
949
  for (const dq of queries) {
916
950
  let raw;
package/dist/generate.js CHANGED
@@ -516,7 +516,7 @@ export function generateIndex(schema) {
516
516
  const lines = [
517
517
  ...generatedFileHeader(),
518
518
  "import { TurbineClient as BaseTurbineClient, TransactionClient as BaseTransactionClient, QueryInterface } from 'turbine-orm';",
519
- "import type { TurbineConfig, TransactionOptions } from 'turbine-orm';",
519
+ "import type { TurbineConfig, TransactionOptions, DeferredQuery, PipelineResults } from 'turbine-orm';",
520
520
  "import { SCHEMA } from './metadata.js';",
521
521
  ];
522
522
  // Import all entity types and relations maps
@@ -600,9 +600,13 @@ export function generateIndex(schema) {
600
600
  lines.push('');
601
601
  // Augment TurbineClient via interface merging with a typed $transaction
602
602
  // overload. The callback parameter is narrowed to `TypedTransactionClient`
603
- // so users get autocomplete on `tx.users`, `tx.posts`, etc. The base
604
- // signature (callback parameter `BaseTransactionClient`) remains valid as
605
- // an overload, so prior usage continues to typecheck.
603
+ // so users get autocomplete on `tx.users`, `tx.posts`, etc.
604
+ //
605
+ // IMPORTANT: the merged member must be compatible with the base class's
606
+ // $transaction ON ITS OWN (TS2415) — since v0.26 the base method also has a
607
+ // batch-array overload (`$transaction([...queries])`), so the merged
608
+ // interface must redeclare BOTH signatures. Emitting only the callback form
609
+ // makes every generated client fail `tsc` with "incorrectly extends".
606
610
  lines.push('export interface TurbineClient {');
607
611
  lines.push(' /**');
608
612
  lines.push(' * Run a callback inside a transaction. The callback receives a typed');
@@ -612,6 +616,13 @@ export function generateIndex(schema) {
612
616
  lines.push(' fn: (tx: TypedTransactionClient) => Promise<R>,');
613
617
  lines.push(' options?: TransactionOptions,');
614
618
  lines.push(' ): Promise<R>;');
619
+ lines.push(' /**');
620
+ lines.push(' * Batch form: run several deferred queries in one transaction and get');
621
+ lines.push(' * their results as a tuple (same as the base client).');
622
+ lines.push(' */');
623
+ lines.push(' $transaction<T extends readonly DeferredQuery<unknown>[]>(');
624
+ lines.push(' queries: readonly [...T],');
625
+ lines.push(' ): Promise<PipelineResults<T>>;');
615
626
  lines.push('}');
616
627
  lines.push('');
617
628
  // Factory function with JSDoc
package/dist/powdb.d.ts CHANGED
@@ -22,8 +22,14 @@
22
22
  * `string` PKs hold UUID strings.
23
23
  * - **No JSON aggregation / link navigation** — single-query nested `with` is
24
24
  * impossible → it degrades to batched N+1 loaders (Phase B).
25
- * - **Single global write lock; no savepoints/isolation/pipelining** — nested
25
+ * - **Single global write lock; no savepoints/isolation** — nested
26
26
  * transactions / isolation / vector / LISTEN-NOTIFY / RLS throw.
27
+ * - **The wire protocol pipelines** — `@zvndev/powdb-client` writes each
28
+ * request frame immediately and matches replies FIFO, so multiple queries
29
+ * may be in flight on one connection. {@link PowdbPool}'s checked-out
30
+ * clients advertise `supportsPipelining`, which lets the batch
31
+ * `$transaction([...])` overload dispatch all statements in one write
32
+ * burst (~1 round trip) instead of one round trip per statement.
27
33
  *
28
34
  * `@zvndev/powdb-client` is an **optional peer dependency** loaded by dynamic
29
35
  * import; `npm i turbine-orm` still pulls only `pg`.
package/dist/powdb.js CHANGED
@@ -22,8 +22,14 @@
22
22
  * `string` PKs hold UUID strings.
23
23
  * - **No JSON aggregation / link navigation** — single-query nested `with` is
24
24
  * impossible → it degrades to batched N+1 loaders (Phase B).
25
- * - **Single global write lock; no savepoints/isolation/pipelining** — nested
25
+ * - **Single global write lock; no savepoints/isolation** — nested
26
26
  * transactions / isolation / vector / LISTEN-NOTIFY / RLS throw.
27
+ * - **The wire protocol pipelines** — `@zvndev/powdb-client` writes each
28
+ * request frame immediately and matches replies FIFO, so multiple queries
29
+ * may be in flight on one connection. {@link PowdbPool}'s checked-out
30
+ * clients advertise `supportsPipelining`, which lets the batch
31
+ * `$transaction([...])` overload dispatch all statements in one write
32
+ * burst (~1 round trip) instead of one round trip per statement.
27
33
  *
28
34
  * `@zvndev/powdb-client` is an **optional peer dependency** loaded by dynamic
29
35
  * import; `npm i turbine-orm` still pulls only `pg`.
@@ -464,6 +470,16 @@ export class PowdbPool {
464
470
  const client = await this.pool.acquire();
465
471
  let broken = false;
466
472
  return {
473
+ // The networked client's query() supports concurrent in-flight calls on
474
+ // one connection: every request frame is written to the socket
475
+ // immediately and replies are matched to callers in FIFO order. That
476
+ // lets the batch `$transaction([...])` path dispatch all statements in
477
+ // one write burst instead of paying a round trip per statement. Safe
478
+ // for the batch's rollback contract because a failed statement leaves
479
+ // the engine's transaction open (no aborted state, no auto-rollback) —
480
+ // later pipelined statements execute inside the same still-open
481
+ // transaction and the final `rollback` discards every effect.
482
+ supportsPipelining: true,
467
483
  // biome-ignore lint/suspicious/noExplicitAny: see query() above.
468
484
  query: async (text, values) => {
469
485
  const { text: powql, params } = normalizeQueryArgs(text, values);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.28.2",
3
+ "version": "0.29.0",
4
4
  "description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
5
5
  "type": "module",
6
6
  "exports": {