turbine-orm 0.27.1 → 0.28.1

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.
Files changed (64) hide show
  1. package/README.md +19 -15
  2. package/dist/cjs/cli/config.js +20 -3
  3. package/dist/cjs/cli/index.js +273 -71
  4. package/dist/cjs/cli/mcp.js +788 -0
  5. package/dist/cjs/cli/migrate.js +95 -20
  6. package/dist/cjs/cli/studio.js +3 -2
  7. package/dist/cjs/client.js +267 -34
  8. package/dist/cjs/dialect.js +2 -0
  9. package/dist/cjs/errors.js +15 -1
  10. package/dist/cjs/generate.js +171 -7
  11. package/dist/cjs/index.js +4 -1
  12. package/dist/cjs/introspect.js +177 -4
  13. package/dist/cjs/powdb.js +1 -1
  14. package/dist/cjs/powql.js +1 -1
  15. package/dist/cjs/query/batched-loader.js +148 -0
  16. package/dist/cjs/query/builder.js +763 -401
  17. package/dist/cjs/query/deferred.js +7 -0
  18. package/dist/cjs/query/filters.js +251 -0
  19. package/dist/cjs/schema-builder.js +59 -4
  20. package/dist/cjs/schema-sql.js +315 -6
  21. package/dist/cjs/seed.js +66 -0
  22. package/dist/cli/config.d.ts +9 -2
  23. package/dist/cli/config.js +19 -3
  24. package/dist/cli/index.d.ts +52 -1
  25. package/dist/cli/index.js +272 -74
  26. package/dist/cli/mcp.d.ts +17 -0
  27. package/dist/cli/mcp.js +781 -0
  28. package/dist/cli/migrate.d.ts +37 -0
  29. package/dist/cli/migrate.js +92 -20
  30. package/dist/cli/studio.d.ts +3 -2
  31. package/dist/cli/studio.js +3 -2
  32. package/dist/client.d.ts +136 -1
  33. package/dist/client.js +267 -34
  34. package/dist/dialect.d.ts +17 -0
  35. package/dist/dialect.js +2 -0
  36. package/dist/errors.js +15 -1
  37. package/dist/generate.d.ts +17 -0
  38. package/dist/generate.js +171 -10
  39. package/dist/index.d.ts +4 -3
  40. package/dist/index.js +2 -0
  41. package/dist/introspect.d.ts +20 -1
  42. package/dist/introspect.js +175 -4
  43. package/dist/powdb.d.ts +1 -1
  44. package/dist/powdb.js +1 -1
  45. package/dist/powql.d.ts +1 -1
  46. package/dist/powql.js +1 -1
  47. package/dist/query/batched-loader.d.ts +29 -2
  48. package/dist/query/batched-loader.js +148 -1
  49. package/dist/query/builder.d.ts +151 -122
  50. package/dist/query/builder.js +701 -339
  51. package/dist/query/deferred.d.ts +130 -0
  52. package/dist/query/deferred.js +6 -0
  53. package/dist/query/filters.d.ts +120 -0
  54. package/dist/query/filters.js +232 -0
  55. package/dist/query/index.d.ts +1 -1
  56. package/dist/query/types.d.ts +113 -8
  57. package/dist/schema-builder.d.ts +73 -8
  58. package/dist/schema-builder.js +59 -4
  59. package/dist/schema-sql.d.ts +67 -0
  60. package/dist/schema-sql.js +310 -6
  61. package/dist/schema.d.ts +53 -0
  62. package/dist/seed.d.ts +4 -0
  63. package/dist/seed.js +63 -0
  64. package/package.json +4 -5
@@ -47,9 +47,9 @@
47
47
  * @module
48
48
  */
49
49
  import type pg from 'pg';
50
- import { type SchemaMetadata, type TableMetadata } from '../schema.js';
50
+ import { type RelationDef, type SchemaMetadata, type TableMetadata } from '../schema.js';
51
51
  import type { ReselectExecutor } from './builder.js';
52
- import type { WithClause } from './types.js';
52
+ import type { SkipGlobalFilters, WithClause, WithCount } from './types.js';
53
53
  /**
54
54
  * A DeferredQuery, minimally typed for what the loader consumes. Kept local to
55
55
  * avoid a value import of builder.ts (which imports this module).
@@ -88,6 +88,23 @@ export interface RelationLoadContext {
88
88
  inClauseParam: (values: unknown[]) => unknown;
89
89
  /** Placeholder for a 1-indexed parameter position (PG: `$n`). */
