uql-orm 0.47.0 → 0.47.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.
- package/dist/dialect/abstractDialect.d.ts +35 -1
- package/dist/dialect/abstractDialect.js +29 -0
- package/dist/dialect/abstractSqlDialect.d.ts +12 -4
- package/dist/dialect/abstractSqlDialect.js +31 -43
- package/dist/mongo/mongoDialect.d.ts +11 -1
- package/dist/mongo/mongoDialect.js +34 -10
- package/dist/querier/abstractQuerier.js +8 -6
- package/dist/querier/relationCount.js +15 -9
- package/dist/type/queryWhere.d.ts +11 -1
- package/dist/util/relationQuery.util.d.ts +9 -6
- package/dist/util/relationQuery.util.js +11 -11
- package/dist/util/rowKey.util.d.ts +18 -4
- package/dist/util/rowKey.util.js +29 -5
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DialectFeatures, DialectName, EntityMeta, ExtraOptions, FieldOptions, InsertIdSource, NamingStrategy, QueryOptions, QueryWhere, QueryWhereMap } from '../type/index.js';
|
|
1
|
+
import type { DialectFeatures, DialectName, EntityMeta, ExtraOptions, FieldOptions, InsertIdSource, NamingStrategy, QueryGroupOp, QueryOptions, QueryWhere, QueryWhereArray, QueryWhereMap } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Options for initializing a dialect.
|
|
4
4
|
*/
|
|
@@ -77,4 +77,38 @@ export declare abstract class AbstractDialect {
|
|
|
77
77
|
* filters skipped. Recursion within one scope renders the returned map directly instead.
|
|
78
78
|
*/
|
|
79
79
|
protected scopedWhereMap<E>(meta: EntityMeta<E>, where?: QueryWhere<E>, opts?: QueryOptions): QueryWhereMap<E>;
|
|
80
|
+
/**
|
|
81
|
+
* How each clause-grouping operator renders: which operator joins its clauses, and whether the
|
|
82
|
+
* group is negated afterwards - so `$not` is `NOT (a AND b)` and `$nor` is `NOT (a OR b)`. SQL
|
|
83
|
+
* negates the rendered group; MongoDB spells the same thing with its own `$nor`.
|
|
84
|
+
*
|
|
85
|
+
* Total over {@link QueryGroupOp}, so a fifth operator cannot reach a dialect without both being
|
|
86
|
+
* told how to render it.
|
|
87
|
+
*/
|
|
88
|
+
protected static readonly GROUP_OPS: {
|
|
89
|
+
readonly $and: {
|
|
90
|
+
readonly join: '$and';
|
|
91
|
+
readonly negate: false;
|
|
92
|
+
};
|
|
93
|
+
readonly $or: {
|
|
94
|
+
readonly join: '$or';
|
|
95
|
+
readonly negate: false;
|
|
96
|
+
};
|
|
97
|
+
readonly $not: {
|
|
98
|
+
readonly join: '$and';
|
|
99
|
+
readonly negate: true;
|
|
100
|
+
};
|
|
101
|
+
readonly $nor: {
|
|
102
|
+
readonly join: '$or';
|
|
103
|
+
readonly negate: true;
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
/** Whether a `$where` key groups clauses, narrowing it for the renderers that read {@link GROUP_OPS}. */
|
|
107
|
+
protected static isGroupOp(key: string): key is QueryGroupOp;
|
|
108
|
+
/**
|
|
109
|
+
* A group operator's clauses, rejecting what the types do not cover: `/http` casts client JSON
|
|
110
|
+
* straight to `Query`, so a scalar can arrive where an array belongs. Shared so both backends
|
|
111
|
+
* refuse the same payload rather than one throwing and the other failing further in.
|
|
112
|
+
*/
|
|
113
|
+
protected static groupClauses<E>(key: QueryGroupOp, val: QueryWhereArray<E>): QueryWhereArray<E>;
|
|
80
114
|
}
|
|
@@ -88,4 +88,33 @@ export class AbstractDialect {
|
|
|
88
88
|
scopedWhereMap(meta, where = {}, opts) {
|
|
89
89
|
return applyFilters(meta, buildQueryWhereAsMap(meta, where), opts);
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* How each clause-grouping operator renders: which operator joins its clauses, and whether the
|
|
93
|
+
* group is negated afterwards - so `$not` is `NOT (a AND b)` and `$nor` is `NOT (a OR b)`. SQL
|
|
94
|
+
* negates the rendered group; MongoDB spells the same thing with its own `$nor`.
|
|
95
|
+
*
|
|
96
|
+
* Total over {@link QueryGroupOp}, so a fifth operator cannot reach a dialect without both being
|
|
97
|
+
* told how to render it.
|
|
98
|
+
*/
|
|
99
|
+
static GROUP_OPS = {
|
|
100
|
+
$and: { join: '$and', negate: false },
|
|
101
|
+
$or: { join: '$or', negate: false },
|
|
102
|
+
$not: { join: '$and', negate: true },
|
|
103
|
+
$nor: { join: '$or', negate: true },
|
|
104
|
+
};
|
|
105
|
+
/** Whether a `$where` key groups clauses, narrowing it for the renderers that read {@link GROUP_OPS}. */
|
|
106
|
+
static isGroupOp(key) {
|
|
107
|
+
return Object.hasOwn(AbstractDialect.GROUP_OPS, key);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A group operator's clauses, rejecting what the types do not cover: `/http` casts client JSON
|
|
111
|
+
* straight to `Query`, so a scalar can arrive where an array belongs. Shared so both backends
|
|
112
|
+
* refuse the same payload rather than one throwing and the other failing further in.
|
|
113
|
+
*/
|
|
114
|
+
static groupClauses(key, val) {
|
|
115
|
+
if (val !== undefined && !Array.isArray(val)) {
|
|
116
|
+
throw TypeError(`${key} expects an array, got ${val === null ? 'null' : typeof val}`);
|
|
117
|
+
}
|
|
118
|
+
return val ?? [];
|
|
119
|
+
}
|
|
91
120
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type EntityMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, type QueryBuildFn, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryDialect, type QueryExclude, type QueryFilter, type QueryGroupMap, type QueryHavingMap, type QueryOptions, type QueryPager, QueryRaw, type QueryRawFnOptions, type QuerySearch, type QuerySelectOptions, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorNear, type QueryWhere, type QueryWhereArray, type QueryWhereFieldOperatorMap, type QueryWhereMap, type QueryWhereOptions, type RelationMeta, type SqlDialectName, type SqlQueryDialect, type Type, type UpdatePayload } from '../type/index.js';
|
|
1
|
+
import { type EntityMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, type QueryBuildFn, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryDialect, type QueryExclude, type QueryFilter, type QueryGroupMap, type QueryGroupOp, type QueryHavingMap, type QueryOptions, type QueryPager, QueryRaw, type QueryRawFnOptions, type QuerySearch, type QuerySelectOptions, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorNear, type QueryWhere, type QueryWhereArray, type QueryWhereFieldOperatorMap, type QueryWhereMap, type QueryWhereOptions, type RelationMeta, type SqlDialectName, type SqlQueryDialect, type Type, type UpdatePayload } from '../type/index.js';
|
|
2
2
|
import { type ParentPartition } from '../util/index.js';
|
|
3
3
|
import type { HydrateKind } from './hydrateColumn.js';
|
|
4
4
|
import { type JsonAccessMode } from './jsonSql.js';
|
|
@@ -76,6 +76,15 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
76
76
|
* shared for the same reason - see {@link SqlQueryContext}.
|
|
77
77
|
*/
|
|
78
78
|
protected buildFragment(ctx: QueryContext, build: QueryBuildFn): string;
|
|
79
|
+
/**
|
|
80
|
+
* Each operand rendered into its own fragment, keeping only those that emitted SQL.
|
|
81
|
+
*
|
|
82
|
+
* Nothing reaches `ctx` until every one has rendered, because an operand that emits nothing - an
|
|
83
|
+
* empty `$and`, an `{}` entry - must leave behind neither a dangling separator nor a clause with no
|
|
84
|
+
* condition after it. How many terms really emit is also what decides the parentheses, which is why
|
|
85
|
+
* the caller counts what comes back rather than what it passed in.
|
|
86
|
+
*/
|
|
87
|
+
protected renderOperands<T>(ctx: QueryContext, operands: readonly T[], render: (ctx: QueryContext, operand: T) => void): string[];
|
|
79
88
|
addValue(values: unknown[], value: unknown): string;
|
|
80
89
|
/**
|
|
81
90
|
* Normalizes a parameter value for the database driver.
|
|
@@ -134,13 +143,12 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
134
143
|
protected selectRelationFields(ctx: QueryContext, joins: QueryJoins): void;
|
|
135
144
|
protected selectRelationJoins<E>(ctx: QueryContext, meta: EntityMeta<E>, rootAlias: string, joins: QueryJoins): void;
|
|
136
145
|
where<E>(ctx: QueryContext, entity: Type<E>, where?: QueryWhere<E>, opts?: QueryWhereOptions): void;
|
|
137
|
-
/** Renders a `$where` tree without applying entity filters (used for same-scope
|
|
146
|
+
/** Renders a `$where` tree without applying entity filters (used for same-scope group-operator recursion). */
|
|
138
147
|
protected renderWhere<E>(ctx: QueryContext, entity: Type<E>, where?: QueryWhere<E>, opts?: QueryWhereOptions): void;
|
|
139
148
|
compare<E>(ctx: QueryContext, entity: Type<E>, key: string, val: unknown, opts?: QueryComparisonOptions): void;
|
|
140
|
-
protected compareLogicalOperator<E>(ctx: QueryContext, entity: Type<E>, key:
|
|
149
|
+
protected compareLogicalOperator<E>(ctx: QueryContext, entity: Type<E>, key: QueryGroupOp, val: QueryWhereArray<E>, opts: QueryComparisonOptions): void;
|
|
141
150
|
/** Memoizes {@link escapedColumnName}; see there for why it is per dialect instance. */
|
|
142
151
|
private readonly escapedColumns;
|
|
143
|
-
private static readonly NEGATE_OP_MAP;
|
|
144
152
|
private static readonly COMPARE_OP_MAP;
|
|
145
153
|
/** What a `$near` says about the search itself; everything else in it is a bound. */
|
|
146
154
|
private static readonly VECTOR_QUERY_KEYS;
|
|
@@ -100,6 +100,19 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
100
100
|
build(fragmentCtx);
|
|
101
101
|
return fragmentCtx.sql;
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Each operand rendered into its own fragment, keeping only those that emitted SQL.
|
|
105
|
+
*
|
|
106
|
+
* Nothing reaches `ctx` until every one has rendered, because an operand that emits nothing - an
|
|
107
|
+
* empty `$and`, an `{}` entry - must leave behind neither a dangling separator nor a clause with no
|
|
108
|
+
* condition after it. How many terms really emit is also what decides the parentheses, which is why
|
|
109
|
+
* the caller counts what comes back rather than what it passed in.
|
|
110
|
+
*/
|
|
111
|
+
renderOperands(ctx, operands, render) {
|
|
112
|
+
return operands
|
|
113
|
+
.map((operand) => this.buildFragment(ctx, (fragmentCtx) => render(fragmentCtx, operand)))
|
|
114
|
+
.filter((part) => part !== '');
|
|
115
|
+
}
|
|
103
116
|
addValue(values, value) {
|
|
104
117
|
values.push(this.normalizeValue(value));
|
|
105
118
|
return this.placeholder(values.length);
|
|
@@ -306,40 +319,29 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
306
319
|
// Filters are applied once, here at the scope entry point; recursion uses `renderWhere`.
|
|
307
320
|
this.renderWhere(ctx, entity, this.scopedWhereMap(meta, where, opts), opts);
|
|
308
321
|
}
|
|
309
|
-
/** Renders a `$where` tree without applying entity filters (used for same-scope
|
|
322
|
+
/** Renders a `$where` tree without applying entity filters (used for same-scope group-operator recursion). */
|
|
310
323
|
renderWhere(ctx, entity, where = {}, opts = {}) {
|
|
311
324
|
const meta = getMeta(entity);
|
|
312
325
|
const { clause = 'WHERE' } = opts;
|
|
313
|
-
|
|
326
|
+
const whereMap = buildQueryWhereAsMap(meta, where);
|
|
314
327
|
// An `undefined` value emits nothing, so it must not count towards the terms either: it decides
|
|
315
|
-
//
|
|
316
|
-
const whereKeys = getKeys(
|
|
317
|
-
|
|
328
|
+
// whether the keys below render as operands of an `AND`.
|
|
329
|
+
const whereKeys = getKeys(whereMap).filter((key) => whereMap[key] !== undefined);
|
|
330
|
+
// Each key is an operand of the `AND` joining them; a lone key emits this fragment verbatim, so
|
|
331
|
+
// it inherits this one's position instead.
|
|
332
|
+
const childOperand = whereKeys.length > 1 || opts.operand || clause === 'AND';
|
|
333
|
+
const childOpts = opts.operand === childOperand ? opts : { ...opts, operand: childOperand };
|
|
334
|
+
const parts = this.renderOperands(ctx, whereKeys, (fragmentCtx, key) => this.compare(fragmentCtx, entity, key, whereMap[key], childOpts));
|
|
335
|
+
if (!parts.length) {
|
|
318
336
|
return;
|
|
319
337
|
}
|
|
320
338
|
if (clause) {
|
|
321
339
|
ctx.append(` ${clause} `);
|
|
322
340
|
}
|
|
323
|
-
const multipleKeys = whereKeys.length > 1;
|
|
324
341
|
// This fragment joins its own keys with `AND`, so appending it after one (a JOIN's `ON`) needs no
|
|
325
342
|
// parentheses - but anything nested in it is still an operand, since that may be an `OR`.
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
ctx.append('(');
|
|
329
|
-
}
|
|
330
|
-
// Each key is an operand of the `AND` joining them; a lone key emits this fragment verbatim, so
|
|
331
|
-
// it inherits this one's position instead.
|
|
332
|
-
const childOperand = multipleKeys || opts.operand || clause === 'AND';
|
|
333
|
-
const childOpts = opts.operand === childOperand ? opts : { ...opts, operand: childOperand };
|
|
334
|
-
whereKeys.forEach((key, index) => {
|
|
335
|
-
if (index > 0) {
|
|
336
|
-
ctx.append(' AND ');
|
|
337
|
-
}
|
|
338
|
-
this.compare(ctx, entity, key, where[key], childOpts);
|
|
339
|
-
});
|
|
340
|
-
if (parenthesize) {
|
|
341
|
-
ctx.append(')');
|
|
342
|
-
}
|
|
343
|
+
const body = parts.join(' AND ');
|
|
344
|
+
ctx.append(parts.length > 1 && opts.operand ? `(${body})` : body);
|
|
343
345
|
}
|
|
344
346
|
compare(ctx, entity, key, val, opts = {}) {
|
|
345
347
|
const meta = getMeta(entity);
|
|
@@ -366,7 +368,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
366
368
|
this.appendTextSearch(ctx, entity, meta, val);
|
|
367
369
|
return;
|
|
368
370
|
}
|
|
369
|
-
if (key
|
|
371
|
+
if (AbstractSqlDialect.isGroupOp(key)) {
|
|
370
372
|
this.compareLogicalOperator(ctx, entity, key, val, opts);
|
|
371
373
|
return;
|
|
372
374
|
}
|
|
@@ -405,43 +407,29 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
405
407
|
}
|
|
406
408
|
}
|
|
407
409
|
compareLogicalOperator(ctx, entity, key, val, opts) {
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
if (val !== undefined && !Array.isArray(val)) {
|
|
411
|
-
// Not covered by the types: `/http` casts client JSON straight to `Query`, so this arrives untyped.
|
|
412
|
-
throw TypeError(`${key} expects an array, got ${val === null ? 'null' : typeof val}`);
|
|
413
|
-
}
|
|
414
|
-
const items = val ?? [];
|
|
410
|
+
const { join, negate } = AbstractSqlDialect.GROUP_OPS[key];
|
|
411
|
+
const items = AbstractSqlDialect.groupClauses(key, val);
|
|
415
412
|
// With more than one item each is an operand of the operator joining them, so a compound item
|
|
416
413
|
// parenthesizes itself and precedence never applies; a lone item is this group verbatim, so it
|
|
417
414
|
// inherits the group's own position. A negation always makes its subject an operand.
|
|
418
415
|
const childOperand = items.length > 1 || negate || opts.operand;
|
|
419
|
-
|
|
420
|
-
// `undefined` entry) must leave no dangling separator behind, and how many terms this fragment
|
|
421
|
-
// really emits is what decides whether it needs parentheses.
|
|
422
|
-
const parts = items
|
|
423
|
-
.map((entry) => this.buildFragment(ctx, (fragmentCtx) => {
|
|
416
|
+
const parts = this.renderOperands(ctx, items, (fragmentCtx, entry) => {
|
|
424
417
|
if (entry instanceof QueryRaw) {
|
|
425
418
|
this.getRawValue(fragmentCtx, { value: entry });
|
|
426
419
|
}
|
|
427
420
|
else if (entry) {
|
|
428
421
|
this.renderWhere(fragmentCtx, entity, entry, { prefix: opts.prefix, operand: childOperand, clause: false });
|
|
429
422
|
}
|
|
430
|
-
})
|
|
431
|
-
.filter((part) => part !== '');
|
|
423
|
+
});
|
|
432
424
|
if (!parts.length) {
|
|
433
425
|
return;
|
|
434
426
|
}
|
|
435
|
-
const body = parts.join(
|
|
427
|
+
const body = parts.join(join === '$or' ? ' OR ' : ' AND ');
|
|
436
428
|
const parenthesize = parts.length > 1 && (opts.operand || negate);
|
|
437
429
|
ctx.append((negate ? 'NOT ' : '') + (parenthesize ? `(${body})` : body));
|
|
438
430
|
}
|
|
439
431
|
/** Memoizes {@link escapedColumnName}; see there for why it is per dialect instance. */
|
|
440
432
|
escapedColumns = new WeakMap();
|
|
441
|
-
static NEGATE_OP_MAP = new Map([
|
|
442
|
-
['$not', '$and'],
|
|
443
|
-
['$nor', '$or'],
|
|
444
|
-
]);
|
|
445
433
|
static COMPARE_OP_MAP = new Map([
|
|
446
434
|
['$gt', ' > '],
|
|
447
435
|
['$gte', ' >= '],
|
|
@@ -43,11 +43,21 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
43
43
|
/** Whether a `$where` constrains any relation, and so needs the aggregation path rather than a cursor. */
|
|
44
44
|
constrainsRelations<E extends Document>(entity: Type<E>, where: QueryWhere<E> | undefined): boolean;
|
|
45
45
|
/**
|
|
46
|
-
* Renders a `$where` tree without applying entity filters (used for same-scope
|
|
46
|
+
* Renders a `$where` tree without applying entity filters (used for same-scope group-operator
|
|
47
47
|
* recursion). Relation keys need `$lookup` stages, so they are only accepted when `lookups` is
|
|
48
48
|
* given - a plain `find`/`updateMany` filter has nowhere to put them.
|
|
49
49
|
*/
|
|
50
50
|
private renderFilter;
|
|
51
|
+
/**
|
|
52
|
+
* Renders `$and`/`$or`/`$not`/`$nor` into `filter`. MongoDB has no root-level `$not`, so both
|
|
53
|
+
* negating operators become its `$nor`, which is exactly `NOT (a OR b)` - and by De Morgan that
|
|
54
|
+
* makes a `$nor` list its clauses directly while a `$not` wraps them in one `$and` first.
|
|
55
|
+
*
|
|
56
|
+
* Clauses that render to nothing are dropped and an empty operator emits no key at all: MongoDB
|
|
57
|
+
* rejects an empty `$and`/`$or`/`$nor` outright, where the SQL dialects contribute no term.
|
|
58
|
+
* Negations accumulate into the one `$nor`, since `NOT a AND NOT b` is `$nor: [a, b]`.
|
|
59
|
+
*/
|
|
60
|
+
private appendLogicalOperator;
|
|
51
61
|
/**
|
|
52
62
|
* Emits the correlated `$lookup` for one relation condition and returns the condition that tests its
|
|
53
63
|
* result: presence of a row for a plain relation filter, a comparison against the row count for
|
|
@@ -83,12 +83,12 @@ export class MongoDialect extends AbstractDialect {
|
|
|
83
83
|
}
|
|
84
84
|
const meta = getMeta(entity);
|
|
85
85
|
const whereMap = buildQueryWhereAsMap(meta, where);
|
|
86
|
-
return someKey(whereMap, (key) => key
|
|
87
|
-
? whereMap[key].some((it) => this.constrainsRelations(entity, it))
|
|
86
|
+
return someKey(whereMap, (key) => MongoDialect.isGroupOp(key)
|
|
87
|
+
? (whereMap[key] ?? []).some((it) => this.constrainsRelations(entity, it))
|
|
88
88
|
: Boolean(meta.relations[key]));
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
* Renders a `$where` tree without applying entity filters (used for same-scope
|
|
91
|
+
* Renders a `$where` tree without applying entity filters (used for same-scope group-operator
|
|
92
92
|
* recursion). Relation keys need `$lookup` stages, so they are only accepted when `lookups` is
|
|
93
93
|
* given - a plain `find`/`updateMany` filter has nowhere to put them.
|
|
94
94
|
*/
|
|
@@ -99,13 +99,8 @@ export class MongoDialect extends AbstractDialect {
|
|
|
99
99
|
for (const [rawKey, rawVal] of Object.entries(whereMap)) {
|
|
100
100
|
let key = rawKey;
|
|
101
101
|
let val = rawVal;
|
|
102
|
-
if (key
|
|
103
|
-
filter
|
|
104
|
-
// A `QueryRaw` here would recurse forever: `buildQueryWhereAsMap` re-wraps it as
|
|
105
|
-
// `{ $and: [raw] }`, which lands back on this branch.
|
|
106
|
-
this.assertNoRaw(filterIt);
|
|
107
|
-
return this.renderFilter(entity, filterIt, opts, lookups);
|
|
108
|
-
});
|
|
102
|
+
if (MongoDialect.isGroupOp(key)) {
|
|
103
|
+
this.appendLogicalOperator(filter, entity, key, val, opts, lookups);
|
|
109
104
|
}
|
|
110
105
|
else if (key === '$text') {
|
|
111
106
|
// MongoDB's text index declares which fields it covers, so `$fields` cannot narrow the search
|
|
@@ -137,6 +132,35 @@ export class MongoDialect extends AbstractDialect {
|
|
|
137
132
|
}
|
|
138
133
|
return filter;
|
|
139
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Renders `$and`/`$or`/`$not`/`$nor` into `filter`. MongoDB has no root-level `$not`, so both
|
|
137
|
+
* negating operators become its `$nor`, which is exactly `NOT (a OR b)` - and by De Morgan that
|
|
138
|
+
* makes a `$nor` list its clauses directly while a `$not` wraps them in one `$and` first.
|
|
139
|
+
*
|
|
140
|
+
* Clauses that render to nothing are dropped and an empty operator emits no key at all: MongoDB
|
|
141
|
+
* rejects an empty `$and`/`$or`/`$nor` outright, where the SQL dialects contribute no term.
|
|
142
|
+
* Negations accumulate into the one `$nor`, since `NOT a AND NOT b` is `$nor: [a, b]`.
|
|
143
|
+
*/
|
|
144
|
+
appendLogicalOperator(filter, entity, key, val, opts, lookups) {
|
|
145
|
+
const { join, negate } = MongoDialect.GROUP_OPS[key];
|
|
146
|
+
const parts = MongoDialect.groupClauses(key, val)
|
|
147
|
+
.map((filterIt) => {
|
|
148
|
+
// A `QueryRaw` here would recurse forever: `buildQueryWhereAsMap` re-wraps it as
|
|
149
|
+
// `{ $and: [raw] }`, which lands back on this branch.
|
|
150
|
+
this.assertNoRaw(filterIt);
|
|
151
|
+
return this.renderFilter(entity, filterIt, opts, lookups);
|
|
152
|
+
})
|
|
153
|
+
.filter((part) => Object.keys(part).length > 0);
|
|
154
|
+
if (!parts.length) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!negate) {
|
|
158
|
+
filter[key] = parts;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const negated = join === '$and' && parts.length > 1 ? [{ $and: parts }] : parts;
|
|
162
|
+
filter['$nor'] = [...(filter['$nor'] ?? []), ...negated];
|
|
163
|
+
}
|
|
140
164
|
/**
|
|
141
165
|
* Emits the correlated `$lookup` for one relation condition and returns the condition that tests its
|
|
142
166
|
* result: presence of a row for a plain relation filter, a comparison against the row count for
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertSoleId, getMeta, idOf, soleIdOf } from '../entity/index.js';
|
|
2
|
-
import { asSelectMap, augmentWhere, childrenOf, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns,
|
|
2
|
+
import { asSelectMap, augmentWhere, childrenOf, clone, dataKeyed, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns, keyColumns, LoggerWrapper, isBoundedPerParent, parentJoins, queryChildrenOfAll, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
3
|
import { enrichError } from './queryError.js';
|
|
4
4
|
import { fillRelationCounts, withIdForCounts } from './relationCount.js';
|
|
5
5
|
/**
|
|
@@ -377,17 +377,19 @@ export class AbstractQuerier {
|
|
|
377
377
|
return founds;
|
|
378
378
|
}
|
|
379
379
|
putChildrenInParents(parents, children, joins, relKey) {
|
|
380
|
-
const childrenByParentId =
|
|
380
|
+
const childrenByParentId = dataKeyed();
|
|
381
|
+
// Every joined column, so two children agreeing on one column of a composite key are not
|
|
382
|
+
// gathered under the same parent. Both column lists are read once, not once per row.
|
|
383
|
+
const joinedKeys = keyColumns(joins, 'joined');
|
|
384
|
+
const parentKeys = keyColumns(joins, 'parent');
|
|
381
385
|
for (const child of children) {
|
|
382
|
-
|
|
383
|
-
// gathered under the same parent.
|
|
384
|
-
(childrenByParentId[joinedRowKey(joins, child)] ??= []).push(child);
|
|
386
|
+
(childrenByParentId[rowKey(child, joinedKeys)] ??= []).push(child);
|
|
385
387
|
}
|
|
386
388
|
for (const parent of parents) {
|
|
387
389
|
// `[]` rather than nothing for a parent with no children: a populated to-many is a list the
|
|
388
390
|
// caller asked for, so it maps and counts without a guard, and its type can say so. An
|
|
389
391
|
// unpopulated one stays absent, which is what tells the two apart.
|
|
390
|
-
parent[relKey] = (childrenByParentId[
|
|
392
|
+
parent[relKey] = (childrenByParentId[rowKey(parent, parentKeys)] ?? []);
|
|
391
393
|
}
|
|
392
394
|
}
|
|
393
395
|
async insertRelations(entity, payload) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
2
2
|
import { getMeta, soleIdOf } from '../entity/index.js';
|
|
3
3
|
import { COUNT_RESULT_KEY } from '../type/index.js';
|
|
4
|
-
import { asSelectMap, getKeys, joinedColumns,
|
|
4
|
+
import { asSelectMap, dataKeyed, getKeys, joinedColumns, keyColumns, parentJoins, parentsIn, rowKey, targetKeyColumns, } from '../util/index.js';
|
|
5
5
|
/**
|
|
6
6
|
* A `$count` groups its tallies by the parent's id, so the id has to outlive the projection - the
|
|
7
7
|
* same reason populating a relation keeps it. A whitelisting `$select` gains the key and an
|
|
@@ -50,7 +50,7 @@ export async function fillRelationCounts(querier, entity, payload, count) {
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
const meta = getMeta(entity);
|
|
53
|
-
// The tallies come back keyed by the columns *this relation* joins from, so
|
|
53
|
+
// The tallies come back keyed by the columns *this relation* joins from, so those columns are kept
|
|
54
54
|
// beside them: reading the parent through `meta.ids` instead matches only where the two coincide,
|
|
55
55
|
// which is a to-many and nothing else.
|
|
56
56
|
const counted = new Map();
|
|
@@ -62,14 +62,19 @@ export async function fillRelationCounts(querier, entity, payload, count) {
|
|
|
62
62
|
}
|
|
63
63
|
const where = typeof value === 'object' ? value.$where : undefined;
|
|
64
64
|
const joins = parentJoins(relOpts, meta.ids.length);
|
|
65
|
-
counted.set(relKey, {
|
|
65
|
+
counted.set(relKey, {
|
|
66
|
+
parentKeys: keyColumns(joins, 'parent'),
|
|
67
|
+
byParent: await countPerParent(querier, relOpts, joins, payload, where),
|
|
68
|
+
});
|
|
66
69
|
}
|
|
67
70
|
for (const parent of payload) {
|
|
71
|
+
// A plain object, unlike the tallies below: this one is keyed by relation names the entity
|
|
72
|
+
// declares, not by data, and it is handed to the caller - who would meet a null prototype.
|
|
68
73
|
const row = {};
|
|
69
|
-
for (const [relKey, {
|
|
74
|
+
for (const [relKey, { parentKeys, byParent }] of counted) {
|
|
70
75
|
// A parent the grouped result has no row for matched nothing, which is a zero rather than a
|
|
71
76
|
// gap: `_count` names what the caller asked to count, so every key it asked for is present.
|
|
72
|
-
row[relKey] = byParent[
|
|
77
|
+
row[relKey] = byParent[rowKey(parent, parentKeys)] ?? 0;
|
|
73
78
|
}
|
|
74
79
|
parent[COUNT_RESULT_KEY] = row;
|
|
75
80
|
}
|
|
@@ -105,11 +110,12 @@ async function groupedCount(querier, entity, joins, where) {
|
|
|
105
110
|
const $agg = { [COUNT_ALIAS]: { $count: '*' } };
|
|
106
111
|
const $group = joinedColumns(joins);
|
|
107
112
|
const rows = await querier.aggregate(entity, { $group, $agg, $where: where });
|
|
108
|
-
const byParent =
|
|
113
|
+
const byParent = dataKeyed();
|
|
114
|
+
// Keyed by every joined column, which is how a tally finds the one parent whose whole key it
|
|
115
|
+
// matches - and how the rows an over-selecting `IN` brought back find no parent at all.
|
|
116
|
+
const joinedKeys = keyColumns(joins, 'joined');
|
|
109
117
|
for (const row of rows) {
|
|
110
|
-
|
|
111
|
-
// matches - and how the rows an over-selecting `IN` brought back find no parent at all.
|
|
112
|
-
byParent[joinedRowKey(joins, row)] = Number(row[COUNT_ALIAS]);
|
|
118
|
+
byParent[rowKey(row, joinedKeys)] = Number(row[COUNT_ALIAS]);
|
|
113
119
|
}
|
|
114
120
|
return byParent;
|
|
115
121
|
}
|
|
@@ -85,6 +85,16 @@ export type QueryWhereRootOperator<E> = {
|
|
|
85
85
|
* {@link QueryWhereRootOperator} so a rename there breaks this union at compile time.
|
|
86
86
|
*/
|
|
87
87
|
export type QueryNegateOp = keyof Pick<QueryWhereRootOperator<unknown>, '$not' | '$nor'>;
|
|
88
|
+
/**
|
|
89
|
+
* The root operators that join their clauses instead of negating them, tied back to
|
|
90
|
+
* {@link QueryWhereRootOperator} on the same terms as {@link QueryNegateOp}.
|
|
91
|
+
*/
|
|
92
|
+
export type QueryJoinOp = keyof Pick<QueryWhereRootOperator<unknown>, '$and' | '$or'>;
|
|
93
|
+
/**
|
|
94
|
+
* Every root operator whose value is a {@link QueryWhereArray} rather than a field condition: the
|
|
95
|
+
* two that join their clauses and the two that negate the join.
|
|
96
|
+
*/
|
|
97
|
+
export type QueryGroupOp = QueryJoinOp | QueryNegateOp;
|
|
88
98
|
/**
|
|
89
99
|
* Comparison operators accepted by `$size` for range queries: {@link QueryHavingOp} plus `$between`.
|
|
90
100
|
* Strips `null` from picked operators since array size is always numeric.
|
|
@@ -320,7 +330,7 @@ type IsUntypedColumn<T> = [Scalar] extends [NonNullable<T>] ? true : false;
|
|
|
320
330
|
*/
|
|
321
331
|
export type QueryWhereFieldValue<T> = T | (undefined extends T ? null : never) | (IsMany<T> extends true ? never : T[]) | QueryWhereFieldOperators<T> | QueryRaw;
|
|
322
332
|
/**
|
|
323
|
-
* query filter array -
|
|
333
|
+
* query filter array - the value every {@link QueryGroupOp} takes.
|
|
324
334
|
*/
|
|
325
335
|
export type QueryWhereArray<E> = (QueryWhereMap<E> | QueryRaw)[];
|
|
326
336
|
/**
|
|
@@ -35,13 +35,16 @@ export declare function targetKeyColumns(relOpts: Pick<RelationMeta, 'references
|
|
|
35
35
|
/** `{ joined column: true }`: the projection or grouping that keeps a parent's key on the rows read. */
|
|
36
36
|
export declare function joinedColumns(joins: readonly ParentJoin[]): Record<string, true>;
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* One side's columns: `'parent'` for the columns a parent is keyed by, `'joined'` for the ones a
|
|
39
|
+
* child or tally row carries that key in. Matching the two halves means agreeing on every column, so
|
|
40
|
+
* each side is read through `joins` rather than through the parent's own key list - the same set only
|
|
41
|
+
* for a to-many, and silently a different one otherwise. `keyof ParentJoin` is what keeps the two
|
|
42
|
+
* sides from being spelled apart.
|
|
43
|
+
*
|
|
44
|
+
* Lifted out of `joins` once per relation, not once per row: {@link rowKey} takes the list and reads
|
|
45
|
+
* each row itself, so a page of children costs one key each and nothing else.
|
|
42
46
|
*/
|
|
43
|
-
export declare function
|
|
44
|
-
export declare function joinedRowKey(joins: readonly ParentJoin[], row: unknown): string;
|
|
47
|
+
export declare function keyColumns(joins: readonly ParentJoin[], side: keyof ParentJoin): string[];
|
|
45
48
|
/**
|
|
46
49
|
* `{ joined column: every parent's value for it }`, the filter that fetches a whole page of parents'
|
|
47
50
|
* children in one statement.
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUERY_ROOT_NUMBER_CLAUSES, } from '../type/query.js';
|
|
2
2
|
import { getKeys, someKey } from './object.util.js';
|
|
3
|
-
import { rowKey } from './rowKey.util.js';
|
|
4
3
|
/**
|
|
5
4
|
* Whether a relation holds many rows per parent, so it cannot be joined into the parent's row. Takes
|
|
6
5
|
* the one field it reads, so it answers for a relation being declared as well as for a resolved one.
|
|
@@ -37,19 +36,20 @@ export function targetKeyColumns(relOpts, parentKeyCount) {
|
|
|
37
36
|
}
|
|
38
37
|
/** `{ joined column: true }`: the projection or grouping that keeps a parent's key on the rows read. */
|
|
39
38
|
export function joinedColumns(joins) {
|
|
40
|
-
return Object.fromEntries(joins.map((
|
|
39
|
+
return Object.fromEntries(keyColumns(joins, 'joined').map((column) => [column, true]));
|
|
41
40
|
}
|
|
42
41
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
42
|
+
* One side's columns: `'parent'` for the columns a parent is keyed by, `'joined'` for the ones a
|
|
43
|
+
* child or tally row carries that key in. Matching the two halves means agreeing on every column, so
|
|
44
|
+
* each side is read through `joins` rather than through the parent's own key list - the same set only
|
|
45
|
+
* for a to-many, and silently a different one otherwise. `keyof ParentJoin` is what keeps the two
|
|
46
|
+
* sides from being spelled apart.
|
|
47
|
+
*
|
|
48
|
+
* Lifted out of `joins` once per relation, not once per row: {@link rowKey} takes the list and reads
|
|
49
|
+
* each row itself, so a page of children costs one key each and nothing else.
|
|
47
50
|
*/
|
|
48
|
-
export function
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
export function joinedRowKey(joins, row) {
|
|
52
|
-
return rowKey(joins.map(({ joined }) => read(row, joined)));
|
|
51
|
+
export function keyColumns(joins, side) {
|
|
52
|
+
return joins.map((join) => join[side]);
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
55
|
* `{ joined column: every parent's value for it }`, the filter that fetches a whole page of parents'
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A row's key as a string, for matching rows to each other in a
|
|
2
|
+
* A row's key as a string, for matching rows to each other in a {@link dataKeyed} lookup.
|
|
3
|
+
*
|
|
4
|
+
* Reads the columns off the row rather than taking their values, because every caller matches a
|
|
5
|
+
* whole page of rows against one fixed column list: taking an array would make each of them build
|
|
6
|
+
* one per row, which is what a page of 250 rows paid 56 KB for.
|
|
3
7
|
*
|
|
4
8
|
* Values are normalized before joining, not stringified: `String(date)` is locale- and
|
|
5
9
|
* timezone-dependent, so two equal dates could key apart, and a `Uint8Array` stringifies to its
|
|
6
|
-
* bytes with commas. Every
|
|
7
|
-
* are not treated as one row.
|
|
10
|
+
* bytes with commas. Every column is included, so two rows agreeing on one column of a composite
|
|
11
|
+
* key are not treated as one row.
|
|
12
|
+
*/
|
|
13
|
+
export declare function rowKey(row: unknown, columns: readonly string[]): string;
|
|
14
|
+
/**
|
|
15
|
+
* A lookup keyed by data rather than by a name this code chose, so a key that spells `__proto__` or
|
|
16
|
+
* `constructor` is an ordinary entry instead of the prototype: on `{}` those threw when a bucket was
|
|
17
|
+
* pushed to, and a `_count` tally under one silently read back as an object. Cheaper than a `Map`
|
|
18
|
+
* here, and faster than `{}`, which walks the prototype chain on every miss.
|
|
19
|
+
*
|
|
20
|
+
* It carries none of `Object.prototype`, which no type can say: index it and spread it, but calling
|
|
21
|
+
* `hasOwnProperty` on one type-checks and throws.
|
|
8
22
|
*/
|
|
9
|
-
export declare function
|
|
23
|
+
export declare function dataKeyed<V>(): Record<string, V>;
|
package/dist/util/rowKey.util.js
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
/** Separates the parts of a composite key: a unit separator, which no column value carries. */
|
|
2
2
|
const KEY_SEPARATOR = '\u001f';
|
|
3
3
|
/**
|
|
4
|
-
* A row's key as a string, for matching rows to each other in a
|
|
4
|
+
* A row's key as a string, for matching rows to each other in a {@link dataKeyed} lookup.
|
|
5
|
+
*
|
|
6
|
+
* Reads the columns off the row rather than taking their values, because every caller matches a
|
|
7
|
+
* whole page of rows against one fixed column list: taking an array would make each of them build
|
|
8
|
+
* one per row, which is what a page of 250 rows paid 56 KB for.
|
|
5
9
|
*
|
|
6
10
|
* Values are normalized before joining, not stringified: `String(date)` is locale- and
|
|
7
11
|
* timezone-dependent, so two equal dates could key apart, and a `Uint8Array` stringifies to its
|
|
8
|
-
* bytes with commas. Every
|
|
9
|
-
* are not treated as one row.
|
|
12
|
+
* bytes with commas. Every column is included, so two rows agreeing on one column of a composite
|
|
13
|
+
* key are not treated as one row.
|
|
14
|
+
*/
|
|
15
|
+
export function rowKey(row, columns) {
|
|
16
|
+
const values = row;
|
|
17
|
+
let key = '';
|
|
18
|
+
for (let i = 0; i < columns.length; i++) {
|
|
19
|
+
if (i) {
|
|
20
|
+
key += KEY_SEPARATOR;
|
|
21
|
+
}
|
|
22
|
+
key += keyPart(values[columns[i]]);
|
|
23
|
+
}
|
|
24
|
+
return key;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A lookup keyed by data rather than by a name this code chose, so a key that spells `__proto__` or
|
|
28
|
+
* `constructor` is an ordinary entry instead of the prototype: on `{}` those threw when a bucket was
|
|
29
|
+
* pushed to, and a `_count` tally under one silently read back as an object. Cheaper than a `Map`
|
|
30
|
+
* here, and faster than `{}`, which walks the prototype chain on every miss.
|
|
31
|
+
*
|
|
32
|
+
* It carries none of `Object.prototype`, which no type can say: index it and spread it, but calling
|
|
33
|
+
* `hasOwnProperty` on one type-checks and throws.
|
|
10
34
|
*/
|
|
11
|
-
export function
|
|
12
|
-
return
|
|
35
|
+
export function dataKeyed() {
|
|
36
|
+
return Object.create(null);
|
|
13
37
|
}
|
|
14
38
|
function keyPart(value) {
|
|
15
39
|
if (value instanceof Date) {
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
4
|
"description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.47.
|
|
6
|
+
"version": "0.47.1",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|