uql-orm 0.36.1 → 0.37.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/browser/http/http.d.ts +1 -1
- package/dist/browser/querier/httpQuerier.d.ts +12 -7
- package/dist/browser/querier/httpQuerier.js +6 -1
- package/dist/browser/type/clientQuerier.d.ts +10 -23
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +7 -7
- package/dist/cockroachdb/cockroachDialect.d.ts +8 -1
- package/dist/cockroachdb/cockroachDialect.js +12 -0
- package/dist/dialect/abstractSqlDialect.d.ts +31 -4
- package/dist/dialect/abstractSqlDialect.js +64 -15
- package/dist/dialect/aliases.d.ts +30 -0
- package/dist/dialect/aliases.js +32 -0
- package/dist/dialect/jsonSql.d.ts +0 -20
- package/dist/dialect/jsonSql.js +0 -20
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +6 -0
- package/dist/dialect/mysqlLikeSqlDialect.js +20 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +2 -0
- package/dist/dialect/pgLikeSqlDialect.js +4 -1
- package/dist/dialect/queryJoins.d.ts +6 -0
- package/dist/dialect/queryJoins.js +13 -0
- package/dist/http/contract.d.ts +0 -7
- package/dist/http/handler.d.ts +2 -2
- package/dist/http/handler.js +1 -1
- package/dist/http/query.js +3 -2
- package/dist/mongo/mongoDialect.d.ts +15 -4
- package/dist/mongo/mongoDialect.js +65 -16
- package/dist/mongo/mongodbQuerier.d.ts +13 -2
- package/dist/mongo/mongodbQuerier.js +46 -2
- package/dist/postgres/postgresDialect.d.ts +7 -0
- package/dist/postgres/postgresDialect.js +13 -0
- package/dist/querier/abstractQuerier.d.ts +34 -18
- package/dist/querier/abstractQuerier.js +43 -30
- package/dist/querier/abstractQuerierPool.d.ts +9 -7
- package/dist/querier/abstractQuerierPool.js +6 -0
- package/dist/querier/abstractSqlQuerier.d.ts +30 -2
- package/dist/querier/abstractSqlQuerier.js +61 -8
- package/dist/querier/relationCount.d.ts +16 -0
- package/dist/querier/relationCount.js +104 -0
- package/dist/sqlite/sqliteDialect.d.ts +6 -1
- package/dist/sqlite/sqliteDialect.js +12 -1
- package/dist/type/dialect.d.ts +10 -3
- package/dist/type/index.d.ts +1 -0
- package/dist/type/index.js +1 -0
- package/dist/type/querier.d.ts +21 -14
- package/dist/type/query.d.ts +77 -16
- package/dist/type/query.js +17 -0
- package/dist/type/universalQuerier.d.ts +85 -48
- package/dist/type/wire.d.ts +37 -0
- package/dist/type/wire.js +1 -0
- package/dist/util/dialect.util.d.ts +6 -0
- package/dist/util/dialect.util.js +15 -0
- package/dist/util/relationQuery.util.d.ts +9 -0
- package/dist/util/relationQuery.util.js +13 -0
- package/package.json +4 -4
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ObjectId } from 'mongodb';
|
|
2
2
|
import { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
3
|
+
import { COUNT_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, sortCountField } from '../dialect/aliases.js';
|
|
3
4
|
import { resolveQueryJoins, resolveSortableJoin } from '../dialect/queryJoins.js';
|
|
4
5
|
import { getMeta } from '../entity/index.js';
|
|
5
6
|
import { QueryRaw } from '../type/queryRaw.js';
|
|
6
|
-
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap, fillOnFields, filterFieldKeys, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationSize, someKey, } from '../util/index.js';
|
|
7
|
+
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap, fillOnFields, filterFieldKeys, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationSize, parseSortByCount, someKey, } from '../util/index.js';
|
|
7
8
|
/** Default {@link DialectFeatures} for MongoDB; shared by {@link MongoDialect} and its schema generator. */
|
|
8
9
|
export const mongoDialectFeatures = {
|
|
9
10
|
explicitJsonCast: false,
|
|
@@ -27,10 +28,6 @@ export class MongoDialect extends AbstractDialect {
|
|
|
27
28
|
// The MongoDB driver reports the exact `_id` of every inserted document (`insertedIds`).
|
|
28
29
|
insertIdSource = 'returning';
|
|
29
30
|
static ID_KEY = '_id';
|
|
30
|
-
/** Temporary lookup fields for relation conditions, dropped with `$unset` after the `$match`. */
|
|
31
|
-
static REL_TEMP_PREFIX = '__uql_rel_';
|
|
32
|
-
static REL_COUNT_KEY = 'n';
|
|
33
|
-
static REL_NESTED_KEY = '__uql_target';
|
|
34
31
|
static VECTOR_INDEX_TYPES = new Set(['vectorSearch', 'hnsw', 'ivfflat', 'vector']);
|
|
35
32
|
/** Atlas rejects a `$vectorSearch` asking for more candidates than this. */
|
|
36
33
|
static MAX_NUM_CANDIDATES = 10_000;
|
|
@@ -142,10 +139,10 @@ export class MongoDialect extends AbstractDialect {
|
|
|
142
139
|
const relOpts = meta.relations[relKey];
|
|
143
140
|
const relEntity = relOpts.entity();
|
|
144
141
|
const relMeta = getMeta(relEntity);
|
|
145
|
-
const temp = `${
|
|
142
|
+
const temp = `${REL_TEMP_PREFIX}${lookups.temps.length}`;
|
|
146
143
|
const sizeVal = parseRelationSize(val);
|
|
147
144
|
// `$count` for a size test, `$limit: 1` for existence: neither returns the matched documents.
|
|
148
|
-
const tail = sizeVal === undefined ? [{ $limit: 1 }] : [{ $count:
|
|
145
|
+
const tail = sizeVal === undefined ? [{ $limit: 1 }] : [{ $count: COUNT_ALIAS }];
|
|
149
146
|
// Scope first, render once - merging the target's filters into an already-rendered filter would
|
|
150
147
|
// leave their own keys unmapped. The caller's filter bypass is deliberately *not* passed down:
|
|
151
148
|
// `withDeleted()` or `hardDelete` on the parent must not un-hide trashed rows of the target, the
|
|
@@ -153,7 +150,18 @@ export class MongoDialect extends AbstractDialect {
|
|
|
153
150
|
const targetCondition = (sizeVal === undefined ? val : {});
|
|
154
151
|
const targetScope = this.renderFilter(relEntity, this.scopedWhereMap(relMeta, targetCondition), opts);
|
|
155
152
|
lookups.temps.push(temp);
|
|
156
|
-
lookups.stages.push(
|
|
153
|
+
lookups.stages.push(this.relationLookup(meta, relOpts, relMeta, relEntity, targetScope, temp, tail, opts));
|
|
154
|
+
return sizeVal === undefined
|
|
155
|
+
? { [`${temp}.0`]: { $exists: true } }
|
|
156
|
+
: { $expr: this.compareRelationCount(temp, sizeVal) };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The correlated `$lookup` for one relation, as `temp`: through its junction for a ManyToMany, or
|
|
160
|
+
* straight at the target otherwise. `tail` decides what the lookup leaves behind - a row to test
|
|
161
|
+
* for existence, or a `$count` - so a filter and an ordering build the same stage.
|
|
162
|
+
*/
|
|
163
|
+
relationLookup(meta, relOpts, relMeta, relEntity, targetScope, temp, tail, opts) {
|
|
164
|
+
return relOpts.cardinality === 'mm' && relOpts.through
|
|
157
165
|
? this.junctionLookup(relOpts, relMeta, relEntity, targetScope, temp, tail, opts)
|
|
158
166
|
: {
|
|
159
167
|
$lookup: {
|
|
@@ -162,10 +170,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
162
170
|
pipeline: [...(hasKeys(targetScope) ? [{ $match: targetScope }] : []), ...tail],
|
|
163
171
|
as: temp,
|
|
164
172
|
},
|
|
165
|
-
}
|
|
166
|
-
return sizeVal === undefined
|
|
167
|
-
? { [`${temp}.0`]: { $exists: true } }
|
|
168
|
-
: { $expr: this.compareRelationCount(temp, sizeVal) };
|
|
173
|
+
};
|
|
169
174
|
}
|
|
170
175
|
/**
|
|
171
176
|
* ManyToMany counts/tests junction rows, so the target is reached from inside the junction's own
|
|
@@ -175,7 +180,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
175
180
|
const throughEntity = relOpts.through();
|
|
176
181
|
const throughMeta = getMeta(throughEntity);
|
|
177
182
|
const junctionScope = this.renderFilter(throughEntity, this.scopedWhereMap(throughMeta, {}), opts);
|
|
178
|
-
const nested =
|
|
183
|
+
const nested = REL_NESTED_KEY;
|
|
179
184
|
return {
|
|
180
185
|
$lookup: {
|
|
181
186
|
from: this.resolveTableName(throughMeta),
|
|
@@ -204,7 +209,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
204
209
|
* the `$ifNull` fallback to 0, so `{ $size: 0 }` matches parents with no related row at all.
|
|
205
210
|
*/
|
|
206
211
|
compareRelationCount(temp, sizeVal) {
|
|
207
|
-
const count = { $ifNull: [{ $arrayElemAt: [`$${temp}.${
|
|
212
|
+
const count = { $ifNull: [{ $arrayElemAt: [`$${temp}.${COUNT_ALIAS}`, 0] }, 0] };
|
|
208
213
|
if (typeof sizeVal === 'number') {
|
|
209
214
|
return { $eq: [count, sizeVal] };
|
|
210
215
|
}
|
|
@@ -397,10 +402,47 @@ export class MongoDialect extends AbstractDialect {
|
|
|
397
402
|
// one: ordering by a relation nothing looked up reads a field that is not there, which MongoDB
|
|
398
403
|
// ranks as all-equal rather than rejecting. The SQL dialects can add the join themselves.
|
|
399
404
|
const relPath = `${path}${key}`;
|
|
405
|
+
const countDirection = parseSortByCount(value);
|
|
406
|
+
if (countDirection !== undefined) {
|
|
407
|
+
// The tally rides on a field {@link sortCountStages} adds, which only the queried entity's
|
|
408
|
+
// own pipeline has: a nested one is built inside its parent's `$lookup`, where there is no
|
|
409
|
+
// parent document left to hang it off.
|
|
410
|
+
if (path) {
|
|
411
|
+
throw new TypeError(`$sort by '${relPath}.$count' is only supported on the queried entity`);
|
|
412
|
+
}
|
|
413
|
+
out[sortCountField(key)] = sortDirection(countDirection);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
400
416
|
const { join, sort: relationSort } = resolveSortableJoin(relation, relPath, value, joins, `cannot $sort by relation '${relPath}' on MongoDB unless it is populated: only $populate adds its fields to the document`);
|
|
401
417
|
this.collectSort(join.meta, relationSort, joins, `${relPath}.`, out);
|
|
402
418
|
}
|
|
403
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* The stages a `$sort` by a relation's size needs: one correlated `$lookup` tallying the relation
|
|
422
|
+
* per parent, and the `$set` that lifts the tally onto the document as the field the `$sort` then
|
|
423
|
+
* orders by. A parent with no related row gets no lookup result at all, which is a zero.
|
|
424
|
+
*/
|
|
425
|
+
sortCountStages(entity, sort, opts) {
|
|
426
|
+
const meta = getMeta(entity);
|
|
427
|
+
const stages = [];
|
|
428
|
+
const fields = [];
|
|
429
|
+
for (const [key, value] of Object.entries(sort ?? {})) {
|
|
430
|
+
const relOpts = meta.relations[key];
|
|
431
|
+
if (!relOpts || parseSortByCount(value) === undefined) {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const relEntity = relOpts.entity();
|
|
435
|
+
const relMeta = getMeta(relEntity);
|
|
436
|
+
const temp = sortCountField(key);
|
|
437
|
+
const targetScope = this.renderFilter(relEntity, this.scopedWhereMap(relMeta, {}), opts);
|
|
438
|
+
const tail = [{ $count: COUNT_ALIAS }];
|
|
439
|
+
stages.push(this.relationLookup(meta, relOpts, relMeta, relEntity, targetScope, temp, tail, opts), {
|
|
440
|
+
$addFields: { [temp]: { $ifNull: [{ $arrayElemAt: [`$${temp}.${COUNT_ALIAS}`, 0] }, 0] } },
|
|
441
|
+
});
|
|
442
|
+
fields.push(temp);
|
|
443
|
+
}
|
|
444
|
+
return { stages, fields };
|
|
445
|
+
}
|
|
404
446
|
/** Whether a `$sort` reads a relation, which is what forces the lookups to run before it. */
|
|
405
447
|
sortsRelations(entity, sort) {
|
|
406
448
|
if (!sort) {
|
|
@@ -459,7 +501,10 @@ export class MongoDialect extends AbstractDialect {
|
|
|
459
501
|
readStages(entity, q, opts, extra = {}) {
|
|
460
502
|
const meta = getMeta(entity);
|
|
461
503
|
const joins = resolveQueryJoins(meta, q);
|
|
462
|
-
|
|
504
|
+
// The tally an ordering by a relation's size reads, and the field it parks it on: both belong
|
|
505
|
+
// with the lookups, since the `$sort` right after them is what they exist for.
|
|
506
|
+
const counted = this.sortCountStages(entity, q.$sort, opts);
|
|
507
|
+
const lookups = [...this.lookupStages(meta, joins, undefined, opts), ...counted.stages];
|
|
463
508
|
const sort = hasKeys(extra.sort) ? [{ $sort: extra.sort }] : [];
|
|
464
509
|
const pager = extra.pager ?? [];
|
|
465
510
|
// Merged into the query's own projection rather than standing in for one: a query that asked
|
|
@@ -471,10 +516,14 @@ export class MongoDialect extends AbstractDialect {
|
|
|
471
516
|
// which is the one way this differs from a SQL join. Taken back out once the `$sort` that needed
|
|
472
517
|
// it has run, so ordering by an unpopulated relation costs the same nothing it does there.
|
|
473
518
|
const sortOnly = [...joins.values()].filter((join) => !join.projected).map((join) => join.path);
|
|
474
|
-
const
|
|
519
|
+
const dropped = [...sortOnly, ...counted.fields];
|
|
520
|
+
const unset = dropped.length ? [{ $unset: dropped }] : [];
|
|
475
521
|
// The grouping collapses rows onto the columns it projects, which leaves nothing for an ordering
|
|
476
522
|
// that reads a lookup those columns do not carry. Refused rather than answered all-equal, and in
|
|
477
523
|
// the same terms the SQL dialects refuse `SELECT DISTINCT` ordered by an unselected column.
|
|
524
|
+
if (q.$distinct && counted.fields.length) {
|
|
525
|
+
throw new TypeError(`cannot $sort by a relation's $count with $distinct: the grouping keeps only the columns it projects`);
|
|
526
|
+
}
|
|
478
527
|
if (q.$distinct && sortOnly.length) {
|
|
479
528
|
throw new TypeError(`cannot $sort by relation '${sortOnly[0]}' with $distinct unless '${sortOnly[0]}' is populated: the grouping keeps only the columns it projects`);
|
|
480
529
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Document, MongoClient } from 'mongodb';
|
|
2
2
|
import { AbstractQuerier } from '../querier/index.js';
|
|
3
|
-
import type { EntityData, ExtraOptions, IdValue, PrimaryKey, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryGroupMap, QueryOptions, QuerySearch, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
3
|
+
import type { EntityData, ExtraOptions, IdValue, PrimaryKey, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
4
4
|
import type { MongoDialect } from './mongoDialect.js';
|
|
5
5
|
export declare class MongodbQuerier extends AbstractQuerier {
|
|
6
6
|
readonly dialect: MongoDialect;
|
|
@@ -22,7 +22,18 @@ export declare class MongodbQuerier extends AbstractQuerier {
|
|
|
22
22
|
*/
|
|
23
23
|
private buildVectorPipeline;
|
|
24
24
|
protected internalAggregate<E extends Document, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A `$required` relation drops parents that have no match, and `$distinct` collapses them, so both
|
|
27
|
+
* totals have to be taken after the stage that does it - which only the read pipeline builds. Every
|
|
28
|
+
* other query counts through {@link internalCount}, which needs no pipeline of its own.
|
|
29
|
+
*/
|
|
30
|
+
protected internalFindManyAndCount<E extends Document>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<[E[], number]>;
|
|
31
|
+
protected internalCount<E extends Document>(entity: Type<E>, qm?: QueryFilter<E>, opts?: QueryOptions): Promise<number>;
|
|
32
|
+
/**
|
|
33
|
+
* The collection's metadata count, which the driver exposes as its own call and which takes no
|
|
34
|
+
* filter - the reason {@link UniversalQuerier.estimatedCount} takes none either.
|
|
35
|
+
*/
|
|
36
|
+
estimatedCount<E extends Document>(entity: Type<E>): Promise<number>;
|
|
26
37
|
/**
|
|
27
38
|
* The ids matching `q`, in `q`'s own order and page, so a write can name the rows it settled on.
|
|
28
39
|
* Built from the read pipeline rather than stages assembled here: that dropped `$sort`/`$limit` on
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
2
|
+
import { hasRequiredJoin } from '../dialect/queryJoins.js';
|
|
1
3
|
import { getMeta } from '../entity/index.js';
|
|
2
4
|
import { AbstractQuerier, enrichError } from '../querier/index.js';
|
|
3
5
|
import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, isPagedQuery, populatesRelations, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* `$limit: 0` asks for no rows, the way it does on every SQL dialect - but MongoDB reads `limit(0)`
|
|
8
|
+
* as *unlimited*, so a read that passed it straight to the driver came back with the whole
|
|
9
|
+
* collection. The reads answer it here instead, since no cursor can express it.
|
|
10
|
+
*/
|
|
11
|
+
function asksForNoRows(q) {
|
|
12
|
+
return q.$limit === 0;
|
|
13
|
+
}
|
|
4
14
|
export class MongodbQuerier extends AbstractQuerier {
|
|
5
15
|
dialect;
|
|
6
16
|
conn;
|
|
@@ -19,6 +29,9 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
19
29
|
}
|
|
20
30
|
async internalFindMany(entity, q, opts) {
|
|
21
31
|
this.dialect.assertNoLock(q);
|
|
32
|
+
if (asksForNoRows(q)) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
22
35
|
return this.timed('internalFindMany', undefined, async () => {
|
|
23
36
|
const meta = getMeta(entity);
|
|
24
37
|
const vectorSort = this.dialect.extractVectorSort(q.$sort);
|
|
@@ -51,6 +64,9 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
51
64
|
});
|
|
52
65
|
}
|
|
53
66
|
async *internalFindManyStream(entity, q, opts) {
|
|
67
|
+
if (asksForNoRows(q)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
54
70
|
const meta = getMeta(entity);
|
|
55
71
|
const { joinableKeys, toManyKeys } = getRelationRequestSummary(meta, q.$populate);
|
|
56
72
|
if (joinableKeys.length || toManyKeys.length) {
|
|
@@ -99,6 +115,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
99
115
|
if (q.$skip) {
|
|
100
116
|
cursor.skip(q.$skip);
|
|
101
117
|
}
|
|
118
|
+
// Only a positive limit reaches the driver; `asksForNoRows` took the zero.
|
|
102
119
|
if (q.$limit) {
|
|
103
120
|
cursor.limit(q.$limit);
|
|
104
121
|
}
|
|
@@ -134,14 +151,34 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
134
151
|
return this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
|
|
135
152
|
});
|
|
136
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* A `$required` relation drops parents that have no match, and `$distinct` collapses them, so both
|
|
156
|
+
* totals have to be taken after the stage that does it - which only the read pipeline builds. Every
|
|
157
|
+
* other query counts through {@link internalCount}, which needs no pipeline of its own.
|
|
158
|
+
*/
|
|
159
|
+
async internalFindManyAndCount(entity, q, opts) {
|
|
160
|
+
if (!q.$distinct && !hasRequiredJoin(getMeta(entity), q)) {
|
|
161
|
+
return super.internalFindManyAndCount(entity, q, opts);
|
|
162
|
+
}
|
|
163
|
+
const { $sort: _sort, $skip: _skip, $limit: _limit, ...unpaged } = q;
|
|
164
|
+
const [founds, counted] = await Promise.all([
|
|
165
|
+
this.internalFindMany(entity, q, opts),
|
|
166
|
+
this.execute((session) => this.collection(entity)
|
|
167
|
+
.aggregate([...this.dialect.aggregationPipeline(entity, unpaged, opts), { $count: COUNT_ALIAS }], { session })
|
|
168
|
+
.toArray()),
|
|
169
|
+
]);
|
|
170
|
+
return [founds, counted[0]?.[COUNT_ALIAS] ?? 0];
|
|
171
|
+
}
|
|
137
172
|
async internalCount(entity, qm = {}, opts) {
|
|
138
173
|
return this.timed('internalCount', undefined, async () => {
|
|
139
174
|
if (this.dialect.constrainsRelations(entity, qm.$where)) {
|
|
140
175
|
const { stages, filter } = this.dialect.whereWithRelations(entity, qm.$where, opts);
|
|
141
176
|
const [counted] = await this.execute((session) => this.collection(entity)
|
|
142
|
-
.aggregate([...stages, { $match: filter }, { $count:
|
|
177
|
+
.aggregate([...stages, { $match: filter }, { $count: COUNT_ALIAS }], {
|
|
178
|
+
session,
|
|
179
|
+
})
|
|
143
180
|
.toArray());
|
|
144
|
-
return counted?.
|
|
181
|
+
return counted?.[COUNT_ALIAS] ?? 0;
|
|
145
182
|
}
|
|
146
183
|
const filter = this.dialect.where(entity, qm.$where, opts);
|
|
147
184
|
return this.execute((session) => this.collection(entity).countDocuments(filter, {
|
|
@@ -149,6 +186,13 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
149
186
|
}));
|
|
150
187
|
});
|
|
151
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* The collection's metadata count, which the driver exposes as its own call and which takes no
|
|
191
|
+
* filter - the reason {@link UniversalQuerier.estimatedCount} takes none either.
|
|
192
|
+
*/
|
|
193
|
+
async estimatedCount(entity) {
|
|
194
|
+
return this.timed('estimatedCount', undefined, async () => this.execute((session) => this.collection(entity).estimatedDocumentCount({ session })));
|
|
195
|
+
}
|
|
152
196
|
/**
|
|
153
197
|
* The ids matching `q`, in `q`'s own order and page, so a write can name the rows it settled on.
|
|
154
198
|
* Built from the read pipeline rather than stages assembled here: that dropped `$sort`/`$limit` on
|
|
@@ -11,4 +11,11 @@ export declare class PostgresDialect extends PgLikeSqlDialect {
|
|
|
11
11
|
readonly dialectName: SqlDialectName;
|
|
12
12
|
readonly vectorExtension: string | undefined;
|
|
13
13
|
upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[]): void;
|
|
14
|
+
/**
|
|
15
|
+
* `to_regclass` rather than a `::regclass` cast: it answers `NULL` for a table that does not exist
|
|
16
|
+
* (mid-migration, say) where the cast throws. `GREATEST` because a table nothing has analyzed yet
|
|
17
|
+
* carries `reltuples = -1`, Postgres' "no statistic" (not `NULL`, and not `0`) since PG 14 - handed
|
|
18
|
+
* back raw it would read as a negative row count.
|
|
19
|
+
*/
|
|
20
|
+
estimatedCount<E>(ctx: QueryContext, entity: Type<E>): void;
|
|
14
21
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
1
2
|
import { PgLikeSqlDialect } from '../dialect/pgLikeSqlDialect.js';
|
|
3
|
+
import { getMeta } from '../entity/index.js';
|
|
2
4
|
/**
|
|
3
5
|
* PostgreSQL dialect. For node-pg use PgDialect. Neon, Bun SQL, and Cockroach use driver-specific
|
|
4
6
|
* subclasses. Shared Postgres-wire AST/quoting/JSONB/full-text-search/vector-search logic
|
|
@@ -13,4 +15,15 @@ export class PostgresDialect extends PgLikeSqlDialect {
|
|
|
13
15
|
// The xmax system column is 0 for a newly inserted row and non-zero for an updated one (MVCC).
|
|
14
16
|
super.upsert(ctx, entity, conflictPaths, payload, `, (xmax = 0) AS ${this.escapeId('_created')}`);
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* `to_regclass` rather than a `::regclass` cast: it answers `NULL` for a table that does not exist
|
|
20
|
+
* (mid-migration, say) where the cast throws. `GREATEST` because a table nothing has analyzed yet
|
|
21
|
+
* carries `reltuples = -1`, Postgres' "no statistic" (not `NULL`, and not `0`) since PG 14 - handed
|
|
22
|
+
* back raw it would read as a negative row count.
|
|
23
|
+
*/
|
|
24
|
+
estimatedCount(ctx, entity) {
|
|
25
|
+
ctx.append(`SELECT GREATEST(reltuples, 0)::bigint ${this.escapeId(COUNT_ALIAS, true)} FROM pg_class WHERE oid = to_regclass(`);
|
|
26
|
+
ctx.addValue(this.escapedTableName(getMeta(entity)));
|
|
27
|
+
ctx.append(')');
|
|
28
|
+
}
|
|
16
29
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EntityData, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPopulate, QueryProjected, QuerySearch, QueryUpdateResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
1
|
+
import type { EntityData, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryPopulate, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
2
2
|
import { LoggerWrapper } from '../util/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Base class for all database queriers.
|
|
@@ -30,23 +30,23 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
30
30
|
protected resolveEntityQuery<E extends object, Q extends object>(entityOrQuery: Type<E> | (Q & {
|
|
31
31
|
$entity: Type<E>;
|
|
32
32
|
}), maybeQueryOrOpts?: Q | QueryOptions, maybeOpts?: QueryOptions): [Type<E>, Q, QueryOptions | undefined];
|
|
33
|
-
findOneById<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, id: IdValue<E>, q?: QueryOneProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P> | undefined>;
|
|
33
|
+
findOneById<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, id: IdValue<E>, q?: QueryOneProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
|
|
34
34
|
/**
|
|
35
35
|
* Find a single record matching the query.
|
|
36
36
|
* Supports both entity-as-argument and entity-as-field patterns.
|
|
37
37
|
*/
|
|
38
|
-
findOne<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(q: QueryOneProjected<E, S, V, X, P> & {
|
|
38
|
+
findOne<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(q: QueryOneProjected<E, S, V, X, P, C> & {
|
|
39
39
|
$entity: Type<E>;
|
|
40
|
-
}, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P> | undefined>;
|
|
41
|
-
findOne<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryOneProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P> | undefined>;
|
|
40
|
+
}, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
|
|
41
|
+
findOne<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryOneProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
|
|
42
42
|
/**
|
|
43
43
|
* Find multiple records matching the query.
|
|
44
44
|
* Supports both entity-as-argument and entity-as-field patterns.
|
|
45
45
|
*/
|
|
46
|
-
findMany<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(q: QueryProjected<E, S, V, X, P> & {
|
|
46
|
+
findMany<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(q: QueryProjected<E, S, V, X, P, C> & {
|
|
47
47
|
$entity: Type<E>;
|
|
48
|
-
}, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P>[]>;
|
|
49
|
-
findMany<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P>[]>;
|
|
48
|
+
}, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C>[]>;
|
|
49
|
+
findMany<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C>[]>;
|
|
50
50
|
protected abstract internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
|
|
51
51
|
/**
|
|
52
52
|
* Stream records as an async iterable.
|
|
@@ -60,33 +60,49 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
60
60
|
*
|
|
61
61
|
* No `afterLoad` hooks on streamed rows.
|
|
62
62
|
*/
|
|
63
|
-
findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(q:
|
|
63
|
+
findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(q: QueryStreamProjected<E, S, V, X, P> & {
|
|
64
64
|
$entity: Type<E>;
|
|
65
65
|
}, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
|
|
66
|
-
findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q:
|
|
66
|
+
findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryStreamProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
|
|
67
67
|
protected abstract internalFindManyStream<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): AsyncIterable<E>;
|
|
68
68
|
/**
|
|
69
69
|
* Find multiple records and return both the records and total count.
|
|
70
70
|
* Supports both entity-as-argument and entity-as-field patterns.
|
|
71
71
|
*/
|
|
72
|
-
findManyAndCount<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(q: QueryProjected<E, S, V, X, P> & {
|
|
72
|
+
findManyAndCount<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(q: QueryProjected<E, S, V, X, P, C> & {
|
|
73
73
|
$entity: Type<E>;
|
|
74
|
-
}, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P>[], number]>;
|
|
75
|
-
findManyAndCount<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P>[], number]>;
|
|
74
|
+
}, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P, C>[], number]>;
|
|
75
|
+
findManyAndCount<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P, C>[], number]>;
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
77
|
+
* The page and how many rows the filter matched beyond it. Two statements here, which is what a
|
|
78
|
+
* backend with no way to answer both at once is left with; SQL overrides it to answer from one.
|
|
79
|
+
*/
|
|
80
|
+
protected internalFindManyAndCount<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<[E[], number]>;
|
|
81
|
+
/**
|
|
82
|
+
* Count records matching the query, in both the entity-as-argument and entity-as-field patterns.
|
|
83
|
+
* A `$skip`/`$limit` settles the matching ids and counts those, rather than scanning every match;
|
|
84
|
+
* `$sort` never reaches that SELECT, since it changes which rows a page holds, never how many.
|
|
79
85
|
*/
|
|
80
|
-
count<E extends object>(entity: Type<E>, q?:
|
|
81
|
-
count<E extends object>(q:
|
|
86
|
+
count<E extends object>(entity: Type<E>, q?: QueryPage<E>, opts?: QueryOptions): Promise<number>;
|
|
87
|
+
count<E extends object>(q: QueryPage<E> & {
|
|
82
88
|
$entity: Type<E>;
|
|
83
89
|
}, opts?: QueryOptions): Promise<number>;
|
|
84
|
-
protected abstract internalCount<E extends object>(entity: Type<E>, q:
|
|
90
|
+
protected abstract internalCount<E extends object>(entity: Type<E>, q: QueryFilter<E>, opts?: QueryOptions): Promise<number>;
|
|
91
|
+
/**
|
|
92
|
+
* Whether anything matches, in both the entity-as-argument and entity-as-field patterns. A count
|
|
93
|
+
* capped at one row, so the engine stops at the first match instead of scanning every other one.
|
|
94
|
+
*/
|
|
95
|
+
exists<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<boolean>;
|
|
96
|
+
exists<E extends object>(q: QueryFilter<E> & {
|
|
97
|
+
$entity: Type<E>;
|
|
98
|
+
}, opts?: QueryOptions): Promise<boolean>;
|
|
85
99
|
/**
|
|
86
100
|
* Run an aggregate query.
|
|
87
101
|
*/
|
|
88
102
|
aggregate<E extends object, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
|
|
89
103
|
protected abstract internalAggregate<E extends object, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
|
|
104
|
+
/** Abstract outright: nothing is shared to do around it. See {@link UniversalQuerier.estimatedCount}. */
|
|
105
|
+
abstract estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
|
|
90
106
|
insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
|
|
91
107
|
insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
92
108
|
protected abstract internalInsertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getMeta } from '../entity/index.js';
|
|
2
|
-
import { asSelectMap, augmentWhere, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, LoggerWrapper, parseRelationAtKey, parseRelationQueryValue, runHooks, someKey, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
2
|
+
import { asSelectMap, augmentWhere, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, LoggerWrapper, parentKeyColumn, parseRelationAtKey, parseRelationQueryValue, runHooks, someKey, targetKeyColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
3
|
import { enrichError } from './queryError.js';
|
|
4
|
+
import { fillRelationCounts, withIdForCounts } from './relationCount.js';
|
|
4
5
|
/**
|
|
5
6
|
* Rejects a nullish primary key before it reaches a statement. The by-id methods reduce to
|
|
6
7
|
* `{ $where: id }`, and a nullish `$where` is *no filter*, so an unchecked one addresses the whole
|
|
@@ -92,7 +93,8 @@ export class AbstractQuerier {
|
|
|
92
93
|
async findMany(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
|
|
93
94
|
const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
|
|
94
95
|
this.validateProjectionQuery(entity, q);
|
|
95
|
-
const founds = await this.internalFindMany(entity, q, opts);
|
|
96
|
+
const founds = await this.internalFindMany(entity, withIdForCounts(entity, q), opts);
|
|
97
|
+
await fillRelationCounts(this, entity, founds, q.$count);
|
|
96
98
|
await this.emitHook(entity, 'afterLoad', founds);
|
|
97
99
|
return founds;
|
|
98
100
|
}
|
|
@@ -104,18 +106,34 @@ export class AbstractQuerier {
|
|
|
104
106
|
async findManyAndCount(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
|
|
105
107
|
const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
|
|
106
108
|
this.validateProjectionQuery(entity, q);
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
this.internalFindMany(entity, q, opts),
|
|
110
|
-
this.internalCount(entity, qCount, opts),
|
|
111
|
-
]);
|
|
109
|
+
const [founds, count] = await this.internalFindManyAndCount(entity, withIdForCounts(entity, q), opts);
|
|
110
|
+
await fillRelationCounts(this, entity, founds, q.$count);
|
|
112
111
|
await this.emitHook(entity, 'afterLoad', founds);
|
|
113
112
|
return [founds, count];
|
|
114
113
|
}
|
|
115
|
-
|
|
114
|
+
/**
|
|
115
|
+
* The page and how many rows the filter matched beyond it. Two statements here, which is what a
|
|
116
|
+
* backend with no way to answer both at once is left with; SQL overrides it to answer from one.
|
|
117
|
+
*/
|
|
118
|
+
async internalFindManyAndCount(entity, q, opts) {
|
|
119
|
+
return Promise.all([
|
|
120
|
+
this.internalFindMany(entity, q, opts),
|
|
121
|
+
this.internalCount(entity, { $where: q.$where }, opts),
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
async count(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
|
|
116
125
|
const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
|
|
126
|
+
if (q.$skip !== undefined || q.$limit !== undefined) {
|
|
127
|
+
const meta = getMeta(entity);
|
|
128
|
+
const rows = await this.internalFindMany(entity, idOnlyQuery(meta, { $where: q.$where, $skip: q.$skip, $limit: q.$limit }), opts);
|
|
129
|
+
return rows.length;
|
|
130
|
+
}
|
|
117
131
|
return this.internalCount(entity, q, opts);
|
|
118
132
|
}
|
|
133
|
+
async exists(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
|
|
134
|
+
const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
|
|
135
|
+
return (await this.count(entity, { $where: q.$where, $limit: 1 }, opts)) > 0;
|
|
136
|
+
}
|
|
119
137
|
/**
|
|
120
138
|
* Run an aggregate query.
|
|
121
139
|
*/
|
|
@@ -200,13 +218,12 @@ export class AbstractQuerier {
|
|
|
200
218
|
const toInsert = [];
|
|
201
219
|
const toUpdate = [];
|
|
202
220
|
const existingIds = [];
|
|
203
|
-
const idKey = (meta.id ?? 'id');
|
|
204
221
|
for (const it of payload) {
|
|
205
|
-
const id = it[
|
|
222
|
+
const id = it[meta.id];
|
|
206
223
|
if (!id) {
|
|
207
224
|
toInsert.push(it);
|
|
208
225
|
}
|
|
209
|
-
else if (!someKey(it, (key) => key !==
|
|
226
|
+
else if (!someKey(it, (key) => key !== meta.id)) {
|
|
210
227
|
existingIds.push(id);
|
|
211
228
|
}
|
|
212
229
|
else {
|
|
@@ -216,9 +233,9 @@ export class AbstractQuerier {
|
|
|
216
233
|
const [insertedIds, updatedIds] = await Promise.all([
|
|
217
234
|
toInsert.length ? this.insertMany(entity, toInsert) : [],
|
|
218
235
|
Promise.all(toUpdate.map(async (it) => {
|
|
219
|
-
const id = it[
|
|
236
|
+
const id = it[meta.id];
|
|
220
237
|
const data = { ...it };
|
|
221
|
-
delete data[
|
|
238
|
+
delete data[meta.id];
|
|
222
239
|
await this.updateOneById(entity, id, data);
|
|
223
240
|
return id;
|
|
224
241
|
})),
|
|
@@ -246,10 +263,10 @@ export class AbstractQuerier {
|
|
|
246
263
|
}
|
|
247
264
|
}
|
|
248
265
|
async fillToManyThroughRelation(payload, meta, relKey, relOpts, relationQuery) {
|
|
249
|
-
const localField = relOpts
|
|
266
|
+
const localField = parentKeyColumn(relOpts);
|
|
250
267
|
const throughEntity = relOpts.through();
|
|
251
268
|
const throughMeta = getMeta(throughEntity);
|
|
252
|
-
const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === relOpts
|
|
269
|
+
const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === targetKeyColumn(relOpts)));
|
|
253
270
|
const ids = payload.map((it) => it[meta.id]);
|
|
254
271
|
// A relation query names the target's columns, not the join table's, so the projection and the
|
|
255
272
|
// filter belong on the populate below - resolved there against the entity that has them. Spread
|
|
@@ -278,7 +295,7 @@ export class AbstractQuerier {
|
|
|
278
295
|
this.putChildrenInParents(payload, founds, meta.id, localField, relKey);
|
|
279
296
|
}
|
|
280
297
|
async fillToManyOneToMany(payload, meta, relKey, relOpts, relationQuery, relEntity) {
|
|
281
|
-
const foreignField = relOpts
|
|
298
|
+
const foreignField = parentKeyColumn(relOpts);
|
|
282
299
|
// The FK is what putChildrenInParents groups on, so it outlives the relation's projection
|
|
283
300
|
// either way: added to a whitelisting `$select` (the raw-array form has nothing to augment),
|
|
284
301
|
// dropped from a subtractive `$exclude`. `relationQuery` is already a clone.
|
|
@@ -343,15 +360,9 @@ export class AbstractQuerier {
|
|
|
343
360
|
if (!relOpts)
|
|
344
361
|
continue;
|
|
345
362
|
const relEntity = relOpts.entity();
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
await this.deleteMany(throughEntity, { $where: { [localField]: ids } }, opts);
|
|
350
|
-
}
|
|
351
|
-
else {
|
|
352
|
-
const foreignField = relOpts.references[0].foreign;
|
|
353
|
-
await this.deleteMany(relEntity, { $where: { [foreignField]: ids } }, opts);
|
|
354
|
-
}
|
|
363
|
+
const parentKey = parentKeyColumn(relOpts);
|
|
364
|
+
const target = relOpts.through ? relOpts.through() : relEntity;
|
|
365
|
+
await this.deleteMany(target, { $where: { [parentKey]: ids } }, opts);
|
|
355
366
|
}
|
|
356
367
|
}
|
|
357
368
|
/**
|
|
@@ -378,9 +389,9 @@ export class AbstractQuerier {
|
|
|
378
389
|
}
|
|
379
390
|
}
|
|
380
391
|
async saveToMany(relOpts, relEntity, ids, relPayload, isUpdate) {
|
|
381
|
-
const {
|
|
392
|
+
const { through } = relOpts;
|
|
382
393
|
if (through) {
|
|
383
|
-
const localField =
|
|
394
|
+
const localField = parentKeyColumn(relOpts);
|
|
384
395
|
const throughEntity = through();
|
|
385
396
|
if (isUpdate) {
|
|
386
397
|
await this.deleteMany(throughEntity, { $where: { [localField]: ids } });
|
|
@@ -390,12 +401,12 @@ export class AbstractQuerier {
|
|
|
390
401
|
// would link every parent to a single shared row instead.
|
|
391
402
|
for (const id of ids) {
|
|
392
403
|
const savedIds = await this.saveMany(relEntity, relPayload);
|
|
393
|
-
await this.insertMany(throughEntity, savedIds.map((relId) => ({ [
|
|
404
|
+
await this.insertMany(throughEntity, savedIds.map((relId) => ({ [localField]: id, [targetKeyColumn(relOpts)]: relId })));
|
|
394
405
|
}
|
|
395
406
|
}
|
|
396
407
|
return;
|
|
397
408
|
}
|
|
398
|
-
const foreignField =
|
|
409
|
+
const foreignField = parentKeyColumn(relOpts);
|
|
399
410
|
if (isUpdate) {
|
|
400
411
|
await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
|
|
401
412
|
}
|
|
@@ -404,7 +415,7 @@ export class AbstractQuerier {
|
|
|
404
415
|
}
|
|
405
416
|
}
|
|
406
417
|
async saveOneToOne(relEntity, relOpts, ids, relPayload) {
|
|
407
|
-
const foreignField = relOpts
|
|
418
|
+
const foreignField = parentKeyColumn(relOpts);
|
|
408
419
|
if (relPayload === null) {
|
|
409
420
|
await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
|
|
410
421
|
return;
|
|
@@ -412,6 +423,8 @@ export class AbstractQuerier {
|
|
|
412
423
|
await this.saveMany(relEntity, ids.map((id) => ({ ...relPayload, [foreignField]: id })));
|
|
413
424
|
}
|
|
414
425
|
async saveManyToOne(entity, relEntity, relOpts, ids, relPayload) {
|
|
426
|
+
// Not `parentKeyColumn`: a many-to-one points the other way, so this is the *parent's* own column
|
|
427
|
+
// holding the child's id - the one place `references[0].local` does not name a key of the parent.
|
|
415
428
|
const localField = relOpts.references[0].local;
|
|
416
429
|
// Per parent: each gets its own reference row, so each `SET` carries a different value.
|
|
417
430
|
for (const id of ids) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AbstractDialect } from '../dialect/index.js';
|
|
2
|
-
import type { EntityData, ExtraOptions, FieldKey, IdValue, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryProjected, QuerySearch, QueryUpdateResult, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
2
|
+
import type { EntityData, ExtraOptions, FieldKey, IdValue, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Base pool: dialect id and behavior come only from the `dialect` instance (see {@link QuerierPool}).
|
|
5
5
|
*/
|
|
@@ -24,17 +24,19 @@ export declare abstract class AbstractQuerierPool<Q extends Querier, D extends A
|
|
|
24
24
|
withQuerier<T>(callback: (querier: Q) => Promise<T>, opts?: PoolRunOptions): Promise<T>;
|
|
25
25
|
/** Run `fn` under `context` (an enclosing {@link withContext}) when provided, else run it as-is. */
|
|
26
26
|
private runScoped;
|
|
27
|
-
findOneById<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, id: IdValue<E>, q?: QueryOneProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P> | undefined>;
|
|
28
|
-
findOne<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryOneProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P> | undefined>;
|
|
29
|
-
findMany<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P>[]>;
|
|
27
|
+
findOneById<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, id: IdValue<E>, q?: QueryOneProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
|
|
28
|
+
findOne<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryOneProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
|
|
29
|
+
findMany<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C>[]>;
|
|
30
30
|
/**
|
|
31
31
|
* The connection outlives the call here: it is held until the iterator is drained or closed by a
|
|
32
32
|
* `break`/`throw`. Abandoning the iterator instead leaks it until GC, so consume it in a `for await`.
|
|
33
33
|
*/
|
|
34
|
-
findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q:
|
|
35
|
-
findManyAndCount<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P>[], number]>;
|
|
36
|
-
count<E extends object>(entity: Type<E>, q?:
|
|
34
|
+
findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryStreamProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncGenerator<QueryFindResult<E, S, V, X, P>>;
|
|
35
|
+
findManyAndCount<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P, C>[], number]>;
|
|
36
|
+
count<E extends object>(entity: Type<E>, q?: QueryPage<E>, opts?: QueryOptions): Promise<number>;
|
|
37
|
+
exists<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<boolean>;
|
|
37
38
|
aggregate<E extends object, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
|
|
39
|
+
estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
|
|
38
40
|
insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
|
|
39
41
|
insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
40
42
|
updateOneById<E extends object>(entity: Type<E>, id: IdValue<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|