90
90
  paramPlaceholder: (index: number) => string;
91
+ /**
92
+ * The query's `skipGlobalFilters` opt-out, threaded onto every child
93
+ * `buildFindMany` so relation row loads honor (or skip) the target table's
94
+ * global filter exactly as the join strategy would.
95
+ */
96
+ skipGlobalFilters?: SkipGlobalFilters;
97
+ /**
98
+ * Render `table`'s global filter against `alias` for a raw follow-up query
99
+ * (the batched `_count`), numbering its `$n` placeholders AFTER
100
+ * `precedingParams` already-bound params. Returns `null` when no filter
101
+ * applies. Provided by the owning QueryInterface so this module needs no
102
+ * filter machinery of its own.
103
+ */
104
+ tableGlobalFilter?: (table: string, alias: string, precedingParams: number) => {
105
+ clause: string;
106
+ params: unknown[];
107
+ } | null;
91
108
  }
92
109
  /**
93
110
  * Adjust a `select`/`omit` pair so that `fields` are guaranteed present in the
@@ -111,6 +128,16 @@ export declare function stripFields(rows: Record<string, unknown>[], fields: str
111
128
  * The caller adds these to the base query and strips the added ones afterwards.
112
129
  */
113
130
  export declare function neededParentKeyFields(parentMeta: TableMetadata, withClause: WithClause): string[];
131
+ /**
132
+ * Resolve the set of to-many relations a `_count` spec selects. `true` counts
133
+ * every to-many relation (hasMany + manyToMany) of the table; the record form
134
+ * counts only the enabled names. Shared by the join builder and the batched
135
+ * loader so both count the exact same relations.
136
+ *
137
+ * Errors: E005 ({@link RelationError}) for an unknown relation name, E003
138
+ * ({@link ValidationError}) when a named relation is to-one.
139
+ */
140
+ export declare function resolveCountRelations(parentMeta: TableMetadata, countSpec: WithCount): RelationDef[];
114
141
  /**
115
142
  * Load every relation in `withClause` for `parents` and attach it onto each row
116
143
  * in place. Mirrors the join strategy's output shape exactly. Recurses for nested
@@ -46,7 +46,7 @@
46
46
  *
47
47
  * @module
48
48
  */
49
- import { CircularRelationError, UnsupportedFeatureError, ValidationError } from '../errors.js';
49
+ import { CircularRelationError, RelationError, UnsupportedFeatureError, ValidationError } from '../errors.js';
50
50
  import { normalizeKeyColumns } from '../schema.js';
51
51
  /**
52
52
  * Max parent keys per follow-up query. On Postgres the whole key set travels as
@@ -114,6 +114,14 @@ export function neededParentKeyFields(parentMeta, withClause) {
114
114
  for (const [relName, spec] of Object.entries(withClause)) {
115
115
  if (!spec)
116
116
  continue;
117
+ // `_count` needs each counted relation's parent-side key to stitch counts.
118
+ if (relName === '_count') {
119
+ for (const rel of resolveCountRelations(parentMeta, spec)) {
120
+ for (const col of localKeyColumns(rel))
121
+ fields.add(parentMeta.reverseColumnMap[col] ?? col);
122
+ }
123
+ continue;
124
+ }
117
125
  const rel = parentMeta.relations[relName];
118
126
  if (!rel)
119
127
  continue; // unknown relation — the join path throws; let the loader surface it
@@ -134,6 +142,37 @@ function localKeyColumns(rel) {
134
142
  return normalizeKeyColumns(rel.foreignKey);
135
143
  return normalizeKeyColumns(rel.referenceKey);
136
144
  }
145
+ /**
146
+ * Resolve the set of to-many relations a `_count` spec selects. `true` counts
147
+ * every to-many relation (hasMany + manyToMany) of the table; the record form
148
+ * counts only the enabled names. Shared by the join builder and the batched
149
+ * loader so both count the exact same relations.
150
+ *
151
+ * Errors: E005 ({@link RelationError}) for an unknown relation name, E003
152
+ * ({@link ValidationError}) when a named relation is to-one.
153
+ */
154
+ export function resolveCountRelations(parentMeta, countSpec) {
155
+ const isToMany = (r) => r.type === 'hasMany' || r.type === 'manyToMany';
156
+ if (countSpec === true) {
157
+ return Object.values(parentMeta.relations).filter(isToMany);
158
+ }
159
+ const out = [];
160
+ for (const [relName, enabled] of Object.entries(countSpec)) {
161
+ if (!enabled)
162
+ continue;
163
+ const rel = parentMeta.relations[relName];
164
+ if (!rel) {
165
+ throw new RelationError(`[turbine] Unknown relation "${relName}" in _count on table "${parentMeta.name}". ` +
166
+ `Available: ${Object.keys(parentMeta.relations).join(', ')}`);
167
+ }
168
+ if (!isToMany(rel)) {
169
+ throw new ValidationError(`[turbine] _count is only supported for to-many relations; "${relName}" on ` +
170
+ `"${parentMeta.name}" is a to-one relation.`);
171
+ }
172
+ out.push(rel);
173
+ }
174
+ return out;
175
+ }
137
176
  /** Stringified stitch key — robust to number/uuid/bigint type drift across a join. */
