turbine-orm 0.27.0 → 0.28.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/README.md +17 -13
- package/dist/cjs/cli/config.js +20 -3
- package/dist/cjs/cli/destructive.js +47 -31
- package/dist/cjs/cli/index.js +273 -71
- package/dist/cjs/cli/mcp.js +788 -0
- package/dist/cjs/cli/migrate.js +95 -20
- package/dist/cjs/cli/studio.js +3 -2
- package/dist/cjs/client.js +267 -34
- package/dist/cjs/dialect.js +2 -0
- package/dist/cjs/generate.js +171 -7
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/introspect.js +177 -4
- package/dist/cjs/query/batched-loader.js +148 -0
- package/dist/cjs/query/builder.js +714 -133
- package/dist/cjs/schema-builder.js +59 -4
- package/dist/cjs/schema-sql.js +315 -6
- package/dist/cjs/seed.js +66 -0
- package/dist/cli/config.d.ts +9 -2
- package/dist/cli/config.js +19 -3
- package/dist/cli/destructive.js +47 -31
- package/dist/cli/index.d.ts +52 -1
- package/dist/cli/index.js +272 -74
- package/dist/cli/mcp.d.ts +17 -0
- package/dist/cli/mcp.js +781 -0
- package/dist/cli/migrate.d.ts +37 -0
- package/dist/cli/migrate.js +92 -20
- package/dist/cli/studio.d.ts +3 -2
- package/dist/cli/studio.js +3 -2
- package/dist/client.d.ts +136 -1
- package/dist/client.js +267 -34
- package/dist/dialect.d.ts +17 -0
- package/dist/dialect.js +2 -0
- package/dist/generate.d.ts +17 -0
- package/dist/generate.js +171 -10
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -0
- package/dist/introspect.d.ts +20 -1
- package/dist/introspect.js +175 -4
- package/dist/query/batched-loader.d.ts +29 -2
- package/dist/query/batched-loader.js +148 -1
- package/dist/query/builder.d.ts +156 -8
- package/dist/query/builder.js +715 -134
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +113 -8
- package/dist/schema-builder.d.ts +73 -8
- package/dist/schema-builder.js +59 -4
- package/dist/schema-sql.d.ts +67 -0
- package/dist/schema-sql.js +310 -6
- package/dist/schema.d.ts +53 -0
- package/dist/seed.d.ts +4 -0
- package/dist/seed.js +63 -0
- package/package.json +2 -3
|
@@ -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
|
// ---------------------------------------------------------------------------
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import type pg from 'pg';
|
|
14
14
|
import type { Dialect } from '../dialect.js';
|
|
15
15
|
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';
|
|
16
|
+
import type { AggregateArgs, AggregateResult, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GlobalFilters, GroupByArgs, QueryResult, RelationLoadStrategy, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs, WithClause } from './types.js';
|
|
17
17
|
/**
|
|
18
18
|
* Runs a SQL statement and resolves its raw result. Passed to a
|
|
19
19
|
* {@link DeferredQuery.reselect} plan so it can run the write and the follow-up
|
|
@@ -114,6 +114,13 @@ export interface QueryInterfaceOptions {
|
|
|
114
114
|
* `with` clause on any other dialect throws `UnsupportedFeatureError` (E017).
|
|
115
115
|
*/
|
|
116
116
|
jsonEncoding?: 'object' | 'positional';
|
|
117
|
+
/**
|
|
118
|
+
* Automatic WHERE filters keyed by table accessor, AND-merged into every
|
|
119
|
+
* query on that table and every relation subquery targeting it (soft-delete /
|
|
120
|
+
* multi-tenancy). Function values are evaluated at query-build time. See
|
|
121
|
+
* {@link GlobalFilters}.
|
|
122
|
+
*/
|
|
123
|
+
globalFilters?: GlobalFilters;
|
|
117
124
|
/** @internal Set by TransactionClient — signals that this QI runs inside an active transaction. */
|
|
118
125
|
_txScoped?: boolean;
|
|
119
126
|
/** @internal Callback from TurbineClient for query event emission. */
|
|
@@ -145,6 +152,13 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
145
152
|
private readonly relationLoadStrategy;
|
|
146
153
|
/** Nested-relation JSON encoding: 'object' (default) or 'positional'. */
|
|
147
154
|
private readonly jsonEncoding;
|
|
155
|
+
/**
|
|
156
|
+
* Client-level automatic WHERE filters keyed by table accessor (soft-delete /
|
|
157
|
+
* multi-tenancy). AND-merged into every query on the keyed table and every
|
|
158
|
+
* relation subquery targeting it. Undefined when none are configured, in
|
|
159
|
+
* which case every path is byte-identical to the pre-0.28 behavior.
|
|
160
|
+
*/
|
|
161
|
+
private readonly globalFilters?;
|
|
148
162
|
/**
|
|
149
163
|
* Tracks tables that have already triggered an unlimited-query warning so
|
|
150
164
|
* the user is not spammed once per row. Per-instance state — each
|
|
@@ -176,6 +190,15 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
176
190
|
private readonly options?;
|
|
177
191
|
/** Set by executeWithMiddleware so queryWithTimeout can include it in events. */
|
|
178
192
|
private currentAction;
|
|
193
|
+
/**
|
|
194
|
+
* The active query's `skipGlobalFilters` opt-out, set at the top of each
|
|
195
|
+
* `build*` method and read deep in the (synchronous) SQL-build + param-collect
|
|
196
|
+
* tree — so relation subqueries, relation filters, `_count`, and relation
|
|
197
|
+
* `orderBy` all see it without threading it through dozens of signatures.
|
|
198
|
+
* Only load-bearing when {@link globalFilters} is configured; build+collect are
|
|
199
|
+
* synchronous per call, so this transient is never observed across an await.
|
|
200
|
+
*/
|
|
201
|
+
private currentSkip;
|
|
179
202
|
constructor(pool: pg.Pool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[], options?: QueryInterfaceOptions);
|
|
180
203
|
/** Quote an identifier through the active SQL dialect. */
|
|
181
204
|
private q;
|
|
@@ -451,6 +474,21 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
451
474
|
* Returns null if neither is provided (meaning all columns).
|
|
452
475
|
*/
|
|
453
476
|
private resolveColumns;
|
|
477
|
+
/**
|
|
478
|
+
* Reject any write against a view (H4). Views are introspected with
|
|
479
|
+
* `isView: true` and are read-only in every engine; a write raises a
|
|
480
|
+
* {@link ValidationError} (E003) rather than emitting SQL Postgres would
|
|
481
|
+
* reject (or, worse, silently applying to an updatable view).
|
|
482
|
+
*/
|
|
483
|
+
private assertWritable;
|
|
484
|
+
/**
|
|
485
|
+
* Reject a write whose `data` names a `GENERATED ALWAYS AS (...) STORED`
|
|
486
|
+
* column (H3). Postgres computes these from other columns and errors if you
|
|
487
|
+
* try to write them; we fail early with a clear {@link ValidationError} (E003)
|
|
488
|
+
* instead of surfacing a cryptic driver error. Undefined values are ignored
|
|
489
|
+
* (they're stripped from the statement anyway).
|
|
490
|
+
*/
|
|
491
|
+
private assertNoGeneratedColumns;
|
|
454
492
|
/** Convert camelCase field name to snake_case column name (unquoted, for non-SQL uses) */
|
|
455
493
|
private toColumn;
|
|
456
494
|
/** Convert camelCase field name to a double-quoted SQL identifier */
|
|
@@ -496,7 +534,17 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
496
534
|
* @internal Exposed as package-private for testing.
|
|
497
535
|
*/
|
|
498
536
|
collectWhereParams(where: Record<string, unknown>, params: unknown[]): void;
|
|
499
|
-
/**
|
|
537
|
+
/**
|
|
538
|
+
* Param-collect mirror of {@link buildRelationFilter} for one relation-filter
|
|
539
|
+
* object (`{ some/every/none/is/isNot }`, already normalized). Pushes, per
|
|
540
|
+
* present branch and in the canonical order some→none→every→is→isNot, the
|
|
541
|
+
* branch's sub-where params THEN the target table's global-filter params —
|
|
542
|
+
* exactly the order buildRelationFilter emits. When no global filter applies
|
|
543
|
+
* the gf calls are no-ops, so this stays byte-identical to the pre-0.28 path.
|
|
544
|
+
* Shared by every collect site that mirrors buildRelationFilter
|
|
545
|
+
* (collectWhereParams, collectRelFilterParams, collectAliasWhereParams).
|
|
546
|
+
*/
|
|
547
|
+
private collectRelationFilterParams;
|
|
500
548
|
private collectRelFilterParams;
|
|
501
549
|
/** Collect params from operator clauses. Mirrors buildOperatorClauses. */
|
|
502
550
|
private collectOperatorParams;
|
|
@@ -545,13 +593,55 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
545
593
|
/** Build WHERE clause from a where object (supports operators, NULL, OR) */
|
|
546
594
|
private buildWhere;
|
|
547
595
|
/**
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
596
|
+
* Resolve the configured global filter for `table`, evaluating a function
|
|
597
|
+
* filter, honoring the active query's `skipGlobalFilters`. Returns `null` when
|
|
598
|
+
* no filter applies, the query opted out, or the filter is empty.
|
|
599
|
+
*/
|
|
600
|
+
private resolveGlobalFilter;
|
|
601
|
+
/**
|
|
602
|
+
* AND-merge this table's resolved global filter into a user `where`. Either
|
|
603
|
+
* side may be absent. When no filter applies the user where is returned by
|
|
604
|
+
* reference, so fingerprints/SQL stay byte-identical to the pre-0.28 path.
|
|
554
605
|
*/
|
|
606
|
+
private mergeGlobalFilter;
|
|
607
|
+
/**
|
|
608
|
+
* SQL clause for `targetTable`'s global filter rendered against `alias`
|
|
609
|
+
* (relation subqueries, `_count`, relation `orderBy`). Pushes its params to
|
|
610
|
+
* `params`; returns `''` when no filter applies. Mirror:
|
|
611
|
+
* {@link collectTargetGlobalFilterAlias}.
|
|
612
|
+
*/
|
|
613
|
+
private targetGlobalFilterAlias;
|
|
614
|
+
/** Param-collect mirror of {@link targetGlobalFilterAlias}. */
|
|
615
|
+
private collectTargetGlobalFilterAlias;
|
|
616
|
+
/**
|
|
617
|
+
* SQL clause for `targetTable`'s global filter rendered against the bare
|
|
618
|
+
* (unaliased) table name — the form used inside relation-filter `EXISTS`
|
|
619
|
+
* subqueries. Pushes its params; `''` when none. Mirror:
|
|
620
|
+
* {@link collectTargetGlobalFilterExists}.
|
|
621
|
+
*/
|
|
622
|
+
private targetGlobalFilterExists;
|
|
623
|
+
/** Param-collect mirror of {@link targetGlobalFilterExists}. */
|
|
624
|
+
private collectTargetGlobalFilterExists;
|
|
625
|
+
/**
|
|
626
|
+
* Value-invariant SQL-cache-key segment for the active global-filter
|
|
627
|
+
* environment. Relation-subquery / relation-filter / `_count` / relation-
|
|
628
|
+
* `orderBy` global filters are rendered at build time but their SHAPE is not
|
|
629
|
+
* otherwise in the where/with fingerprint, so this segment guards the cache:
|
|
630
|
+
* two different filter shapes never collide on one cached SQL text, while two
|
|
631
|
+
* function-filter results of the SAME shape (differing only in values) share
|
|
632
|
+
* the entry and bind their own params. Empty (`''`) when no filter applies, so
|
|
633
|
+
* cache keys stay byte-identical when the feature is unused.
|
|
634
|
+
*/
|
|
635
|
+
private globalFilterCacheSegment;
|
|
636
|
+
/**
|
|
637
|
+
* True when the USER-supplied `where` compiles to no predicate (`{}`,
|
|
638
|
+
* `{ id: undefined }`, `{ OR: [{ a: undefined }] }`, …). This is the exact
|
|
639
|
+
* signal the empty-`where` guard needs — the compiled emptiness, NOT the
|
|
640
|
+
* fingerprint (which is non-empty for an all-undefined `OR`/`AND`). It ignores
|
|
641
|
+
* any configured global filter, so a global filter never lets an unguarded
|
|
642
|
+
* mass mutation through.
|
|
643
|
+
*/
|
|
644
|
+
private userPredicateIsEmpty;
|
|
555
645
|
private assertMutationHasPredicate;
|
|
556
646
|
/**
|
|
557
647
|
* Build the inner WHERE expression (without the WHERE keyword).
|
|
@@ -619,7 +709,65 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
619
709
|
* findMany path). When `params` is omitted (groupBy / relation path) a vector
|
|
620
710
|
* ordering throws — KNN ordering is only supported at the top level.
|
|
621
711
|
*/
|
|
712
|
+
/**
|
|
713
|
+
* Value-shape fingerprint for a single orderBy entry, so two queries whose
|
|
714
|
+
* ORDER BY differs only in nulls placement, vector metric, or relation-count
|
|
715
|
+
* vs relation-column never collide on one cached SQL string. Captures the
|
|
716
|
+
* SQL-shaping bits (direction, nulls, metric, relation keys) — never values.
|
|
717
|
+
*/
|
|
718
|
+
private orderByEntryFingerprint;
|
|
622
719
|
private buildOrderBy;
|
|
720
|
+
/**
|
|
721
|
+
* True when an orderBy value is a relation-ordering object: a plain object
|
|
722
|
+
* that is neither a vector KNN ordering nor an {@link OrderBySpec}. Its key
|
|
723
|
+
* in the orderBy clause is a relation name.
|
|
724
|
+
*/
|
|
725
|
+
private isRelationOrderByValue;
|
|
726
|
+
/**
|
|
727
|
+
* Render the ` NULLS FIRST` / ` NULLS LAST` suffix for a column ordering.
|
|
728
|
+
* Only PostgreSQL and SQLite support the `NULLS FIRST/LAST` grammar — on any
|
|
729
|
+
* other engine a caller asking for explicit nulls placement gets a clear
|
|
730
|
+
* {@link UnsupportedFeatureError} (E017) instead of broken SQL.
|
|
731
|
+
*/
|
|
732
|
+
private nullsSuffix;
|
|
733
|
+
/**
|
|
734
|
+
* Compile a relation ordering term. For a to-many relation the only allowed
|
|
735
|
+
* key is `_count`, which becomes a correlated `COUNT(*)` subquery. For a
|
|
736
|
+
* to-one relation each entry names a target column and becomes a correlated
|
|
737
|
+
* scalar subquery (supporting {@link OrderBySpec} nulls placement).
|
|
738
|
+
*
|
|
739
|
+
* Validation: relation must exist (E005); to-many only allows `_count`, and
|
|
740
|
+
* to-one only allows real target columns (E003).
|
|
741
|
+
*/
|
|
742
|
+
private buildRelationOrderBy;
|
|
743
|
+
/**
|
|
744
|
+
* Build a correlated `(SELECT COUNT(*) …)` scalar subquery for a to-many
|
|
745
|
+
* relation, correlated to `parentRef`. hasMany counts child rows via the FK;
|
|
746
|
+
* manyToMany counts junction rows via the source key. Shared by the `_count`
|
|
747
|
+
* `with` key and to-many relation orderBy.
|
|
748
|
+
*
|
|
749
|
+
* When `params` is supplied and the target has a global filter, it is
|
|
750
|
+
* AND-merged so the count only sees surviving rows (a soft-deleted child is
|
|
751
|
+
* not counted): hasMany filters the counted rows directly; manyToMany adds an
|
|
752
|
+
* `EXISTS` on the target through the junction (the junction rows themselves
|
|
753
|
+
* carry no filter). Params are mirrored by {@link collectRelationCountParams}.
|
|
754
|
+
*/
|
|
755
|
+
private buildRelationCountExpr;
|
|
756
|
+
/**
|
|
757
|
+
* `EXISTS (SELECT 1 FROM <target> <talias> WHERE <join> AND <gf>)` restricting
|
|
758
|
+
* a manyToMany `_count` to targets that survive their global filter. `''` when
|
|
759
|
+
* the target has no filter. Pushes gf params; mirror:
|
|
760
|
+
* {@link collectManyToManyTargetGlobalFilter}.
|
|
761
|
+
*/
|
|
762
|
+
private manyToManyTargetGlobalFilterExists;
|
|
763
|
+
/** Param-collect mirror of {@link manyToManyTargetGlobalFilterExists}. */
|
|
764
|
+
private collectManyToManyTargetGlobalFilter;
|
|
765
|
+
/**
|
|
766
|
+
* Param-collect mirror of {@link buildRelationCountExpr}'s global-filter
|
|
767
|
+
* params (hasMany direct filter, or manyToMany EXISTS-on-target). Only pushes
|
|
768
|
+
* when a filter applies — no-op otherwise.
|
|
769
|
+
*/
|
|
770
|
+
private collectRelationCountParams;
|
|
623
771
|
/**
|
|
624
772
|
* Resolve a {@link VectorMetric} to its pgvector distance operator from a
|
|
625
773
|
* fixed allow-list, validating the target column is actually a `vector`
|