turbine-orm 0.34.0 → 0.35.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.
@@ -36,7 +36,12 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
36
36
  private readonly warnOnUnlimited;
37
37
  private readonly utcTimestamps;
38
38
  private readonly preparedStatementsEnabled;
39
- private readonly sqlCacheEnabled;
39
+ /**
40
+ * Whether the SQL template cache is active. Set once in the constructor.
41
+ * Mutable (not `readonly`) only so {@link withSqlCacheDisabled} can flip it
42
+ * off around a single synchronous compile (see {@link explain}).
43
+ */
44
+ private sqlCacheEnabled;
40
45
  private readonly dialect;
41
46
  /** Client-level default relation-loading strategy ('join' unless configured). */
42
47
  private readonly relationLoadStrategy;
@@ -290,6 +295,36 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
290
295
  private runFindUniqueBatched;
291
296
  buildFindUnique<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W, Record<string, boolean> | undefined, Record<string, boolean> | undefined>): DeferredQuery<T | null>;
292
297
  findMany<W extends TypedWithClause<R> = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args?: FindManyArgs<T, R, W, S, O>): Promise<QueryResult<T, R, W, S, O>[]>;
298
+ /**
299
+ * Return the engine's query plan for a {@link findMany}-shaped query as plain
300
+ * text lines: a diagnostic surface for inspecting how the database will run a
301
+ * query (index usage, join strategy, scan type).
302
+ *
303
+ * The compiled SELECT is prefixed with the dialect's explain syntax
304
+ * (Postgres `EXPLAIN`, SQLite `EXPLAIN QUERY PLAN`, MySQL `EXPLAIN
305
+ * FORMAT=TREE`) and run as a read. The findMany args are compiled with the
306
+ * SQL template cache disabled, so an explain never reads or writes the shared
307
+ * cache. Middleware is NOT applied: the returned rows are plan text, not
308
+ * entity rows. Each result row is flattened to one line by joining its column
309
+ * values with a single space (Postgres returns one `QUERY PLAN` text column,
310
+ * SQLite's `EXPLAIN QUERY PLAN` returns four, MySQL's tree format one).
311
+ *
312
+ * Only `findMany` shapes are supported (where / orderBy / with / limit /
313
+ * pagination). Engines whose plan cannot be requested in-band from a compiled
314
+ * query (SQL Server, whose SHOWPLAN is a session toggle) throw
315
+ * {@link UnsupportedFeatureError} (E017).
316
+ *
317
+ * The plan text itself is engine-owned and NOT covered by semver: its content
318
+ * and formatting can change with the underlying database version.
319
+ */
320
+ explain(args?: FindManyArgs<T, R>): Promise<string[]>;
321
+ /**
322
+ * Run `fn` with the SQL template cache forced off, restoring the prior state
323
+ * afterward. Used by {@link explain}, whose one-off prefixed statement must
324
+ * neither read nor write the shared cache. `fn` is synchronous, so no query
325
+ * interleaves between the toggle and its restore.
326
+ */
327
+ private withSqlCacheDisabled;
293
328
  /**
294
329
  * Emit a one-time `console.warn` when {@link findMany} is called without an
295
330
  * explicit `limit`/`take` and `warnOnUnlimited` has not been disabled.
@@ -141,6 +141,11 @@ export class QueryInterface {
141
141
  warnOnUnlimited;
142
142
  utcTimestamps;
143
143
  preparedStatementsEnabled;
144
+ /**
145
+ * Whether the SQL template cache is active. Set once in the constructor.
146
+ * Mutable (not `readonly`) only so {@link withSqlCacheDisabled} can flip it
147
+ * off around a single synchronous compile (see {@link explain}).
148
+ */
144
149
  sqlCacheEnabled;
145
150
  dialect;
146
151
  /** Client-level default relation-loading strategy ('join' unless configured). */
@@ -834,6 +839,61 @@ export class QueryInterface {
834
839
  return deferred.transform(result);
835
840
  });
836
841
  }
