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/query.d.ts CHANGED
@@ -72,12 +72,26 @@ export interface WithClause {
72
72
  * Relation-aware with clause. When R (the relations map) is provided,
73
73
  * only keys from R are autocompleted. Used in public method signatures
74
74
  * so the compiler can narrow the return type.
75
+ *
76
+ * For typed maps, each relation accepts either `true` (default include) or a
77
+ * {@link WithOptions} object whose nested `with` is keyed against the relation
78
+ * target's own relations interface — this is what enables deep
79
+ * `WithResult` inference.
75
80
  */
76
81
  export type TypedWithClause<R extends object = {}> = [keyof R] extends [never] ? WithClause : {
77
- [K in keyof R]?: true | WithOptions;
82
+ [K in keyof R]?: true | WithOptions<RelationRelations<R[K]> & object>;
78
83
  };
79
- export interface WithOptions<_T = unknown> {
80
- with?: WithClause;
84
+ /**
85
+ * Options for an included relation.
86
+ *
87
+ * Generic over `NestedR` — the relations interface of the *target* entity —
88
+ * so the nested `with` clause is autocompleted with the correct relation keys
89
+ * and so {@link WithResult} can recursively infer the return type. Defaults to
90
+ * `{}` (no relation suggestions) for callers that use the unparameterized
91
+ * {@link WithClause}.
92
+ */
93
+ export interface WithOptions<NestedR extends object = {}> {
94
+ with?: TypedWithClause<NestedR>;
81
95
  where?: Record<string, unknown>;
82
96
  orderBy?: Record<string, OrderDirection>;
83
97
  limit?: number;
@@ -86,18 +100,87 @@ export interface WithOptions<_T = unknown> {
86
100
  /** Exclude these fields from the relation */
87
101
  omit?: Record<string, boolean>;
88
102
  }
103
+ /**
104
+ * A relation descriptor used by generated `*Relations` interfaces to make deep
105
+ * `with` clause inference work. It bundles three pieces of information that
106
+ * `WithResult` needs to recurse through nested relations:
107
+ *
108
+ * - `__target` — the target entity type (e.g. `Post`)
109
+ * - `__cardinality`— `'many'` for hasMany, `'one'` for belongsTo / hasOne
110
+ * - `__relations` — the target entity's relations interface (for further recursion)
111
+ *
112
+ * **Generator contract (Track 3):** the code generator emits `*Relations`
113
+ * interfaces in the following shape so that `WithResult` can walk arbitrary
114
+ * nesting depth:
115
+ *
116
+ * ```ts
117
+ * export interface UserRelations {
118
+ * posts: RelationDescriptor<Post, 'many', PostRelations>;
119
+ * profile: RelationDescriptor<Profile, 'one', ProfileRelations>;
120
+ * }
121
+ * ```
122
+ *
123
+ * The brand fields are phantom — they exist only for type inference and have
124
+ * no runtime representation. The runtime always sees the parsed entity values
125
+ * (arrays for hasMany, single object or null for belongsTo / hasOne) — see the
126
+ * cardinality projection inside {@link WithResult}.
127
+ *
128
+ * **Backward compatibility:** legacy generated code emitted bare types
129
+ * (`posts: Post[]`, `profile: Profile | null`). `WithResult` still accepts that
130
+ * shape via a fallback branch — it just cannot recurse into nested `with` for
131
+ * those relations until the generator is updated.
132
+ *
133
+ * @typeParam Target - The entity type the relation points at.
134
+ * @typeParam Cardinality - `'many'` (array) or `'one'` (single object | null).
135
+ * @typeParam Relations - The target entity's own `*Relations` interface, or
136
+ * `{}` if the target has no relations of its own.
137
+ */
138
+ export interface RelationDescriptor<Target, Cardinality extends 'one' | 'many', Relations extends object = {}> {
139
+ readonly __target?: Target;
140
+ readonly __cardinality?: Cardinality;
141
+ readonly __relations?: Relations;
142
+ }
143
+ /** Extract the target entity from a relation descriptor or bare relation type. */
144
+ type RelationTarget<Rel> = Rel extends RelationDescriptor<infer Target, infer _C, infer _R> ? Target : Rel extends Array<infer Item> ? Item : Rel extends infer One | null ? One : Rel;
145
+ /** Extract the target's relations map from a relation descriptor (or `{}` for bare types). */
146
+ type RelationRelations<Rel> = Rel extends RelationDescriptor<infer _T, infer _C, infer R> ? R : {};
147
+ /** Project the target type into its runtime shape (array for many, single for one). */
148
+ type ApplyCardinality<Rel, Resolved> = Rel extends RelationDescriptor<infer _T, infer Cardinality, infer _R> ? Cardinality extends 'many' ? Resolved[] : Resolved | null : Rel extends Array<infer _Item> ? Resolved[] : Resolved | null;
89
149
  /**
90
150
  * Compute the result type when relations are included via `with`.
91
151
  *
92
- * - When R is the default ({}) or W is empty, resolves to plain T.
93
- * - When R is a real relations map and W specifies relation keys, merges matching
94
- * relation types from R onto T.
152
+ * Recursively walks the `with` clause, looking up each relation in `R` and:
153
+ *
154
+ * 1. If the relation is included with `true` (or no nested `with`), the
155
+ * relation's bare resolved type is grafted onto `T` (e.g. `posts: Post[]`).
156
+ * 2. If the relation is included with a nested `with: {...}`, the recursion
157
+ * looks up the target entity's relations interface (via the
158
+ * {@link RelationDescriptor} brand fields the generator emits) and
159
+ * recursively applies `WithResult` to the nested target. Cardinality is
160
+ * re-applied at each level so a hasMany relation stays an array even after
161
+ * deep nesting.
95
162
  *
96
- * @typeParam T - Base entity type (e.g. User)
97
- * @typeParam R - Relations map (e.g. { posts: Post[]; profile: Profile | null })
98
- * @typeParam W - The with clause object (e.g. { posts: true })
163
+ * **When `R` is `{}` (the default):** the recursion short-circuits and the
164
+ * function returns plain `T` preserving the existing untyped escape hatch
165
+ * for callers that have not generated typed clients.
166
+ *
167
+ * **When `R` does not contain the requested relations:** the unknown keys are
168
+ * ignored (the runtime will throw a `RelationError`, but the type system stays
169
+ * permissive so the legacy `WithClause` index signature still typechecks).
170
+ *
171
+ * @typeParam T - Base entity type (e.g. `User`).
172
+ * @typeParam R - Relations map for `T` (e.g.
173
+ * `{ posts: RelationDescriptor<Post, 'many', PostRelations>; ... }`).
174
+ * Legacy bare shapes (`{ posts: Post[]; profile: Profile | null }`)
175
+ * are also accepted, but cannot recurse beyond one level.
176
+ * @typeParam W - The `with` clause the user passed (e.g. `{ posts: true }` or
177
+ * `{ posts: { with: { comments: true } } }`).
99
178
  */
100
- export type WithResult<T, R extends object, W> = [keyof R] extends [never] ? T : [keyof W & keyof R] extends [never] ? T : T & Pick<R, keyof W & keyof R>;
179
+ export type WithResult<T, R extends object, W> = [keyof R] extends [never] ? T : W extends object ? [keyof W & keyof R] extends [never] ? T : T & {
180
+ [K in keyof W & keyof R]: W[K] extends true ? ApplyCardinality<R[K], RelationTarget<R[K]>> : W[K] extends {
181
+ with?: infer NestedW;
182
+ } ? NestedW extends object ? ApplyCardinality<R[K], WithResult<RelationTarget<R[K]>, RelationRelations<R[K]> & object, NestedW>> : ApplyCardinality<R[K], RelationTarget<R[K]>> : ApplyCardinality<R[K], RelationTarget<R[K]>>;
183
+ } : T;
101
184
  export interface FindUniqueArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> {
102
185
  where: WhereClause<T>;
103
186
  select?: Record<string, boolean>;
@@ -124,7 +207,17 @@ export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClaus
124
207
  timeout?: number;
125
208
  }
126
209
  export interface FindManyStreamArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> extends FindManyArgs<T, R, W> {
127
- /** 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
+ */
128
221
  batchSize?: number;
129
222
  }
130
223
  export interface CreateArgs<T> {
@@ -287,6 +380,25 @@ export interface ArrayFilter {
287
380
  /** Check if array is empty: array_length(column, 1) IS NULL */
288
381
  isEmpty?: boolean;
289
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;
290
402
  export interface DeferredQuery<T> {
291
403
  /** SQL text with $1, $2 placeholders */
292
404
  sql: string;
@@ -296,6 +408,8 @@ export interface DeferredQuery<T> {
296
408
  transform: (result: pg.QueryResult) => T;
297
409
  /** Tag for debugging / logging */
298
410
  tag: string;
411
+ /** Prepared statement name (t_<16hex>). Set when SQL cache is enabled. */
412
+ preparedName?: string;
299
413
  }
300
414
  /** Middleware function type — imported from client to avoid circular deps */
301
415
  type MiddlewareFn = (params: {
@@ -311,23 +425,84 @@ type MiddlewareFn = (params: {
311
425
  export interface QueryInterfaceOptions {
312
426
  /** Default LIMIT applied to findMany() when no limit is specified */
313
427
  defaultLimit?: number;
314
- /** Log a warning when findMany() is called without a limit */
428
+ /**
429
+ * Log a one-time warning when {@link QueryInterface.findMany} is called
430
+ * without a `limit`. Defaults to `true` so that accidental unbounded
431
+ * queries are surfaced loudly during development. Pass `false` to silence
432
+ * the warning entirely (e.g. for CLI tooling that intentionally streams
433
+ * full tables).
434
+ */
315
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;
316
453
  }
317
454
  export declare class QueryInterface<T extends object, R extends object = {}> {
318
455
  private readonly pool;
319
456
  private readonly table;
320
457
  private readonly schema;
321
458
  private readonly tableMeta;
322
- /** SQL template cache: cacheKey → sql string (params are always positional $1,$2,...) */
323
- private readonly sqlCache;
459
+ /** SQL template cache: cacheKey → SqlCacheEntry (sql + prepared statement name) */
460
+ private readonly sqlTemplateCache;
324
461
  private readonly middlewares;
325
462
  private readonly defaultLimit?;
326
463
  private readonly warnOnUnlimited;
464
+ private readonly preparedStatementsEnabled;
465
+ private readonly sqlCacheEnabled;
466
+ /**
467
+ * Tracks tables that have already triggered an unlimited-query warning so
468
+ * the user is not spammed once per row. Per-instance state — each
469
+ * QueryInterface is bound to a single table, so this set will only ever
470
+ * contain at most one entry, but using a Set keeps the API consistent with
471
+ * the audit's "Set<string>" guidance and leaves room for future
472
+ * cross-table sharing.
473
+ */
474
+ private readonly warnedTables;
475
+ /** Cache hit/miss counters for diagnostics */
476
+ private cacheHits;
477
+ private cacheMisses;
327
478
  /** Pre-computed column type lookups (avoids linear scans per query) */
328
479
  private readonly columnPgTypeMap;
329
480
  private readonly columnArrayTypeMap;
330
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;
500
+ /**
501
+ * Reset the per-instance unlimited-query warning dedupe set.
502
+ * Exposed for tests so a single test process can verify the warning fires
503
+ * exactly once per table without bleeding state between assertions.
504
+ */
505
+ resetUnlimitedWarnings(): void;
331
506
  /**
332
507
  * Execute a pool.query with an optional timeout.
333
508
  * If timeout is set, races the query against a timer and rejects on expiry.
@@ -347,14 +522,37 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
347
522
  findUnique<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): Promise<WithResult<T, R, W> | null>;
348
523
  buildFindUnique<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): DeferredQuery<T | null>;
349
524
  findMany<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): Promise<WithResult<T, R, W>[]>;
525
+ /**
526
+ * Emit a one-time `console.warn` when {@link findMany} is called without an
527
+ * explicit `limit`/`take` and `warnOnUnlimited` has not been disabled.
528
+ *
529
+ * Deduped per QueryInterface instance via {@link warnedTables} so a busy
530
+ * loop calling `db.users.findMany()` thousands of times only logs once.
531
+ * Suppressed when `defaultLimit` is configured (the caller has already
532
+ * opted in to a bounded query) and when the user passed an explicit
533
+ * `limit`, `take`, or `cursor`.
534
+ */
535
+ private maybeWarnUnlimited;
350
536
  buildFindMany<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): DeferredQuery<T[]>;
351
537
  /**
352
538
  * Stream rows from a findMany query using PostgreSQL cursors.
353
539
  * Returns an AsyncIterable that yields individual rows, fetching in batches internally.
354
540
  *
355
- * Uses DECLARE CURSOR within a dedicated transaction on a single pooled connection.
356
- * The cursor is automatically closed and the connection released when iteration
357
- * 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`.
358
556
  *
359
557
  * @example
360
558
  * ```ts
@@ -423,6 +621,59 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
423
621
  * clause numbering continues correctly afterward.
424
622
  */
425
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;
426
677
  /** Build WHERE clause from a where object (supports operators, NULL, OR) */
427
678
  private buildWhere;
428
679
  /**