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/query.d.ts CHANGED
@@ -207,7 +207,17 @@ export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClaus
207
207
  timeout?: number;
208
208
  }
209
209
  export interface FindManyStreamArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> extends FindManyArgs<T, R, W> {
210
- /** Number of rows to fetch per internal FETCH batch (default: 100) */
210
+ /**
211
+ * Number of rows to fetch per internal FETCH batch (default: 1000).
212
+ *
213
+ * Trade-off: larger batches reduce network round-trips (important for
214
+ * high-latency connections like Neon) but increase per-batch memory.
215
+ * At 1000 rows x ~500 bytes/row the default is ~500 KB per batch.
216
+ *
217
+ * When the total result set fits within one batch, the stream avoids
218
+ * cursor overhead entirely (no BEGIN / DECLARE / CLOSE / COMMIT) by
219
+ * using a speculative `SELECT ... LIMIT batchSize+1` first.
220
+ */
211
221
  batchSize?: number;
212
222
  }
213
223
  export interface CreateArgs<T> {
@@ -370,6 +380,25 @@ export interface ArrayFilter {
370
380
  /** Check if array is empty: array_length(column, 1) IS NULL */
371
381
  isEmpty?: boolean;
372
382
  }
383
+ /** Cached SQL template paired with its prepared-statement name. */
384
+ export interface SqlCacheEntry {
385
+ sql: string;
386
+ name: string;
387
+ }
388
+ /**
389
+ * FNV-1a 64-bit hash returning 16 lowercase hex chars.
390
+ * Single-loop string iteration. Uses BigInt for 64-bit math.
391
+ *
392
+ * @internal Exported for testing only.
393
+ */
394
+ export declare function fnv1a64Hex(s: string): string;
395
+ /**
396
+ * Derive a prepared-statement name from a SQL string.
397
+ * Format: `t_<16hex>` — always 18 chars, well under NAMEDATALEN (63).
398
+ *
399
+ * @internal Exported for testing only.
400
+ */
401
+ export declare function sqlToPreparedName(sql: string): string;
373
402
  export interface DeferredQuery<T> {
374
403
  /** SQL text with $1, $2 placeholders */
375
404
  sql: string;
@@ -379,6 +408,8 @@ export interface DeferredQuery<T> {
379
408
  transform: (result: pg.QueryResult) => T;
380
409
  /** Tag for debugging / logging */
381
410
  tag: string;
411
+ /** Prepared statement name (t_<16hex>). Set when SQL cache is enabled. */
412
+ preparedName?: string;
382
413
  }
383
414
  /** Middleware function type — imported from client to avoid circular deps */
384
415
  type MiddlewareFn = (params: {
@@ -402,17 +433,36 @@ export interface QueryInterfaceOptions {
402
433
  * full tables).
403
434
  */
404
435
  warnOnUnlimited?: boolean;
436
+ /**
437
+ * Enable prepared statements. When true, queries are submitted with a
438
+ * `{ name, text, values }` object to the pg driver, which caches the
439
+ * parse+plan on the server per connection.
440
+ *
441
+ * Default: `true` for Turbine-owned pools, `false` for external pools
442
+ * (serverless drivers may not support named statements).
443
+ */
444
+ preparedStatements?: boolean;
445
+ /**
446
+ * Enable the SQL template cache. When true, repeated queries with the
447
+ * same shape (same keys, operators, relations — different values) reuse
448
+ * cached SQL text instead of rebuilding from scratch.
449
+ *
450
+ * Default: `true`. Set to `false` as a nuclear kill switch.
451
+ */
452
+ sqlCache?: boolean;
405
453
  }
406
454
  export declare class QueryInterface<T extends object, R extends object = {}> {
407
455
  private readonly pool;
408
456
  private readonly table;
409
457
  private readonly schema;
410
458
  private readonly tableMeta;
411
- /** SQL template cache: cacheKey → sql string (params are always positional $1,$2,...) */
412
- private readonly sqlCache;
459
+ /** SQL template cache: cacheKey → SqlCacheEntry (sql + prepared statement name) */
460
+ private readonly sqlTemplateCache;
413
461
  private readonly middlewares;
414
462
  private readonly defaultLimit?;
415
463
  private readonly warnOnUnlimited;
464
+ private readonly preparedStatementsEnabled;
465
+ private readonly sqlCacheEnabled;
416
466
  /**
417
467
  * Tracks tables that have already triggered an unlimited-query warning so
418
468
  * the user is not spammed once per row. Per-instance state — each
@@ -422,10 +472,31 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
422
472
  * cross-table sharing.
423
473
  */
424
474
  private readonly warnedTables;
475
+ /** Cache hit/miss counters for diagnostics */
476
+ private cacheHits;
477
+ private cacheMisses;
425
478
  /** Pre-computed column type lookups (avoids linear scans per query) */
426
479
  private readonly columnPgTypeMap;
427
480
  private readonly columnArrayTypeMap;
428
481
  constructor(pool: pg.Pool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[], options?: QueryInterfaceOptions);
482
+ /**
483
+ * Return cache hit/miss statistics for this QueryInterface instance.
484
+ * Useful for monitoring and benchmarking.
485
+ */
486
+ cacheStats(): {
487
+ hits: number;
488
+ misses: number;
489
+ hitRate: number;
490
+ size: number;
491
+ };
492
+ /**
493
+ * Look up or build a SQL template in the cache.
494
+ * On miss, calls `build()` to generate the SQL, stores the entry, and returns it.
495
+ * On hit, increments counters and returns the cached entry.
496
+ *
497
+ * When `sqlCache` is disabled, always calls `build()` without caching.
498
+ */
499
+ private acquireSql;
429
500
  /**
430
501
  * Reset the per-instance unlimited-query warning dedupe set.
431
502
  * Exposed for tests so a single test process can verify the warning fires
@@ -467,9 +538,21 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
467
538
  * Stream rows from a findMany query using PostgreSQL cursors.
468
539
  * Returns an AsyncIterable that yields individual rows, fetching in batches internally.
469
540
  *
470
- * Uses DECLARE CURSOR within a dedicated transaction on a single pooled connection.
471
- * The cursor is automatically closed and the connection released when iteration
472
- * completes or is terminated early (e.g. `break` from `for await`).
541
+ * **Speculative fast-path:** Before opening a cursor, issues a single
542
+ * `SELECT ... LIMIT batchSize+1`. If the result fits within `batchSize`,
543
+ * all rows are yielded immediately with zero cursor overhead (no BEGIN /
544
+ * DECLARE / CLOSE / COMMIT). Only when the result overflows does the
545
+ * method fall back to the full cursor path.
546
+ *
547
+ * **Cursor path:** Uses DECLARE CURSOR within a dedicated transaction on a
548
+ * single pooled connection. The cursor is automatically closed and the
549
+ * connection released when iteration completes or is terminated early
550
+ * (e.g. `break` from `for await`).
551
+ *
552
+ * **Snapshot semantics note:** The speculative fast-path runs outside a
553
+ * transaction. If the result overflows and the cursor path is opened, the
554
+ * cursor runs in its own transaction — spanning two separate snapshots.
555
+ * For strict single-snapshot semantics, wrap the call in `$transaction`.
473
556
  *
474
557
  * @example
475
558
  * ```ts
@@ -538,6 +621,59 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
538
621
  * clause numbering continues correctly afterward.
539
622
  */
540
623
  private buildSetClause;
624
+ /**
625
+ * Produce a value-invariant fingerprint of a where clause.
626
+ * Same keys + same operator shapes + same combinator structure => same string.
627
+ * Different values (e.g. id=1 vs id=999) => identical fingerprint.
628
+ *
629
+ * @internal Exposed as package-private for testing via class access.
630
+ */
631
+ fingerprintWhere(where: Record<string, unknown>): string;
632
+ /**
633
+ * Fingerprint a relation filter sub-where for some/every/none.
634
+ */
635
+ private fingerprintRelFilter;
636
+ /**
637
+ * Walk a where clause and push ONLY values into `params`, in the EXACT same
638
+ * order that `buildWhereClause` pushes them. Used on cache hit to fill params
639
+ * without rebuilding SQL.
640
+ *
641
+ * @internal Exposed as package-private for testing.
642
+ */
643
+ collectWhereParams(where: Record<string, unknown>, params: unknown[]): void;
644
+ /** Collect params from a relation filter sub-where. Mirrors buildSubWhereForRelation. */
645
+ private collectRelFilterParams;
646
+ /** Collect params from operator clauses. Mirrors buildOperatorClauses. */
647
+ private collectOperatorParams;
648
+ /** Collect params from JSON filter. Mirrors buildJsonFilterClauses. */
649
+ private collectJsonFilterParams;
650
+ /** Collect params from array filter. Mirrors buildArrayFilterClauses. */
651
+ private collectArrayFilterParams;
652
+ /**
653
+ * Produce a fingerprint for a `with` clause tree. Recursion mirrors
654
+ * buildSelectWithRelations / buildRelationSubquery.
655
+ *
656
+ * @internal Exposed as package-private for testing.
657
+ */
658
+ withFingerprint(withClause: WithClause | undefined, table?: string, depth?: number): string;
659
+ /**
660
+ * Collect params from a `with` clause tree. Mirrors buildSelectWithRelations +
661
+ * buildRelationSubquery param-push order.
662
+ */
663
+ private collectWithParams;
664
+ /**
665
+ * Collect params from a single relation subquery. Mirrors buildRelationSubquery.
666
+ */
667
+ private collectRelationSubqueryParams;
668
+ /**
669
+ * Fingerprint SET clauses for update/updateMany.
670
+ * Captures key names + operator types (set/increment/etc) but not values.
671
+ */
672
+ private fingerprintSet;
673
+ /**
674
+ * Collect SET params for update/updateMany. Mirrors buildSetClause param order.
675
+ */
676
+ private collectSetParams;
541
677
  /** Build WHERE clause from a where object (supports operators, NULL, OR) */
542
678
  private buildWhere;
543
679
  /**