842
+ /**
843
+ * Return the engine's query plan for a {@link findMany}-shaped query as plain
844
+ * text lines: a diagnostic surface for inspecting how the database will run a
845
+ * query (index usage, join strategy, scan type).
846
+ *
847
+ * The compiled SELECT is prefixed with the dialect's explain syntax
848
+ * (Postgres `EXPLAIN`, SQLite `EXPLAIN QUERY PLAN`, MySQL `EXPLAIN
849
+ * FORMAT=TREE`) and run as a read. The findMany args are compiled with the
850
+ * SQL template cache disabled, so an explain never reads or writes the shared
851
+ * cache. Middleware is NOT applied: the returned rows are plan text, not
852
+ * entity rows. Each result row is flattened to one line by joining its column
853
+ * values with a single space (Postgres returns one `QUERY PLAN` text column,
854
+ * SQLite's `EXPLAIN QUERY PLAN` returns four, MySQL's tree format one).
855
+ *
856
+ * Only `findMany` shapes are supported (where / orderBy / with / limit /
857
+ * pagination). Engines whose plan cannot be requested in-band from a compiled
858
+ * query (SQL Server, whose SHOWPLAN is a session toggle) throw
859
+ * {@link UnsupportedFeatureError} (E017).
860
+ *
861
+ * The plan text itself is engine-owned and NOT covered by semver: its content
862
+ * and formatting can change with the underlying database version.
863
+ */
864
+ async explain(args) {
865
+ const explainSyntax = this.dialect.explainQuery;
866
+ if (!explainSyntax) {
867
+ throw new UnsupportedFeatureError('explain()', this.dialect.name, 'This engine cannot explain a compiled query in-band.');
868
+ }
869
+ // Compile the findMany SQL with the cache disabled: the prefixed EXPLAIN
870
+ // statement is a one-off diagnostic and must never read or write the shared
871
+ // query-template cache.
872
+ const deferred = this.withSqlCacheDisabled(() => this.buildFindMany(args));
873
+ const sql = `${explainSyntax.prefix} ${deferred.sql}`;
874
+ this.currentAction = 'explain';
875
+ // No preparedName: keep the diagnostic statement out of the prepared path.
876
+ const result = await this.queryWithTimeout(sql, deferred.params, args?.timeout);
877
+ return result.rows.map((row) => Object.values(row)
878
+ .map((value) => (typeof value === 'string' ? value : String(value)))
879
+ .join(' '));
880
+ }
881
+ /**
882
+ * Run `fn` with the SQL template cache forced off, restoring the prior state
883
+ * afterward. Used by {@link explain}, whose one-off prefixed statement must
884
+ * neither read nor write the shared cache. `fn` is synchronous, so no query
885
+ * interleaves between the toggle and its restore.
886
+ */
887
+ withSqlCacheDisabled(fn) {
888
+ const prev = this.sqlCacheEnabled;
889
+ this.sqlCacheEnabled = false;
890
+ try {
891
+ return fn();
892
+ }
893
+ finally {
894
+ this.sqlCacheEnabled = prev;
895
+ }
896
+ }
837
897
  /**
838
898
  * Emit a one-time `console.warn` when {@link findMany} is called without an
839
899
  * explicit `limit`/`take` and `warnOnUnlimited` has not been disabled.
@@ -111,8 +111,12 @@ export interface QueryInterfaceOptions {
111
111
  */
112
112
  utcTimestamps?: boolean;
113
113
  /**
114
- * Client-level default relation-loading strategy for `with` clauses. Per-query
115
- * `relationLoadStrategy` args override this; both default to `'join'`.
114
+ * Client-level default relation-loading strategy for `with` clauses; a
115
+ * per-query `relationLoadStrategy` arg overrides it. On SQL engines the default
116
+ * is `'join'` (one single-statement `json_agg` query). On PowDB the default is
117
+ * the batched loaders, and `'join'` opts INTO native server-side joins where
118
+ * eligible (ineligible relations fall back to the loaders per-relation and
119
+ * silently; see the PowDB docs).
116
120
  */
117
121
  relationLoadStrategy?: RelationLoadStrategy;
118
122
  /**
@@ -7,15 +7,19 @@ export type OrderDirection = 'asc' | 'desc';
7
7
  /**
8
8
  * How a query resolves its `with` relations.
9
9
  *
10
- * - `'join'` (default) — one SQL statement with correlated
11
- * `json_agg(json_build_object(...))` subqueries. One round-trip; an index
12
- * seek per parent row when the child FK is indexed.
13
- * - `'batched'` run the base query, then ONE flat follow-up query per
10
+ * - `'join'`: one SQL statement with correlated
11
+ * `json_agg(json_build_object(...))` subqueries. One round-trip, an index
12
+ * seek per parent row when the child FK is indexed. On PowDB, `'join'`
13
+ * instead opts into native PowQL server-side joins where eligible.
14
+ * - `'batched'`: run the base query, then ONE flat follow-up query per
14
15
  * relation (`WHERE fk = ANY($1)`), stitching children client-side. D levels
15
16
  * cost D extra round-trips, but each is a single key-set lookup and rows come
16
- * back flat a win when FK columns are unindexed or result sets are huge.
17
+ * back flat (a win when FK columns are unindexed or result sets are huge).
17
18
  *
18
- * Precedence: per-query arg > client `relationLoadStrategy` config > `'join'`.
19
+ * Precedence: per-query arg > client `relationLoadStrategy` config > the engine
20
+ * default. On SQL engines the default is `'join'`; on PowDB the default is the
21
+ * batched loaders (an ineligible relation falls back to them per-relation and
22
+ * silently even when `'join'` is requested).
19
23
  */
20
24
  export type RelationLoadStrategy = 'join' | 'batched';
21
25
  /**
package/dist/sqlite.js CHANGED
@@ -372,6 +372,9 @@ export const sqliteDialect = {
372
372
  supportsAdvisoryLock: false,
373
373
  // No FROM-clause LATERAL: the opt-in lateral pick plan is Postgres-only.
374
374
  supportsLateralJoin: false,
375
+ // SQLite explains a compiled query with `EXPLAIN QUERY PLAN` (four columns:
376
+ // id, parent, notused, detail), overriding the inherited Postgres `EXPLAIN`.
377
+ explainQuery: { prefix: 'EXPLAIN QUERY PLAN' },
375
378
  // json_group_array / json_object have no inline ORDER BY argument, so every
376
379
  // ordered to-many relation is forced through the inner-subquery rewrite.
377
380
  aggSupportsInlineOrderBy: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.34.0",
3
+ "version": "0.35.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": {
@@ -103,8 +103,8 @@
103
103
  "@size-limit/esbuild": "^12.1.0",
104
104
  "@size-limit/file": "^12.1.0",
105
105
  "@types/node": "^26.1.0",
106
- "@zvndev/powdb-client": "^0.13.0",
107
- "@zvndev/powdb-embedded": "^0.13.0",
106
+ "@zvndev/powdb-client": "^0.15.0",
107
+ "@zvndev/powdb-embedded": "^0.15.0",
108
108
  "c8": "^11.0.0",
109
109
  "husky": "^9.1.7",
110
110
  "lint-staged": "^17.0.8",