138
177
  function keyOf(value) {
139
178
  return String(value);
@@ -155,6 +194,11 @@ export async function loadRelationsBatched(ctx, parents, withClause, timeout, de
155
194
  for (const [relName, spec] of Object.entries(withClause)) {
156
195
  if (!spec)
157
196
  continue;
197
+ // Reserved `_count` key — one grouped COUNT(*) follow-up per counted relation.
198
+ if (relName === '_count') {
199
+ loads.push(loadCounts(ctx, parents, spec));
200
+ continue;
201
+ }
158
202
  const rel = ctx.parentMeta.relations[relName];
159
203
  if (!rel) {
160
204
  throw new ValidationError(`[turbine] Unknown relation "${relName}" on table "${ctx.parentMeta.name}". ` +
@@ -209,6 +253,7 @@ async function loadToOneOrMany(ctx, parents, rel, relName, options, timeout, dep
209
253
  select: proj.select,
210
254
  omit: proj.omit,
211
255
  orderBy: options.orderBy,
256
+ skipGlobalFilters: ctx.skipGlobalFilters,
212
257
  });
213
258
  const result = await ctx.exec(deferred.sql, deferred.params, deferred.preparedName);
214
259
  return deferred.transform(result);
@@ -307,6 +352,7 @@ async function loadManyToMany(ctx, parents, rel, relName, options, timeout, dept
307
352
  select: proj.select,
308
353
  omit: proj.omit,
309
354
  orderBy: options.orderBy,
355
+ skipGlobalFilters: ctx.skipGlobalFilters,
310
356
  });
311
357
  const result = await ctx.exec(deferred.sql, deferred.params, deferred.preparedName);
312
358
  return deferred.transform(result);
@@ -341,6 +387,107 @@ async function loadManyToMany(ctx, parents, rel, relName, options, timeout, dept
341
387
  }
342
388
  stripFields(targetsInOrder, proj.strip);
343
389
  }
390
+ /**
391
+ * Load correlated `_count` values for the counted relations. One grouped
392
+ * follow-up per relation (`SELECT key, COUNT(*) … WHERE key = ANY($1) GROUP BY
393
+ * key`), attached onto each parent's `_count` object (0 when a parent has no
394
+ * matching rows) — byte-identical to the join strategy's `_count` output.
395
+ */
396
+ async function loadCounts(ctx, parents, countSpec) {
397
+ const rels = resolveCountRelations(ctx.parentMeta, countSpec);
398
+ // Initialise every parent's `_count` up-front so the concurrent per-relation
399
+ // loads below (each writing its own key) never race on the object creation.
400
+ for (const parent of parents) {
401
+ if (parent._count === undefined)
402
+ parent._count = {};
403
+ }
404
+ await Promise.all(rels.map((rel) => loadOneCount(ctx, parents, rel)));
405
+ }
406
+ /** One grouped COUNT(*) follow-up for a single to-many relation. */
407
+ async function loadOneCount(ctx, parents, rel) {
408
+ let parentKeyCol;
409
+ let childTable;
410
+ let childKeyCol;
411
+ if (rel.type === 'manyToMany') {
412
+ const through = rel.through;
413
+ if (!through) {
414
+ throw new ValidationError(`[turbine] manyToMany relation "${rel.name}" is missing its junction (\`through\`).`);
415
+ }
416
+ const sourceRef = normalizeKeyColumns(rel.referenceKey);
417
+ const sourceJ = normalizeKeyColumns(through.sourceKey);
418
+ if (sourceRef.length > 1 || sourceJ.length > 1) {
419
+ throw new UnsupportedFeatureError('composite-key batched _count', 'relationLoadStrategy: "batched"', `relation "${rel.name}" — use the default 'join' strategy for composite-key m2m _count`);
420
+ }
421
+ parentKeyCol = sourceRef[0];
422
+ childTable = through.table;
423
+ childKeyCol = sourceJ[0];
424
+ }
425
+ else {
426
+ // hasMany: child FK correlates to the parent reference key.
427
+ const fk = normalizeKeyColumns(rel.foreignKey);
428
+ const rk = normalizeKeyColumns(rel.referenceKey);
429
+ if (fk.length > 1 || rk.length > 1) {
430
+ throw new UnsupportedFeatureError('composite-key batched _count', 'relationLoadStrategy: "batched"', `relation "${rel.name}" — use the default 'join' strategy for composite-key _count`);
431
+ }
432
+ parentKeyCol = rk[0];
433
+ childTable = rel.to;
434
+ childKeyCol = fk[0];
435
+ }
436
+ const parentKeyField = ctx.parentMeta.reverseColumnMap[parentKeyCol] ?? parentKeyCol;
437
+ const keys = uniqueKeys(parents, parentKeyField);
438
+ const counts = new Map();
439
+ if (keys.length > 0) {
440
+ const qChild = ctx.quote(childTable);
441
+ const qKey = ctx.quote(childKeyCol);
442
+ const chunks = [];
443
+ for (let i = 0; i < keys.length; i += MAX_RELATION_KEYS)
444
+ chunks.push(keys.slice(i, i + MAX_RELATION_KEYS));
445
+ // Global filter on the counted target, matching the join strategy so the
446
+ // two strategies return identical counts under a filter. hasMany filters
447
+ // the counted table directly; m2m counts junction rows but restricts them
448
+ // to junction rows whose TARGET survives the target table's filter via
449
+ // EXISTS — mirroring buildRelationCountExpr's EXISTS-on-target (which also
450
+ // skips the filter when the junction targetKey arity doesn't match the
451
+ // target PK). Rendered after the $1 key array.
452
+ let gf = null;
453
+ if (ctx.tableGlobalFilter) {
454
+ if (rel.type === 'manyToMany' && rel.through) {
455
+ const targetKeys = normalizeKeyColumns(rel.through.targetKey);
456
+ const targetMeta = ctx.schema.tables[rel.to];
457
+ const pk = targetMeta?.primaryKey ?? [];
458
+ if (targetMeta && pk.length > 0 && pk.length === targetKeys.length) {
459
+ const targetGf = ctx.tableGlobalFilter(rel.to, 't', 1);
460
+ if (targetGf) {
461
+ const join = targetKeys.map((jc, i) => `t.${ctx.quote(pk[i])} = ${qChild}.${ctx.quote(jc)}`).join(' AND ');
462
+ gf = {
463
+ clause: `EXISTS (SELECT 1 FROM ${ctx.quote(rel.to)} t WHERE ${join} AND ${targetGf.clause})`,
464
+ params: targetGf.params,
465
+ };
466
+ }
467
+ }
468
+ }
469
+ else if (rel.type !== 'manyToMany') {
470
+ gf = ctx.tableGlobalFilter(childTable, qChild, 1);
471
+ }
472
+ }
473
+ const gfAnd = gf ? ` AND ${gf.clause}` : '';
474
+ const results = await Promise.all(chunks.map((chunk) => {
475
+ const params = [ctx.inClauseParam(chunk), ...(gf ? gf.params : [])];
476
+ const predicate = ctx.buildInClause(`${qChild}.${qKey}`, ctx.paramPlaceholder(1), false);
477
+ const sql = `SELECT ${qChild}.${qKey} AS "k", COUNT(*) AS "c" FROM ${qChild} ` +
478
+ `WHERE ${predicate}${gfAnd} GROUP BY ${qChild}.${qKey}`;
479
+ return ctx.exec(sql, params);
480
+ }));
481
+ for (const { rows } of results) {
482
+ for (const row of rows) {
483
+ counts.set(keyOf(row.k), Number(row.c));
484
+ }
485
+ }
486
+ }
487
+ for (const parent of parents) {
488
+ parent._count[rel.name] = counts.get(keyOf(parent[parentKeyField])) ?? 0;
489
+ }
490
+ }
344
491
  // ---------------------------------------------------------------------------
345
492
  // Small helpers
346
493
  // ---------------------------------------------------------------------------
@@ -11,122 +11,10 @@
11
11
  * metadata — nothing is hardcoded.
12
12
  */
13
13
  import type pg from 'pg';
14
- import type { Dialect } from '../dialect.js';
15
14
  import type { SchemaMetadata } from '../schema.js';
16
- import type { AggregateArgs, AggregateResult, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, QueryResult, RelationLoadStrategy, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs, WithClause } from './types.js';
17
- /**
18
- * Runs a SQL statement and resolves its raw result. Passed to a
19
- * {@link DeferredQuery.reselect} plan so it can run the write and the follow-up
20
- * SELECT through the same timeout/instrumentation path as the primary query.
21
- */
22
- export type ReselectExecutor = (sql: string, params: unknown[], preparedName?: string) => Promise<pg.QueryResult>;
23
- export interface DeferredQuery<T> {
24
- /** SQL text with $1, $2 placeholders */
25
- sql: string;
26
- /** Bound parameter values */
27
- params: unknown[];
28
- /** How to transform the raw pg.QueryResult into the final value */
29
- transform: (result: pg.QueryResult) => T;
30
- /** Tag for debugging / logging */
31
- tag: string;
32
- /** Prepared statement name (t_<16hex>). Set when SQL cache is enabled. */
33
- preparedName?: string;
34
- /**
35
- * Execution plan for dialects whose {@link Dialect.resultStrategy} is
36
- * `'reselect'` (no RETURNING — e.g. MySQL). Owns the statement ordering: it
37
- * runs the write and the follow-up row-fetching SELECT(s) via `exec`, and
38
- * resolves the result whose rows {@link DeferredQuery.transform} consumes.
39
- * Absent for `'returning'`/`'output'` dialects (the statement returns its own
40
- * rows), so the PostgreSQL path never allocates or consults it.
41
- */
42
- reselect?: (exec: ReselectExecutor) => Promise<pg.QueryResult>;
43
- }
44
- /** Middleware function type — imported from client to avoid circular deps */
45
- export type MiddlewareFn = (params: {
46
- model: string;
47
- action: string;
48
- args: Record<string, unknown>;
49
- }, next: (params: {
50
- model: string;
51
- action: string;
52
- args: Record<string, unknown>;
53
- }) => Promise<unknown>) => Promise<unknown>;
54
- /** Emitted after every query execution (success or failure). */
55
- export interface QueryEvent {
56
- sql: string;
57
- params: unknown[];
58
- duration: number;
59
- model: string;
60
- action: string;
61
- rows: number;
62
- timestamp: Date;
63
- error?: Error;
64
- }
65
- export type QueryEventListener = (event: QueryEvent) => void;
66
- /** Options passed from TurbineClient to QueryInterface */
67
- export interface QueryInterfaceOptions {
68
- /** Default LIMIT applied to findMany() when no limit is specified */
69
- defaultLimit?: number;
70
- /**
71
- * Log a one-time warning when {@link QueryInterface.findMany} is called
72
- * without a `limit`. Defaults to `true` so that accidental unbounded
73
- * queries are surfaced loudly during development. Pass `false` to silence
74
- * the warning entirely (e.g. for CLI tooling that intentionally streams
75
- * full tables).
76
- */
77
- warnOnUnlimited?: boolean;
78
- /**
79
- * Enable prepared statements. When true, queries are submitted with a
80
- * `{ name, text, values }` object to the pg driver, which caches the
81
- * parse+plan on the server per connection.
82
- *
83
- * Default: `true` for Turbine-owned pools, `false` for external pools
84
- * (serverless drivers may not support named statements).
85
- */
86
- preparedStatements?: boolean;
87
- /**
88
- * Enable the SQL template cache. When true, repeated queries with the
89
- * same shape (same keys, operators, relations — different values) reuse
90
- * cached SQL text instead of rebuilding from scratch.
91
- *
92
- * Default: `true`. Set to `false` as a nuclear kill switch.
93
- */
94
- sqlCache?: boolean;
95
- /** SQL dialect implementation. Defaults to PostgreSQL. */
96
- dialect?: Dialect;
97
- /**
98
- * Interpret offset-less timestamp strings (Postgres `timestamp` without
99
- * time zone, and the JSON emitted by nested-relation subqueries) as UTC.
100
- * This is the Prisma/Rails/Django convention and makes results independent
101
- * of the server's local time zone. Default: `true`. Set `false` to restore
102
- * the pre-0.26 behavior (JS local-time interpretation).
103
- */
104
- utcTimestamps?: boolean;
105
- /**
106
- * Client-level default relation-loading strategy for `with` clauses. Per-query
107
- * `relationLoadStrategy` args override this; both default to `'join'`.
108
- */
109
- relationLoadStrategy?: RelationLoadStrategy;
110
- /**
111
- * How nested-relation subqueries encode each row's JSON: `'object'` (default,
112
- * `json_build_object`) or `'positional'` (`json_build_array`, key-less — see
113
- * {@link Dialect.buildJsonArray}). Positional is Postgres-only in v1; a
114
- * `with` clause on any other dialect throws `UnsupportedFeatureError` (E017).
115
- */
116
- jsonEncoding?: 'object' | 'positional';
117
- /** @internal Set by TransactionClient — signals that this QI runs inside an active transaction. */
118
- _txScoped?: boolean;
119
- /** @internal Callback from TurbineClient for query event emission. */
120
- _onQuery?: (event: QueryEvent) => void;
121
- /**
122
- * @internal Factory that builds the per-table query interface. Defaults to
123
- * `new QueryInterface` (the SQL path). Non-SQL backends (PowDB) supply a
124
- * factory returning a structurally-compatible interface that generates their
125
- * own query language instead of SQL. The SQL dialects never set this, so their
126
- * `table()` behavior is byte-identical.
127
- */
128
- queryInterfaceFactory?: (pool: pg.Pool, table: string, schema: SchemaMetadata, middlewares: MiddlewareFn[], options: QueryInterfaceOptions) => QueryInterface<object>;
129
- }
15
+ import type { AggregateArgs, AggregateResult, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, QueryResult, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs, WithClause } from './types.js';
16
+ export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, } from './deferred.js';
17
+ import type { DeferredQuery, MiddlewareFn, QueryInterfaceOptions } from './deferred.js';
130
18
  export declare class QueryInterface<T extends object, R extends object = {}> {
131
19
  private readonly pool;
132
20
  private readonly table;
@@ -145,6 +33,13 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
145
33
  private readonly relationLoadStrategy;
146
34
  /** Nested-relation JSON encoding: 'object' (default) or 'positional'. */
147
35
  private readonly jsonEncoding;
36
+ /**
37
+ * Client-level automatic WHERE filters keyed by table accessor (soft-delete /
38
+ * multi-tenancy). AND-merged into every query on the keyed table and every
39
+ * relation subquery targeting it. Undefined when none are configured, in
40
+ * which case every path is byte-identical to the pre-0.28 behavior.
41
+ */
42
+ private readonly globalFilters?;
148
43
  /**
149
44
  * Tracks tables that have already triggered an unlimited-query warning so
150
45
  * the user is not spammed once per row. Per-instance state — each
@@ -176,6 +71,15 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
176
71
  private readonly options?;
177
72
  /** Set by executeWithMiddleware so queryWithTimeout can include it in events. */
178
73
  private currentAction;
74
+ /**
75
+ * The active query's `skipGlobalFilters` opt-out, set at the top of each
76
+ * `build*` method and read deep in the (synchronous) SQL-build + param-collect
77
+ * tree — so relation subqueries, relation filters, `_count`, and relation
78
+ * `orderBy` all see it without threading it through dozens of signatures.
79
+ * Only load-bearing when {@link globalFilters} is configured; build+collect are
80
+ * synchronous per call, so this transient is never observed across an await.
81
+ */
82
+ private currentSkip;
179
83
  constructor(pool: pg.Pool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[], options?: QueryInterfaceOptions);
180
84
  /** Quote an identifier through the active SQL dialect. */
181
85
  private q;
@@ -451,6 +355,21 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
451
355
  * Returns null if neither is provided (meaning all columns).
452
356
  */
453
357
  private resolveColumns;
358
+ /**
359
+ * Reject any write against a view (H4). Views are introspected with
360
+ * `isView: true` and are read-only in every engine; a write raises a
361
+ * {@link ValidationError} (E003) rather than emitting SQL Postgres would
362
+ * reject (or, worse, silently applying to an updatable view).
363
+ */
364
+ private assertWritable;
365
+ /**
366
+ * Reject a write whose `data` names a `GENERATED ALWAYS AS (...) STORED`
367
+ * column (H3). Postgres computes these from other columns and errors if you
368
+ * try to write them; we fail early with a clear {@link ValidationError} (E003)
369
+ * instead of surfacing a cryptic driver error. Undefined values are ignored
370
+ * (they're stripped from the statement anyway).
371
+ */
372
+ private assertNoGeneratedColumns;
454
373
  /** Convert camelCase field name to snake_case column name (unquoted, for non-SQL uses) */
455
374
  private toColumn;
456
375
  /** Convert camelCase field name to a double-quoted SQL identifier */
@@ -496,7 +415,17 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
496
415
  * @internal Exposed as package-private for testing.
497
416
  */
498
417
  collectWhereParams(where: Record<string, unknown>, params: unknown[]): void;
499
- /** Collect params from a relation filter sub-where. Mirrors buildSubWhereForRelation. */
418
+ /**
419
+ * Param-collect mirror of {@link buildRelationFilter} for one relation-filter
420
+ * object (`{ some/every/none/is/isNot }`, already normalized). Pushes, per
421
+ * present branch and in the canonical order some→none→every→is→isNot, the
422
+ * branch's sub-where params THEN the target table's global-filter params —
423
+ * exactly the order buildRelationFilter emits. When no global filter applies
424
+ * the gf calls are no-ops, so this stays byte-identical to the pre-0.28 path.
425
+ * Shared by every collect site that mirrors buildRelationFilter
426
+ * (collectWhereParams, collectRelFilterParams, collectAliasWhereParams).
427
+ */
428
+ private collectRelationFilterParams;
500
429
  private collectRelFilterParams;
501
430
  /** Collect params from operator clauses. Mirrors buildOperatorClauses. */
502
431
  private collectOperatorParams;
@@ -545,13 +474,55 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
545
474
  /** Build WHERE clause from a where object (supports operators, NULL, OR) */
546
475
  private buildWhere;
547
476
  /**
548
- * Refuse mutations with an empty predicate unless explicitly opted in.
549
- *
550
- * An empty `where` (e.g. `{}` or `{ id: undefined }`) resolves to a
551
- * mutation with no filter — a common footgun when a caller's filter
552
- * value accidentally resolves to `undefined`. This guard throws
553
- * `ValidationError` in that case unless `allowFullTableScan: true`.
477
+ * Resolve the configured global filter for `table`, evaluating a function
478
+ * filter, honoring the active query's `skipGlobalFilters`. Returns `null` when
479
+ * no filter applies, the query opted out, or the filter is empty.
480
+ */
481
+ private resolveGlobalFilter;
482
+ /**
483
+ * AND-merge this table's resolved global filter into a user `where`. Either
484
+ * side may be absent. When no filter applies the user where is returned by
485
+ * reference, so fingerprints/SQL stay byte-identical to the pre-0.28 path.
486
+ */
487
+ private mergeGlobalFilter;
488
+ /**
489
+ * SQL clause for `targetTable`'s global filter rendered against `alias`
490
+ * (relation subqueries, `_count`, relation `orderBy`). Pushes its params to
491
+ * `params`; returns `''` when no filter applies. Mirror:
492
+ * {@link collectTargetGlobalFilterAlias}.
554
493
  */
494
+ private targetGlobalFilterAlias;
495
+ /** Param-collect mirror of {@link targetGlobalFilterAlias}. */
496
+ private collectTargetGlobalFilterAlias;
497
+ /**
498
+ * SQL clause for `targetTable`'s global filter rendered against the bare
499
+ * (unaliased) table name — the form used inside relation-filter `EXISTS`
500
+ * subqueries. Pushes its params; `''` when none. Mirror:
501
+ * {@link collectTargetGlobalFilterExists}.
502
+ */
503
+ private targetGlobalFilterExists;
504
+ /** Param-collect mirror of {@link targetGlobalFilterExists}. */
505
+ private collectTargetGlobalFilterExists;
506
+ /**
507
+ * Value-invariant SQL-cache-key segment for the active global-filter
508
+ * environment. Relation-subquery / relation-filter / `_count` / relation-
509
+ * `orderBy` global filters are rendered at build time but their SHAPE is not
510
+ * otherwise in the where/with fingerprint, so this segment guards the cache:
511
+ * two different filter shapes never collide on one cached SQL text, while two
512
+ * function-filter results of the SAME shape (differing only in values) share
513
+ * the entry and bind their own params. Empty (`''`) when no filter applies, so
514
+ * cache keys stay byte-identical when the feature is unused.
515
+ */
516
+ private globalFilterCacheSegment;
517
+ /**
518
+ * True when the USER-supplied `where` compiles to no predicate (`{}`,
519
+ * `{ id: undefined }`, `{ OR: [{ a: undefined }] }`, …). This is the exact
520
+ * signal the empty-`where` guard needs — the compiled emptiness, NOT the
521
+ * fingerprint (which is non-empty for an all-undefined `OR`/`AND`). It ignores
522
+ * any configured global filter, so a global filter never lets an unguarded
523
+ * mass mutation through.
524
+ */
525
+ private userPredicateIsEmpty;
555
526
  private assertMutationHasPredicate;
556
527
  /**
557
528
  * Build the inner WHERE expression (without the WHERE keyword).
@@ -619,7 +590,65 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
619
590
  * findMany path). When `params` is omitted (groupBy / relation path) a vector
620
591
  * ordering throws — KNN ordering is only supported at the top level.
621
592
  */
593
+ /**
594
+ * Value-shape fingerprint for a single orderBy entry, so two queries whose
595
+ * ORDER BY differs only in nulls placement, vector metric, or relation-count
596
+ * vs relation-column never collide on one cached SQL string. Captures the
597
+ * SQL-shaping bits (direction, nulls, metric, relation keys) — never values.
598
+ */
599
+ private orderByEntryFingerprint;
622
600
  private buildOrderBy;
601
+ /**
602
+ * True when an orderBy value is a relation-ordering object: a plain object
603
+ * that is neither a vector KNN ordering nor an {@link OrderBySpec}. Its key
604
+ * in the orderBy clause is a relation name.
605
+ */
606
+ private isRelationOrderByValue;
607
+ /**
608
+ * Render the ` NULLS FIRST` / ` NULLS LAST` suffix for a column ordering.
609
+ * Only PostgreSQL and SQLite support the `NULLS FIRST/LAST` grammar — on any
610
+ * other engine a caller asking for explicit nulls placement gets a clear
611
+ * {@link UnsupportedFeatureError} (E017) instead of broken SQL.
612
+ */
613
+ private nullsSuffix;
614
+ /**
615
+ * Compile a relation ordering term. For a to-many relation the only allowed
616
+ * key is `_count`, which becomes a correlated `COUNT(*)` subquery. For a
617
+ * to-one relation each entry names a target column and becomes a correlated
618
+ * scalar subquery (supporting {@link OrderBySpec} nulls placement).
619
+ *
620
+ * Validation: relation must exist (E005); to-many only allows `_count`, and
621
+ * to-one only allows real target columns (E003).
622
+ */
623
+ private buildRelationOrderBy;
624
+ /**
625
+ * Build a correlated `(SELECT COUNT(*) …)` scalar subquery for a to-many
626
+ * relation, correlated to `parentRef`. hasMany counts child rows via the FK;
627
+ * manyToMany counts junction rows via the source key. Shared by the `_count`
628
+ * `with` key and to-many relation orderBy.
629
+ *
630
+ * When `params` is supplied and the target has a global filter, it is
631
+ * AND-merged so the count only sees surviving rows (a soft-deleted child is
632
+ * not counted): hasMany filters the counted rows directly; manyToMany adds an
633
+ * `EXISTS` on the target through the junction (the junction rows themselves
634
+ * carry no filter). Params are mirrored by {@link collectRelationCountParams}.
635
+ */
636
+ private buildRelationCountExpr;
637
+ /**
638
+ * `EXISTS (SELECT 1 FROM <target> <talias> WHERE <join> AND <gf>)` restricting
639
+ * a manyToMany `_count` to targets that survive their global filter. `''` when
640
+ * the target has no filter. Pushes gf params; mirror:
641
+ * {@link collectManyToManyTargetGlobalFilter}.
642
+ */
643
+ private manyToManyTargetGlobalFilterExists;
644
+ /** Param-collect mirror of {@link manyToManyTargetGlobalFilterExists}. */
645
+ private collectManyToManyTargetGlobalFilter;
646
+ /**
647
+ * Param-collect mirror of {@link buildRelationCountExpr}'s global-filter
648
+ * params (hasMany direct filter, or manyToMany EXISTS-on-target). Only pushes
649
+ * when a filter applies — no-op otherwise.
650
+ */
651
+ private collectRelationCountParams;
623
652
  /**
624
653
  * Resolve a {@link VectorMetric} to its pgvector distance operator from a
625
654
  * fixed allow-list, validating the target column is actually a `vector`