uql-orm 0.36.1 → 0.37.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/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 +18 -2
- package/dist/dialect/abstractSqlDialect.js +45 -12
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +6 -0
- package/dist/dialect/mysqlLikeSqlDialect.js +19 -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 +21 -1
- package/dist/mongo/mongoDialect.js +70 -11
- package/dist/mongo/mongodbQuerier.d.ts +13 -2
- package/dist/mongo/mongodbQuerier.js +44 -3
- 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 +39 -25
- package/dist/querier/abstractQuerierPool.d.ts +9 -7
- package/dist/querier/abstractQuerierPool.js +6 -0
- package/dist/querier/abstractSqlQuerier.d.ts +20 -2
- package/dist/querier/abstractSqlQuerier.js +44 -8
- package/dist/querier/relationCount.d.ts +16 -0
- package/dist/querier/relationCount.js +111 -0
- package/dist/sqlite/sqliteDialect.d.ts +6 -1
- package/dist/sqlite/sqliteDialect.js +10 -0
- package/dist/type/dialect.d.ts +4 -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 +16 -0
- package/dist/util/dialect.util.js +25 -0
- package/dist/util/relationQuery.util.d.ts +9 -0
- package/dist/util/relationQuery.util.js +13 -0
- package/package.json +4 -4
|
@@ -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
|
*/
|
|
@@ -246,10 +264,10 @@ export class AbstractQuerier {
|
|
|
246
264
|
}
|
|
247
265
|
}
|
|
248
266
|
async fillToManyThroughRelation(payload, meta, relKey, relOpts, relationQuery) {
|
|
249
|
-
const localField = relOpts
|
|
267
|
+
const localField = parentKeyColumn(relOpts);
|
|
250
268
|
const throughEntity = relOpts.through();
|
|
251
269
|
const throughMeta = getMeta(throughEntity);
|
|
252
|
-
const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === relOpts
|
|
270
|
+
const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === targetKeyColumn(relOpts)));
|
|
253
271
|
const ids = payload.map((it) => it[meta.id]);
|
|
254
272
|
// A relation query names the target's columns, not the join table's, so the projection and the
|
|
255
273
|
// filter belong on the populate below - resolved there against the entity that has them. Spread
|
|
@@ -278,7 +296,7 @@ export class AbstractQuerier {
|
|
|
278
296
|
this.putChildrenInParents(payload, founds, meta.id, localField, relKey);
|
|
279
297
|
}
|
|
280
298
|
async fillToManyOneToMany(payload, meta, relKey, relOpts, relationQuery, relEntity) {
|
|
281
|
-
const foreignField = relOpts
|
|
299
|
+
const foreignField = parentKeyColumn(relOpts);
|
|
282
300
|
// The FK is what putChildrenInParents groups on, so it outlives the relation's projection
|
|
283
301
|
// either way: added to a whitelisting `$select` (the raw-array form has nothing to augment),
|
|
284
302
|
// dropped from a subtractive `$exclude`. `relationQuery` is already a clone.
|
|
@@ -343,15 +361,9 @@ export class AbstractQuerier {
|
|
|
343
361
|
if (!relOpts)
|
|
344
362
|
continue;
|
|
345
363
|
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
|
-
}
|
|
364
|
+
const parentKey = parentKeyColumn(relOpts);
|
|
365
|
+
const target = relOpts.through ? relOpts.through() : relEntity;
|
|
366
|
+
await this.deleteMany(target, { $where: { [parentKey]: ids } }, opts);
|
|
355
367
|
}
|
|
356
368
|
}
|
|
357
369
|
/**
|
|
@@ -378,9 +390,9 @@ export class AbstractQuerier {
|
|
|
378
390
|
}
|
|
379
391
|
}
|
|
380
392
|
async saveToMany(relOpts, relEntity, ids, relPayload, isUpdate) {
|
|
381
|
-
const {
|
|
393
|
+
const { through } = relOpts;
|
|
382
394
|
if (through) {
|
|
383
|
-
const localField =
|
|
395
|
+
const localField = parentKeyColumn(relOpts);
|
|
384
396
|
const throughEntity = through();
|
|
385
397
|
if (isUpdate) {
|
|
386
398
|
await this.deleteMany(throughEntity, { $where: { [localField]: ids } });
|
|
@@ -390,12 +402,12 @@ export class AbstractQuerier {
|
|
|
390
402
|
// would link every parent to a single shared row instead.
|
|
391
403
|
for (const id of ids) {
|
|
392
404
|
const savedIds = await this.saveMany(relEntity, relPayload);
|
|
393
|
-
await this.insertMany(throughEntity, savedIds.map((relId) => ({ [
|
|
405
|
+
await this.insertMany(throughEntity, savedIds.map((relId) => ({ [localField]: id, [targetKeyColumn(relOpts)]: relId })));
|
|
394
406
|
}
|
|
395
407
|
}
|
|
396
408
|
return;
|
|
397
409
|
}
|
|
398
|
-
const foreignField =
|
|
410
|
+
const foreignField = parentKeyColumn(relOpts);
|
|
399
411
|
if (isUpdate) {
|
|
400
412
|
await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
|
|
401
413
|
}
|
|
@@ -404,7 +416,7 @@ export class AbstractQuerier {
|
|
|
404
416
|
}
|
|
405
417
|
}
|
|
406
418
|
async saveOneToOne(relEntity, relOpts, ids, relPayload) {
|
|
407
|
-
const foreignField = relOpts
|
|
419
|
+
const foreignField = parentKeyColumn(relOpts);
|
|
408
420
|
if (relPayload === null) {
|
|
409
421
|
await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
|
|
410
422
|
return;
|
|
@@ -412,6 +424,8 @@ export class AbstractQuerier {
|
|
|
412
424
|
await this.saveMany(relEntity, ids.map((id) => ({ ...relPayload, [foreignField]: id })));
|
|
413
425
|
}
|
|
414
426
|
async saveManyToOne(entity, relEntity, relOpts, ids, relPayload) {
|
|
427
|
+
// Not `parentKeyColumn`: a many-to-one points the other way, so this is the *parent's* own column
|
|
428
|
+
// holding the child's id - the one place `references[0].local` does not name a key of the parent.
|
|
415
429
|
const localField = relOpts.references[0].local;
|
|
416
430
|
// Per parent: each gets its own reference row, so each `SET` carries a different value.
|
|
417
431
|
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>;
|
|
@@ -121,9 +121,15 @@ export class AbstractQuerierPool {
|
|
|
121
121
|
count(entity, q, opts) {
|
|
122
122
|
return this.withQuerier((querier) => querier.count(entity, q, opts));
|
|
123
123
|
}
|
|
124
|
+
exists(entity, q, opts) {
|
|
125
|
+
return this.withQuerier((querier) => querier.exists(entity, q, opts));
|
|
126
|
+
}
|
|
124
127
|
aggregate(entity, q, opts) {
|
|
125
128
|
return this.withQuerier((querier) => querier.aggregate(entity, q, opts));
|
|
126
129
|
}
|
|
130
|
+
estimatedCount(entity) {
|
|
131
|
+
return this.withQuerier((querier) => querier.estimatedCount(entity));
|
|
132
|
+
}
|
|
127
133
|
insertOne(entity, payload) {
|
|
128
134
|
return this.withQuerier((querier) => querier.insertOne(entity, payload));
|
|
129
135
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AbstractSqlDialect } from '../dialect/index.js';
|
|
2
|
-
import type { EntityData, ExtraOptions, IdValue, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryGroupMap, QueryOptions, QuerySearch, QueryUpdateResult, SqlQuerier, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
2
|
+
import type { EntityData, ExtraOptions, IdValue, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, QueryUpdateResult, SqlQuerier, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
3
3
|
import type { BuildUpdateResultPayload } from '../util/sql.util.js';
|
|
4
4
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
5
5
|
export declare abstract class AbstractSqlQuerier extends AbstractQuerier implements SqlQuerier {
|
|
@@ -46,6 +46,17 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
46
46
|
*/
|
|
47
47
|
protected assertLockable<E>(entity: Type<E>, q: Query<E>): void;
|
|
48
48
|
protected internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
|
|
49
|
+
/**
|
|
50
|
+
* One statement for both: the page carries its own unpaged total in an extra column. An empty page
|
|
51
|
+
* has no row to carry it, which is the one case still needing a count of its own - a `$skip` past
|
|
52
|
+
* the end, or a filter nothing matched.
|
|
53
|
+
*
|
|
54
|
+
* A `$required` relation needs no special case: the window counts what the INNER JOIN left, which
|
|
55
|
+
* is exactly the total a caller of a filtered read is asking for.
|
|
56
|
+
*/
|
|
57
|
+
protected internalFindManyAndCount<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<[E[], number]>;
|
|
58
|
+
private selectRows;
|
|
59
|
+
private hydrateRows;
|
|
49
60
|
protected internalFindManyStream<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): AsyncGenerator<Awaited<E>, void, unknown>;
|
|
50
61
|
/**
|
|
51
62
|
* Internal streaming query - returns an async iterable of raw rows.
|
|
@@ -64,7 +75,14 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
64
75
|
* which cost more than the decoding it guards, on a flat read.
|
|
65
76
|
*/
|
|
66
77
|
private hydrateFields;
|
|
67
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Runs a statement whose one row carries a {@link COUNT_ALIAS} column. `Number` because `COUNT(*)` is BIGINT and
|
|
80
|
+
* a caller supplying their own `types` replaces the decoding the pools do at the wire; `?? 0` because
|
|
81
|
+
* a catalog that does not know the table answers with no row, which is nothing counted.
|
|
82
|
+
*/
|
|
83
|
+
private runCount;
|
|
84
|
+
protected internalCount<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<number>;
|
|
85
|
+
estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
|
|
68
86
|
protected 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>[]>;
|
|
69
87
|
internalInsertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
70
88
|
internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { COUNT_ALIAS } from '../dialect/abstractSqlDialect.js';
|
|
1
2
|
import { decodeColumn } from '../dialect/hydrateColumn.js';
|
|
2
3
|
import { getMeta } from '../entity/index.js';
|
|
3
4
|
import { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, getRelationRequestSummary, hasKeys, idOnlyQuery, isAutoIncrement, isPagedQuery, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
4
5
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
5
6
|
import { enrichError } from './queryError.js';
|
|
7
|
+
/**
|
|
8
|
+
* The column a paged read carries its unpaged total in. Prefixed the way the other internal aliases
|
|
9
|
+
* are, since it rides in the same row as the entity's own columns and must not shadow one.
|
|
10
|
+
*/
|
|
11
|
+
const TOTAL_ALIAS = '_uql_total';
|
|
6
12
|
export class AbstractSqlQuerier extends AbstractQuerier {
|
|
7
13
|
dialect;
|
|
8
14
|
extra;
|
|
@@ -76,11 +82,32 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
84
|
async internalFindMany(entity, q, opts) {
|
|
85
|
+
return this.hydrateRows(entity, q, await this.selectRows(entity, q, opts));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* One statement for both: the page carries its own unpaged total in an extra column. An empty page
|
|
89
|
+
* has no row to carry it, which is the one case still needing a count of its own - a `$skip` past
|
|
90
|
+
* the end, or a filter nothing matched.
|
|
91
|
+
*
|
|
92
|
+
* A `$required` relation needs no special case: the window counts what the INNER JOIN left, which
|
|
93
|
+
* is exactly the total a caller of a filtered read is asking for.
|
|
94
|
+
*/
|
|
95
|
+
async internalFindManyAndCount(entity, q, opts) {
|
|
96
|
+
const rows = await this.selectRows(entity, q, opts, TOTAL_ALIAS);
|
|
97
|
+
const total = rows.length ? Number(rows[0][TOTAL_ALIAS]) : await this.internalCount(entity, q, opts);
|
|
98
|
+
for (const row of rows) {
|
|
99
|
+
delete row[TOTAL_ALIAS];
|
|
100
|
+
}
|
|
101
|
+
return [await this.hydrateRows(entity, q, rows), total];
|
|
102
|
+
}
|
|
103
|
+
async selectRows(entity, q, opts, totalAlias) {
|
|
79
104
|
this.assertLockable(entity, q);
|
|
80
105
|
const ctx = this.dialect.createContext();
|
|
81
|
-
this.dialect.find(ctx, entity, q, opts);
|
|
82
|
-
|
|
83
|
-
|
|
106
|
+
this.dialect.find(ctx, entity, q, opts, totalAlias);
|
|
107
|
+
return this.all(ctx.sql, ctx.values);
|
|
108
|
+
}
|
|
109
|
+
async hydrateRows(entity, q, rows) {
|
|
110
|
+
const founds = unflatObjects(rows).map((row) => this.hydrateFields(entity, row));
|
|
84
111
|
await this.fillToManyRelations(entity, founds, q.$populate);
|
|
85
112
|
return founds;
|
|
86
113
|
}
|
|
@@ -166,13 +193,22 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
166
193
|
}
|
|
167
194
|
return dto;
|
|
168
195
|
}
|
|
169
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Runs a statement whose one row carries a {@link COUNT_ALIAS} column. `Number` because `COUNT(*)` is BIGINT and
|
|
198
|
+
* a caller supplying their own `types` replaces the decoding the pools do at the wire; `?? 0` because
|
|
199
|
+
* a catalog that does not know the table answers with no row, which is nothing counted.
|
|
200
|
+
*/
|
|
201
|
+
async runCount(build) {
|
|
170
202
|
const ctx = this.dialect.createContext();
|
|
171
|
-
|
|
203
|
+
build(ctx);
|
|
172
204
|
const res = await this.all(ctx.sql, ctx.values);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
205
|
+
return Number(res[0]?.[COUNT_ALIAS] ?? 0);
|
|
206
|
+
}
|
|
207
|
+
async internalCount(entity, q = {}, opts) {
|
|
208
|
+
return this.runCount((ctx) => this.dialect.count(ctx, entity, q, opts));
|
|
209
|
+
}
|
|
210
|
+
async estimatedCount(entity) {
|
|
211
|
+
return this.runCount((ctx) => this.dialect.estimatedCount(ctx, entity));
|
|
176
212
|
}
|
|
177
213
|
async internalAggregate(entity, q, opts) {
|
|
178
214
|
const ctx = this.dialect.createContext();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Querier, Query, QueryCount, Type } from '../type/index.js';
|
|
2
|
+
/** What counting asks of a querier, rather than the whole interface: two reads, both batched. */
|
|
3
|
+
type CountingQuerier = Pick<Querier, 'aggregate' | 'findMany'>;
|
|
4
|
+
/**
|
|
5
|
+
* A `$count` groups its tallies by the parent's id, so the id has to outlive the projection - the
|
|
6
|
+
* same reason populating a relation keeps it. A whitelisting `$select` gains the key and an
|
|
7
|
+
* `$exclude` loses it; the raw-array `$select` form has nothing to augment, so it is refused.
|
|
8
|
+
*/
|
|
9
|
+
export declare function withIdForCounts<E extends object>(entity: Type<E>, q: Query<E>): Query<E>;
|
|
10
|
+
/**
|
|
11
|
+
* How many rows each counted relation holds, under `_count` on every parent. One grouped aggregate
|
|
12
|
+
* per relation over every parent at once - the batching a populated to-many already gets - so the
|
|
13
|
+
* cost stays flat in the number of rows the read returned rather than one statement per row.
|
|
14
|
+
*/
|
|
15
|
+
export declare function fillRelationCounts<E>(querier: CountingQuerier, entity: Type<E>, payload: E[], count?: QueryCount<E>): Promise<void>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { getMeta } from '../entity/index.js';
|
|
2
|
+
import { COUNT_RESULT_KEY } from '../type/index.js';
|
|
3
|
+
import { asSelectMap, COUNT_AGG_ALIAS, getKeys, parentKeyColumn, targetKeyColumn } from '../util/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* The one place the counting path crosses from the caller's type to a structural one: a relation's
|
|
6
|
+
* entity comes from metadata, which names its columns only as strings. Everything past this is
|
|
7
|
+
* checked against {@link CountedRow} normally.
|
|
8
|
+
*/
|
|
9
|
+
function countable(entity) {
|
|
10
|
+
return entity;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A `$count` groups its tallies by the parent's id, so the id has to outlive the projection - the
|
|
14
|
+
* same reason populating a relation keeps it. A whitelisting `$select` gains the key and an
|
|
15
|
+
* `$exclude` loses it; the raw-array `$select` form has nothing to augment, so it is refused.
|
|
16
|
+
*/
|
|
17
|
+
export function withIdForCounts(entity, q) {
|
|
18
|
+
const meta = getMeta(entity);
|
|
19
|
+
if (!q.$count || !meta.id) {
|
|
20
|
+
return q;
|
|
21
|
+
}
|
|
22
|
+
if (q.$distinct) {
|
|
23
|
+
// The id would have to join the projection for the tallies to group by, and that is the set
|
|
24
|
+
// `$distinct` collapses on: adding it silently stops the grouping collapsing anything at all.
|
|
25
|
+
throw new TypeError(`$count cannot be combined with $distinct: the tallies group by each row's '${String(meta.id)}', which the grouping does not keep.`);
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(q.$select)) {
|
|
28
|
+
// Nothing to add the id to, and without it every tally would be looked up by `undefined` and
|
|
29
|
+
// come back zero. Refused rather than answered wrong, the way an unorderable clause is.
|
|
30
|
+
throw new TypeError(`$count needs the '${String(meta.id)}' of each row to group its tallies by, which a raw $select cannot carry. Use a $select map, or drop the $count.`);
|
|
31
|
+
}
|
|
32
|
+
const next = { ...q };
|
|
33
|
+
const select = asSelectMap(q.$select);
|
|
34
|
+
if (select && !select[meta.id]) {
|
|
35
|
+
next.$select = { ...select, [meta.id]: true };
|
|
36
|
+
}
|
|
37
|
+
if (q.$exclude?.[meta.id]) {
|
|
38
|
+
const kept = { ...q.$exclude };
|
|
39
|
+
delete kept[meta.id];
|
|
40
|
+
next.$exclude = kept;
|
|
41
|
+
}
|
|
42
|
+
return next;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* How many rows each counted relation holds, under `_count` on every parent. One grouped aggregate
|
|
46
|
+
* per relation over every parent at once - the batching a populated to-many already gets - so the
|
|
47
|
+
* cost stays flat in the number of rows the read returned rather than one statement per row.
|
|
48
|
+
*/
|
|
49
|
+
export async function fillRelationCounts(querier, entity, payload, count) {
|
|
50
|
+
if (!payload.length || !count) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const meta = getMeta(entity);
|
|
54
|
+
// The ids cross the same boundary the entity does: past here every column is named by metadata,
|
|
55
|
+
// so they are read as {@link CountedRow}'s values rather than as this entity's own id type.
|
|
56
|
+
const ids = payload.map((it) => it[meta.id]);
|
|
57
|
+
const counted = new Map();
|
|
58
|
+
for (const relKey of getKeys(count)) {
|
|
59
|
+
const value = count[relKey];
|
|
60
|
+
const relOpts = meta.relations[relKey];
|
|
61
|
+
if (!value || !relOpts) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const where = typeof value === 'object' ? value.$where : undefined;
|
|
65
|
+
counted.set(relKey, await countPerParent(querier, relOpts, ids, where));
|
|
66
|
+
}
|
|
67
|
+
for (const parent of payload) {
|
|
68
|
+
const id = String(parent[meta.id]);
|
|
69
|
+
const row = {};
|
|
70
|
+
for (const [relKey, byParent] of counted) {
|
|
71
|
+
// A parent the grouped result has no row for matched nothing, which is a zero rather than a
|
|
72
|
+
// gap: `_count` names what the caller asked to count, so every key it asked for is present.
|
|
73
|
+
row[relKey] = byParent[id] ?? 0;
|
|
74
|
+
}
|
|
75
|
+
parent[COUNT_RESULT_KEY] = row;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function countPerParent(querier, relOpts, ids, where) {
|
|
79
|
+
const through = relOpts.through;
|
|
80
|
+
if (through) {
|
|
81
|
+
return countThroughPerParent(querier, relOpts, countable(through()), ids, where);
|
|
82
|
+
}
|
|
83
|
+
const foreign = parentKeyColumn(relOpts);
|
|
84
|
+
return groupedCount(querier, countable(relOpts.entity()), foreign, { ...where, [foreign]: ids });
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A many-to-many counts its junction rows, one per pairing. A filter names the target's columns,
|
|
88
|
+
* which the junction does not have, so the matching targets are resolved first and the junction
|
|
89
|
+
* counted against them - the one shape here that costs a second statement, and only when filtered.
|
|
90
|
+
*/
|
|
91
|
+
async function countThroughPerParent(querier, relOpts, throughEntity, ids, where) {
|
|
92
|
+
const local = parentKeyColumn(relOpts);
|
|
93
|
+
const throughWhere = { [local]: ids };
|
|
94
|
+
if (where) {
|
|
95
|
+
const target = countable(relOpts.entity());
|
|
96
|
+
const targetId = getMeta(target).id;
|
|
97
|
+
const targets = await querier.findMany(target, { $select: { [targetId]: true }, $where: where });
|
|
98
|
+
throughWhere[targetKeyColumn(relOpts)] = targets.map((it) => it[targetId]);
|
|
99
|
+
}
|
|
100
|
+
return groupedCount(querier, throughEntity, local, throughWhere);
|
|
101
|
+
}
|
|
102
|
+
/** `SELECT <key>, COUNT(*) ... GROUP BY <key>`, as a lookup from parent key to tally. */
|
|
103
|
+
async function groupedCount(querier, entity, groupKey, where) {
|
|
104
|
+
const $agg = { [COUNT_AGG_ALIAS]: { $count: '*' } };
|
|
105
|
+
const rows = await querier.aggregate(entity, { $group: { [groupKey]: true }, $agg, $where: where });
|
|
106
|
+
const byParent = {};
|
|
107
|
+
for (const row of rows) {
|
|
108
|
+
byParent[String(row[groupKey])] = Number(row[COUNT_AGG_ALIAS]);
|
|
109
|
+
}
|
|
110
|
+
return byParent;
|
|
111
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AbstractSqlDialect } from '../dialect/abstractSqlDialect.js';
|
|
2
|
-
import type { DialectFeatures, EntityMeta, FieldOptions, QueryContext, QuerySizeComparisonOps, QueryTextSearchOptions, Type, VectorDistance } from '../type/index.js';
|
|
2
|
+
import type { DialectFeatures, EntityMeta, FieldOptions, QueryContext, QueryPager, QuerySizeComparisonOps, QueryTextSearchOptions, Type, VectorDistance } from '../type/index.js';
|
|
3
3
|
export declare class SqliteDialect extends AbstractSqlDialect {
|
|
4
4
|
/** Default {@link DialectFeatures} for SQLite and SQLite-derived dialects. */
|
|
5
5
|
protected readonly featureDefaults: DialectFeatures;
|
|
@@ -30,6 +30,11 @@ export declare class SqliteDialect extends AbstractSqlDialect {
|
|
|
30
30
|
protected readonly caseInsensitiveMatch = "native";
|
|
31
31
|
protected get neOp(): string;
|
|
32
32
|
normalizeValue(value: unknown): unknown;
|
|
33
|
+
/**
|
|
34
|
+
* `OFFSET` is only legal after a `LIMIT` here too, so a bare `$skip` needs one - `-1` being
|
|
35
|
+
* SQLite's own spelling of "no limit", where the MySQL family uses its largest `BIGINT`.
|
|
36
|
+
*/
|
|
37
|
+
pager(ctx: QueryContext, opts: QueryPager): void;
|
|
33
38
|
/**
|
|
34
39
|
* FTS5 matches the table itself rather than its columns, so this only works when the table *is* an
|
|
35
40
|
* FTS5 virtual table (UQL does not create those; declare it outside your entities).
|
|
@@ -65,6 +65,16 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
65
65
|
return value.getTime();
|
|
66
66
|
return super.normalizeValue(value);
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* `OFFSET` is only legal after a `LIMIT` here too, so a bare `$skip` needs one - `-1` being
|
|
70
|
+
* SQLite's own spelling of "no limit", where the MySQL family uses its largest `BIGINT`.
|
|
71
|
+
*/
|
|
72
|
+
pager(ctx, opts) {
|
|
73
|
+
if (opts.$limit === undefined && opts.$skip !== undefined) {
|
|
74
|
+
ctx.append(' LIMIT -1');
|
|
75
|
+
}
|
|
76
|
+
super.pager(ctx, opts);
|
|
77
|
+
}
|
|
68
78
|
/**
|
|
69
79
|
* FTS5 matches the table itself rather than its columns, so this only works when the table *is* an
|
|
70
80
|
* FTS5 virtual table (UQL does not create those; declare it outside your entities).
|
package/dist/type/dialect.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UpdatePayload } from './entity.js';
|
|
2
|
-
import type { Query, QueryConflictPaths, QueryOptions, QuerySearch } from './query.js';
|
|
2
|
+
import type { Query, QueryConflictPaths, QueryFilter, QueryOptions, QuerySearch } from './query.js';
|
|
3
3
|
import type { QueryAggMap, QueryAggregate, QueryGroupMap } from './queryAggregate.js';
|
|
4
4
|
import type { Type } from './utility.js';
|
|
5
5
|
/**
|
|
@@ -113,8 +113,9 @@ export interface QueryDialect {
|
|
|
113
113
|
* @param entity the target entity
|
|
114
114
|
* @param q the criteria options
|
|
115
115
|
* @param opts the query options
|
|
116
|
+
* @param totalAlias when given, carry the unpaged match count on every row under this alias
|
|
116
117
|
*/
|
|
117
|
-
find<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>, opts?: QueryOptions): void;
|
|
118
|
+
find<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>, opts?: QueryOptions, totalAlias?: string): void;
|
|
118
119
|
/**
|
|
119
120
|
* counts the number of records matching the given search parameters.
|
|
120
121
|
* @param ctx the query context
|
|
@@ -122,7 +123,7 @@ export interface QueryDialect {
|
|
|
122
123
|
* @param q the criteria options
|
|
123
124
|
* @param opts the query options
|
|
124
125
|
*/
|
|
125
|
-
count<E>(ctx: QueryContext, entity: Type<E>, q:
|
|
126
|
+
count<E>(ctx: QueryContext, entity: Type<E>, q: QueryFilter<E>, opts?: QueryOptions): void;
|
|
126
127
|
/**
|
|
127
128
|
* insert records.
|
|
128
129
|
* @param ctx the query context
|
package/dist/type/index.d.ts
CHANGED
package/dist/type/index.js
CHANGED