uql-orm 0.69.0 → 0.70.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/querier/httpQuerier.d.ts +7 -7
- package/dist/browser/type/clientQuerier.d.ts +5 -5
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/cockroachdb/cockroachDialect.js +2 -2
- package/dist/dialect/abstractDialect.d.ts +8 -2
- package/dist/dialect/abstractDialect.js +17 -1
- package/dist/dialect/abstractSqlDialect.d.ts +16 -5
- package/dist/dialect/abstractSqlDialect.js +82 -44
- package/dist/dialect/aliases.d.ts +10 -7
- package/dist/dialect/aliases.js +10 -7
- package/dist/dialect/mysqlLikeSqlDialect.js +3 -2
- package/dist/dialect/pgLikeSqlDialect.js +1 -0
- package/dist/dialect/queryJoins.d.ts +8 -1
- package/dist/dialect/queryJoins.js +33 -10
- package/dist/entity/decorator/members.d.ts +25 -11
- package/dist/entity/metadata/definition.d.ts +8 -3
- package/dist/migrate/codegen/entityCodeGenerator.js +4 -2
- package/dist/migrate/migrator.d.ts +2 -1
- package/dist/migrate/migrator.js +5 -3
- package/dist/mongo/mongoDialect.d.ts +31 -27
- package/dist/mongo/mongoDialect.js +172 -112
- package/dist/mongo/mongodbQuerier.d.ts +0 -2
- package/dist/mongo/mongodbQuerier.js +12 -11
- package/dist/mssql/mssqlDialect.js +3 -2
- package/dist/postgres/postgresDialect.js +2 -2
- package/dist/querier/abstractQuerier.d.ts +16 -11
- package/dist/querier/abstractQuerier.js +28 -8
- package/dist/querier/abstractQuerierPool.d.ts +9 -9
- package/dist/querier/abstractSqlQuerier.d.ts +1 -1
- package/dist/querier/abstractSqlQuerier.js +3 -3
- package/dist/sqlite/sqliteDialect.js +1 -0
- package/dist/turso/tursoDialect.d.ts +1 -1
- package/dist/turso/tursoDialect.js +6 -2
- package/dist/type/dialect.d.ts +6 -0
- package/dist/type/entity.d.ts +31 -2
- package/dist/type/migration.d.ts +7 -0
- package/dist/type/query.d.ts +6 -0
- package/dist/type/queryAggregate.d.ts +77 -42
- package/dist/type/queryAggregate.js +4 -21
- package/dist/type/universalQuerier.d.ts +9 -9
- package/dist/util/dialect.util.d.ts +6 -2
- package/dist/util/dialect.util.js +19 -5
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
2
|
+
import type { EntityData, EntityId, EntityWrite, ExtraOptions, FieldKey, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, PrimaryKey, QueryUpdateResult, QueryUpsertOneResult, QueryUpsertManyResult, RelationKey, TransactionOptions, Type, UpdatePayload, UpdateWrite, WrittenId } from '../type/index.js';
|
|
2
3
|
import { LoggerWrapper } from '../util/index.js';
|
|
3
4
|
/** Base class for all database queriers. */
|
|
4
5
|
export declare abstract class AbstractQuerier implements Querier {
|
|
@@ -15,6 +16,7 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
15
16
|
*/
|
|
16
17
|
protected released: boolean;
|
|
17
18
|
protected readonly logger: LoggerWrapper;
|
|
19
|
+
abstract readonly dialect: AbstractDialect;
|
|
18
20
|
constructor(extra?: ExtraOptions | undefined);
|
|
19
21
|
protected validateProjectionQuery<E extends object>(entity: Type<E>, q: Query<E>): void;
|
|
20
22
|
private validateProjectionQueryRecursive;
|
|
@@ -73,21 +75,24 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
73
75
|
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>[]>;
|
|
74
76
|
/** Abstract outright: nothing is shared to do around it. See {@link UniversalQuerier.estimatedCount}. */
|
|
75
77
|
abstract estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
|
|
76
|
-
insertOne<E extends object>(entity: Type<E>, payload:
|
|
78
|
+
insertOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>): Promise<WrittenId<E> | undefined>;
|
|
77
79
|
/**
|
|
78
80
|
* The `onInsert` values are filled here, before the write, so the after hooks and the ids read the
|
|
79
81
|
* same rows the statement wrote.
|
|
80
82
|
*/
|
|
81
|
-
insertMany<E extends object>(entity: Type<E>, payload:
|
|
83
|
+
insertMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
|
|
82
84
|
/** Writes `rows`, and onto each one the key the database generated for it, where it can tell. */
|
|
83
85
|
protected abstract internalInsertMany<E extends object>(entity: Type<E>, rows: EntityData<E>[]): Promise<void>;
|
|
84
|
-
updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload:
|
|
86
|
+
updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdateWrite<E>, opts?: QueryOptions): Promise<number>;
|
|
85
87
|
/** Settles the rows first where the update cascades, so a payload changing what `$where` reads still names them. */
|
|
86
|
-
updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload:
|
|
88
|
+
updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdateWrite<E>, opts?: QueryOptions): Promise<number>;
|
|
87
89
|
/** The UPDATE, skipped where the payload writes no column, reporting `unwritten` instead. */
|
|
88
90
|
private updateColumns;
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Whether a write has to name the rows `q` matches by their ids: no engine pages or orders an update or
|
|
93
|
+
* delete, and one without {@link DialectFeatures.correlatedWrites} cannot read a relation in its filter.
|
|
94
|
+
*/
|
|
95
|
+
protected settlesWrite<E extends object>(entity: Type<E>, q: QuerySearch<E>): boolean;
|
|
91
96
|
/** The ids `q` matches, in its own order and page. */
|
|
92
97
|
protected settleIds<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<EntityId<E>[]>;
|
|
93
98
|
/** Runs one UPDATE over `q`, which names its rows by id wherever {@link updateMany} settled them. */
|
|
@@ -95,8 +100,8 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
95
100
|
restoreOneById<E extends object>(entity: Type<E>, id: EntityId<E>): Promise<number>;
|
|
96
101
|
restoreMany<E extends object>(entity: Type<E>, q: QuerySearch<E>): Promise<number>;
|
|
97
102
|
/** Fires `beforeUpsert`/`afterUpsert`: which branch a row takes is the database's to decide, so neither the insert's nor the update's pair fits. */
|
|
98
|
-
upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload:
|
|
99
|
-
upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload:
|
|
103
|
+
upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>): Promise<QueryUpsertOneResult<E>>;
|
|
104
|
+
upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>[]): Promise<QueryUpsertManyResult<E>>;
|
|
100
105
|
protected abstract internalUpsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
|
|
101
106
|
protected abstract internalUpsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
|
|
102
107
|
deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts?: QueryOptions): Promise<number>;
|
|
@@ -107,13 +112,13 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
107
112
|
}, opts?: QueryOptions): Promise<number>;
|
|
108
113
|
/** Runs one DELETE (or soft-delete stamp) over `q`, which names its rows by id wherever {@link deleteMany} settled them. */
|
|
109
114
|
protected abstract internalDeleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
110
|
-
saveOne<E extends object>(entity: Type<E>, payload:
|
|
115
|
+
saveOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>): Promise<WrittenId<E> | undefined>;
|
|
111
116
|
/**
|
|
112
117
|
* Whether a row names its key decides its statement, never whether the row exists: a named row
|
|
113
118
|
* upserts on that key, so a stale id is written rather than silently missed, and an unnamed one
|
|
114
119
|
* inserts. A composite is always named. The hooks follow the statement: a named row fires the upsert pair.
|
|
115
120
|
*/
|
|
116
|
-
saveMany<E extends object>(entity: Type<E>, payload:
|
|
121
|
+
saveMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
|
|
117
122
|
/** Writes each inserted row's relations, one set of statements per relation whatever the number of rows. */
|
|
118
123
|
protected insertRelations<E extends object>(entity: Type<E>, rows: E[]): Promise<void>;
|
|
119
124
|
/** `EntityId` because a settled composite row is an object, which {@link childrenOf} reads each foreign key column out of. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertSoleId, getMeta, idOf, namesKey, relationOf } from '../entity/index.js';
|
|
2
|
-
import { cascadesOnDelete, childrenOf, clone, entityName, fillOnFields, filterFieldKeys, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isPagedQuery, isScalarId, LoggerWrapper, parentJoins, queryLoggerFor, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
2
|
+
import { cascadesOnDelete, childrenOf, clone, entityName, fillOnFields, filterFieldKeys, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isPagedQuery, hasKeys, isScalarId, LoggerWrapper, parentJoins, queryLoggerFor, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
3
|
import { enrichError } from './queryError.js';
|
|
4
4
|
/**
|
|
5
5
|
* Refuses a nullish id, which would reduce to no filter at all, and a composite id missing a column,
|
|
@@ -27,6 +27,17 @@ function assertIdValue(entity, id) {
|
|
|
27
27
|
function soleParentColumn(relOpts) {
|
|
28
28
|
return parentJoins(relOpts, 1)[0].joined;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* A bulk write names the rows it changes: a `$where`, or a `$limit` capping how many it reaches. The
|
|
32
|
+
* caller's own clauses are what count - a filter the entity adds, soft delete's, would otherwise make
|
|
33
|
+
* every table look narrowed, which is the case this exists to catch.
|
|
34
|
+
*/
|
|
35
|
+
function assertNamesRows(entity, method, q, opts) {
|
|
36
|
+
if (opts?.unfiltered || hasKeys(q?.$where) || q?.$limit !== undefined) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
throw new TypeError(`'${method}' over '${entity.name}' names no rows, so it would address every one: pass '{ unfiltered: true }' to mean it`);
|
|
40
|
+
}
|
|
30
41
|
/**
|
|
31
42
|
* The id each written row is named by, in payload order. Read off the rows as written, so a key the
|
|
32
43
|
* database generated or the ORM filled is there, and a composite is named by every column of it.
|
|
@@ -183,6 +194,7 @@ export class AbstractQuerier {
|
|
|
183
194
|
}
|
|
184
195
|
/** Settles the rows first where the update cascades, so a payload changing what `$where` reads still names them. */
|
|
185
196
|
async updateMany(entity, q, payload, opts) {
|
|
197
|
+
assertNamesRows(entity, 'updateMany', q, opts);
|
|
186
198
|
const meta = getMeta(entity);
|
|
187
199
|
return this.hooked(entity, 'Update', [payload], async ([row]) => {
|
|
188
200
|
fillOnFields(meta, [row], 'onUpdate');
|
|
@@ -206,9 +218,13 @@ export class AbstractQuerier {
|
|
|
206
218
|
const writes = filterFieldKeys(getMeta(entity), row, 'onUpdate').length > 0;
|
|
207
219
|
return writes ? this.internalUpdateMany(entity, q, row, opts) : unwritten;
|
|
208
220
|
}
|
|
209
|
-
/**
|
|
210
|
-
|
|
211
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Whether a write has to name the rows `q` matches by their ids: no engine pages or orders an update or
|
|
223
|
+
* delete, and one without {@link DialectFeatures.correlatedWrites} cannot read a relation in its filter.
|
|
224
|
+
*/
|
|
225
|
+
settlesWrite(entity, q) {
|
|
226
|
+
const { dialect } = this;
|
|
227
|
+
return isPagedQuery(q) || (!dialect.features.correlatedWrites && dialect.constrainsRelations(entity, q.$where));
|
|
212
228
|
}
|
|
213
229
|
/** The ids `q` matches, in its own order and page. */
|
|
214
230
|
async settleIds(entity, q, opts) {
|
|
@@ -254,6 +270,7 @@ export class AbstractQuerier {
|
|
|
254
270
|
}
|
|
255
271
|
async deleteMany(entityOrQuery, qOrOpts, maybeOpts) {
|
|
256
272
|
const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, qOrOpts, maybeOpts);
|
|
273
|
+
assertNamesRows(entity, 'deleteMany', q, opts);
|
|
257
274
|
const meta = getMeta(entity);
|
|
258
275
|
const cascades = cascadesOnDelete(meta);
|
|
259
276
|
const watched = this.hasHook(entity, 'beforeDelete') || this.hasHook(entity, 'afterDelete');
|
|
@@ -350,8 +367,7 @@ export class AbstractQuerier {
|
|
|
350
367
|
const relOpts = relationOf(meta, relKey);
|
|
351
368
|
const relEntity = relOpts.entity();
|
|
352
369
|
const target = relOpts.through ? relOpts.through() : relEntity;
|
|
353
|
-
|
|
354
|
-
await this.deleteMany(target, { $where: where }, opts);
|
|
370
|
+
await this.deleteMany(target, { $where: childrenOf(parentJoins(relOpts, meta.ids.length), ids) }, opts);
|
|
355
371
|
}
|
|
356
372
|
}
|
|
357
373
|
/**
|
|
@@ -480,8 +496,12 @@ export class AbstractQuerier {
|
|
|
480
496
|
* the write filled in - a generated key, an `onInsert` value - without it landing on the caller's.
|
|
481
497
|
*/
|
|
482
498
|
async hooked(entity, event, payloads, write) {
|
|
483
|
-
|
|
484
|
-
|
|
499
|
+
// The one place a caller's write becomes the row the rest of the library handles. They are the
|
|
500
|
+
// same object: a write is the entity's data without the keys the database fills, which
|
|
501
|
+
// TypeScript cannot relate across an entity it has not resolved.
|
|
502
|
+
const asRows = payloads;
|
|
503
|
+
await this.emitHook(entity, `before${event}`, asRows);
|
|
504
|
+
const rows = clone(asRows);
|
|
485
505
|
const result = await write(rows);
|
|
486
506
|
await this.emitHook(entity, `after${event}`, rows);
|
|
487
507
|
return result;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AbstractDialect } from '../dialect/index.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { EntityWrite, EntityId, ExtraOptions, FieldKey, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryUpsertOneResult, QueryUpsertManyResult, RelationKey, TransactionOptions, Type, UpdateWrite, WrittenId } 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
|
*/
|
|
@@ -37,14 +37,14 @@ export declare abstract class AbstractQuerierPool<Q extends Querier, D extends A
|
|
|
37
37
|
exists<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<boolean>;
|
|
38
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
39
|
estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
|
|
40
|
-
insertOne<E extends object>(entity: Type<E>, payload:
|
|
41
|
-
insertMany<E extends object>(entity: Type<E>, payload:
|
|
42
|
-
updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload:
|
|
43
|
-
updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload:
|
|
44
|
-
upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload:
|
|
45
|
-
upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload:
|
|
46
|
-
saveOne<E extends object>(entity: Type<E>, payload:
|
|
47
|
-
saveMany<E extends object>(entity: Type<E>, payload:
|
|
40
|
+
insertOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>): Promise<WrittenId<E> | undefined>;
|
|
41
|
+
insertMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
|
|
42
|
+
updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdateWrite<E>, opts?: QueryOptions): Promise<number>;
|
|
43
|
+
updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdateWrite<E>, opts?: QueryOptions): Promise<number>;
|
|
44
|
+
upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>): Promise<QueryUpsertOneResult<E>>;
|
|
45
|
+
upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>[]): Promise<QueryUpsertManyResult<E>>;
|
|
46
|
+
saveOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>): Promise<WrittenId<E> | undefined>;
|
|
47
|
+
saveMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
|
|
48
48
|
deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts?: QueryOptions): Promise<number>;
|
|
49
49
|
deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
50
50
|
restoreOneById<E extends object>(entity: Type<E>, id: EntityId<E>): Promise<number>;
|
|
@@ -80,7 +80,7 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
80
80
|
*/
|
|
81
81
|
private hydrateFields;
|
|
82
82
|
/**
|
|
83
|
-
* Runs a statement whose one row carries a {@link
|
|
83
|
+
* Runs a statement whose one row carries a {@link AGGREGATE_VALUE_ALIAS} column. `Number` because `COUNT(*)` is BIGINT and
|
|
84
84
|
* a caller supplying their own `types` replaces the decoding the pools do at the wire; `?? 0` because
|
|
85
85
|
* a catalog that does not know the table answers with no row, which is nothing counted.
|
|
86
86
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AGGREGATE_VALUE_ALIAS, TOTAL_ALIAS } from '../dialect/aliases.js';
|
|
2
2
|
import { decodeColumn } from '../dialect/hydrateColumn.js';
|
|
3
3
|
import { getMeta, namesKey } from '../entity/index.js';
|
|
4
4
|
import { COUNT_RESULT_KEY } from '../type/index.js';
|
|
@@ -290,13 +290,13 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
292
|
/**
|
|
293
|
-
* Runs a statement whose one row carries a {@link
|
|
293
|
+
* Runs a statement whose one row carries a {@link AGGREGATE_VALUE_ALIAS} column. `Number` because `COUNT(*)` is BIGINT and
|
|
294
294
|
* a caller supplying their own `types` replaces the decoding the pools do at the wire; `?? 0` because
|
|
295
295
|
* a catalog that does not know the table answers with no row, which is nothing counted.
|
|
296
296
|
*/
|
|
297
297
|
async runCount(build) {
|
|
298
298
|
const [row] = await this.query(build);
|
|
299
|
-
return Number(row?.[
|
|
299
|
+
return Number(row?.[AGGREGATE_VALUE_ALIAS] ?? 0);
|
|
300
300
|
}
|
|
301
301
|
async internalCount(entity, q, opts) {
|
|
302
302
|
return this.runCount((ctx) => this.dialect.count(ctx, entity, q, opts));
|
|
@@ -5,6 +5,6 @@ import type { SqlDialectFeatures } from '../type/index.js';
|
|
|
5
5
|
* `ORDER BY` inside an aggregate, which its Rust engine lacks. Imports no driver.
|
|
6
6
|
*/
|
|
7
7
|
export declare class TursoDialect extends LibsqlDialect {
|
|
8
|
-
/** The Rust engine takes no `ORDER BY` inside an aggregate. */
|
|
8
|
+
/** The Rust engine takes no `ORDER BY` inside an aggregate, nor a subquery reading the table a write changes. */
|
|
9
9
|
readonly features: SqlDialectFeatures;
|
|
10
10
|
}
|
|
@@ -5,6 +5,10 @@ import { SQLITE_FEATURES } from '../sqlite/sqliteDialect.js';
|
|
|
5
5
|
* `ORDER BY` inside an aggregate, which its Rust engine lacks. Imports no driver.
|
|
6
6
|
*/
|
|
7
7
|
export class TursoDialect extends LibsqlDialect {
|
|
8
|
-
/** The Rust engine takes no `ORDER BY` inside an aggregate. */
|
|
9
|
-
features = {
|
|
8
|
+
/** The Rust engine takes no `ORDER BY` inside an aggregate, nor a subquery reading the table a write changes. */
|
|
9
|
+
features = {
|
|
10
|
+
...SQLITE_FEATURES,
|
|
11
|
+
orderedJsonAggregates: false,
|
|
12
|
+
correlatedWrites: false,
|
|
13
|
+
};
|
|
10
14
|
}
|
package/dist/type/dialect.d.ts
CHANGED
|
@@ -122,6 +122,12 @@ export interface DialectFeatures {
|
|
|
122
122
|
* answer, not the driver's: node-`pg` streams on its own and keeps doing so.
|
|
123
123
|
*/
|
|
124
124
|
readonly serverSideCursors: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Whether an `UPDATE` or `DELETE` can read a relation in its filter. False on MongoDB, whose filter
|
|
127
|
+
* hosts no lookup, and on Turso's engine, which cannot resolve the written table inside a subquery:
|
|
128
|
+
* such a write reads the ids of the rows it names first.
|
|
129
|
+
*/
|
|
130
|
+
readonly correlatedWrites: boolean;
|
|
125
131
|
}
|
|
126
132
|
/** What a SQL engine can do beyond {@link DialectFeatures}, read where a statement is built. */
|
|
127
133
|
export interface SqlDialectFeatures extends DialectFeatures {
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { EnumValues, ForeignKeyAction, IndexType } from '../schema/types.js
|
|
|
2
2
|
import type { FilterOptions, RelationQuery } from './query.js';
|
|
3
3
|
import type { ColumnRef, QueryRaw, RelationAggregate } from './queryRaw.js';
|
|
4
4
|
import type { QueryWhere } from './queryWhere.js';
|
|
5
|
-
import type { Except, IsMany, Json, Scalar, Type, Unpacked } from './utility.js';
|
|
5
|
+
import type { Except, IsMany, Json, Scalar, Type, Unpacked, Writable } from './utility.js';
|
|
6
6
|
import type { VectorDistance, VectorIndexOptions, VectorIndexType } from './vector.js';
|
|
7
7
|
/** Brands the property an entity is identified by, where it is not `id`, `_id` or `uuid`. */
|
|
8
8
|
export declare const idKey: unique symbol;
|
|
@@ -20,6 +20,24 @@ export type Key<E> = keyof E & string;
|
|
|
20
20
|
export type FieldKey<E> = {
|
|
21
21
|
readonly [K in keyof E]-?: [NonNullable<E[K]>] extends [Scalar | readonly Scalar[] | Json | readonly Json[]] ? K : never;
|
|
22
22
|
}[Key<E>];
|
|
23
|
+
/**
|
|
24
|
+
* Whether `A` and `B` are the same type, `readonly` included - which no conditional sees, since
|
|
25
|
+
* assignability ignores the modifier. Two identical generic signatures compare equal only when their
|
|
26
|
+
* deferred bodies do.
|
|
27
|
+
*/
|
|
28
|
+
type IfEquals<A, B, Yes, No> = (<T>() => T extends A ? 1 : 2) extends <T>() => (T extends B ? 1 : 2) ? Yes : No;
|
|
29
|
+
/**
|
|
30
|
+
* The fields a caller writes: every one the class does not declare `readonly`. A field the database
|
|
31
|
+
* writes - a relation aggregate, a stored generated column, a trigger-kept stamp - is `readonly`, and
|
|
32
|
+
* its value never reaches the database, so a write payload leaves it out rather than dropping it.
|
|
33
|
+
*/
|
|
34
|
+
export type WritableKey<E> = {
|
|
35
|
+
readonly [K in FieldKey<E>]-?: IfEquals<Pick<E, K>, Writable<Pick<E, K>>, K, never>;
|
|
36
|
+
}[FieldKey<E>];
|
|
37
|
+
/** A whole-record write as a caller supplies one: {@link EntityData} without the fields it cannot write. */
|
|
38
|
+
export type EntityWrite<E> = EntityData<E, WritableKey<E>>;
|
|
39
|
+
/** A partial write as a caller supplies one: {@link UpdatePayload} without them. */
|
|
40
|
+
export type UpdateWrite<E, Raw = QueryRaw> = UpdatePayload<E, Raw, WritableKey<E>>;
|
|
23
41
|
/** The relation names of an entity: every key but its fields and its methods, so the two sets cannot drift. */
|
|
24
42
|
export type RelationKey<E> = Exclude<Key<E>, FieldKey<E> | MethodKey<E>>;
|
|
25
43
|
/**
|
|
@@ -267,10 +285,21 @@ export type TsTypeOf<T> = T extends StringConstructor ? string : T extends Numbe
|
|
|
267
285
|
*/
|
|
268
286
|
export type FieldOptionsFor<V, E = unknown> = (FieldOptions<NonNullable<V>, E> & {
|
|
269
287
|
readonly type: TypeFor<V>;
|
|
288
|
+
readonly isId: true;
|
|
270
289
|
}) | (FieldOptions<NonNullable<V>, E> & {
|
|
290
|
+
readonly type: TypeFor<V>;
|
|
291
|
+
} & DeclaresNotNull<V>) | (FieldOptions<NonNullable<V>, E> & {
|
|
271
292
|
readonly references: EntityGetter;
|
|
272
293
|
readonly type?: TypeFor<V>;
|
|
273
|
-
}) | AggregateOptionsFor<V, E>;
|
|
294
|
+
} & DeclaresNotNull<V>) | AggregateOptionsFor<V, E>;
|
|
295
|
+
/**
|
|
296
|
+
* A column holds `null` unless `nullable: false` says otherwise, and a read hydrates one, so a property
|
|
297
|
+
* that does not admit it says so here. The decorators state the same rule the other way round, against
|
|
298
|
+
* the property they are applied to; a key needs neither, being NOT NULL on every engine.
|
|
299
|
+
*/
|
|
300
|
+
type DeclaresNotNull<V> = null extends V ? unknown : {
|
|
301
|
+
readonly nullable: false;
|
|
302
|
+
};
|
|
274
303
|
/**
|
|
275
304
|
* A field a relation aggregate computes: the aggregate types it, so it declares no `type`, and only the
|
|
276
305
|
* two a row change turns into a delta - `count` and `sum` - may be `stored`.
|
package/dist/type/migration.d.ts
CHANGED
|
@@ -9,6 +9,13 @@ import type { EntityMeta, EntityWhereMeta, FieldOptions, IndexColumnSchema, Logg
|
|
|
9
9
|
*/
|
|
10
10
|
export interface MigrationDefinition<Q extends Querier = SqlQuerier> {
|
|
11
11
|
readonly name?: string;
|
|
12
|
+
/**
|
|
13
|
+
* `false` runs this migration outside a transaction, for a statement an engine refuses inside one -
|
|
14
|
+
* `CREATE INDEX CONCURRENTLY` on Postgres, the index a busy table needs. The cost is the rollback: a
|
|
15
|
+
* failure part-way leaves the statements before it applied and the migration unlogged. MongoDB
|
|
16
|
+
* creates collections outside any transaction already, so it changes nothing there.
|
|
17
|
+
*/
|
|
18
|
+
readonly transaction?: boolean;
|
|
12
19
|
up(querier: Q): Promise<void>;
|
|
13
20
|
down(querier: Q): Promise<void>;
|
|
14
21
|
}
|
package/dist/type/query.d.ts
CHANGED
|
@@ -16,6 +16,12 @@ export type QueryOptions = {
|
|
|
16
16
|
* already-deleted rows are removed too. No effect on entities without a soft-delete field.
|
|
17
17
|
*/
|
|
18
18
|
hardDelete?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* `updateMany`/`deleteMany` only: address every row of the table on purpose. Without it a bulk write
|
|
21
|
+
* that names none - no `$where` and no `$limit` - is refused, since a forgotten filter and the whole
|
|
22
|
+
* table look alike. The entity's own filters never count as naming one.
|
|
23
|
+
*/
|
|
24
|
+
unfiltered?: boolean;
|
|
19
25
|
/**
|
|
20
26
|
* prefix the query with this.
|
|
21
27
|
*/
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { FieldKey } from './entity.js';
|
|
1
|
+
import type { FieldKey, RelationKey, RelationTarget } from './entity.js';
|
|
2
2
|
import type { QueryPager, QuerySelect, QuerySortDirection } from './query.js';
|
|
3
|
+
import type { QueryRaw } from './queryRaw.js';
|
|
3
4
|
import type { QueryWhere, QueryWhereFieldValue } from './queryWhere.js';
|
|
4
|
-
import type { RejectKeys } from './utility.js';
|
|
5
|
+
import type { IsMany, RejectKeys } from './utility.js';
|
|
5
6
|
/** The columns `$group` names by a literal `true`, so an uninferred `$group`, its own constraint, names none. */
|
|
6
7
|
type GroupedKeys<G> = {
|
|
7
8
|
[K in keyof G]: G[K] extends true ? K : never;
|
|
@@ -23,22 +24,14 @@ export type QueryAggregateOp = (typeof QUERY_AGGREGATE_OPS)[number];
|
|
|
23
24
|
*/
|
|
24
25
|
export declare function isQueryAggregateOp(op: string): op is QueryAggregateOp;
|
|
25
26
|
/**
|
|
26
|
-
* DISTINCT
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* are omitted: DISTINCT is a no-op for them.
|
|
27
|
+
* `$countDistinct` -> `COUNT(DISTINCT col)`. Flat (not a nested `{ $distinct }` argument) so the op is
|
|
28
|
+
* self-documenting and greppable. The other ops have no DISTINCT variant: `$sum`/`$avg` over deduplicated
|
|
29
|
+
* values is rarely what totalling or averaging means, and DISTINCT is a no-op for `$min`/`$max`.
|
|
30
30
|
*/
|
|
31
|
-
|
|
32
|
-
readonly $countDistinct: '$count';
|
|
33
|
-
readonly $sumDistinct: '$sum';
|
|
34
|
-
readonly $avgDistinct: '$avg';
|
|
35
|
-
};
|
|
36
|
-
/** DISTINCT-qualified aggregate operators (the keys of {@link QUERY_AGGREGATE_DISTINCT_OP_BASE}). */
|
|
37
|
-
export type QueryAggregateDistinctOp = keyof typeof QUERY_AGGREGATE_DISTINCT_OP_BASE;
|
|
31
|
+
export type QueryAggregateDistinctOp = '$countDistinct';
|
|
38
32
|
/**
|
|
39
|
-
* Resolve an aggregate op key into its base op and whether it is DISTINCT-qualified
|
|
40
|
-
*
|
|
41
|
-
* (`$min`/`$max` have no distinct variant).
|
|
33
|
+
* Resolve an aggregate op key into its base op and whether it is DISTINCT-qualified: `$countDistinct`
|
|
34
|
+
* resolves to `$count` with `distinct: true`, a plain op to itself with `distinct: false`. Throws otherwise.
|
|
42
35
|
*/
|
|
43
36
|
export declare function resolveAggregateOp(key: string): {
|
|
44
37
|
op: QueryAggregateOp;
|
|
@@ -59,9 +52,8 @@ export type QueryFieldRef<E, F extends keyof E = FieldKey<E>> = ExactlyOne<Requi
|
|
|
59
52
|
/** The argument of an aggregate function: a field, or `'*'` (only meaningful for `COUNT(*)`). */
|
|
60
53
|
export type QueryAggregateArg<E> = QueryFieldRef<E> | '*';
|
|
61
54
|
/**
|
|
62
|
-
* Fields `SUM`/`AVG` can total. Restricted to numeric columns because
|
|
63
|
-
*
|
|
64
|
-
* produces the value the signature promises.
|
|
55
|
+
* Fields `SUM`/`AVG` can total. Restricted to numeric columns because totalling a text or date one is
|
|
56
|
+
* either an engine error or a coercion, and neither produces the value the signature promises.
|
|
65
57
|
*/
|
|
66
58
|
type NumericFieldKey<E> = {
|
|
67
59
|
readonly [K in FieldKey<E>]: [NonNullable<E[K]>] extends [number | bigint] ? K : never;
|
|
@@ -74,8 +66,12 @@ type AggregateOp = QueryAggregateOp | QueryAggregateDistinctOp;
|
|
|
74
66
|
* where `Extract` would quietly drop the renamed member and leave the subset wrong but valid.
|
|
75
67
|
*/
|
|
76
68
|
type OpsOf<K extends AggregateOp> = K;
|
|
69
|
+
/** Ops that add a column up, which keeps the column's own type: a `bigint` column totals to a `bigint`. */
|
|
70
|
+
type SummingOp = OpsOf<'$sum'>;
|
|
71
|
+
/** Ops that mean a column, which the engine floats, so the result is a `number` however wide the column. */
|
|
72
|
+
type AveragingOp = OpsOf<'$avg'>;
|
|
77
73
|
/** Ops that total a column, so their argument has to be numeric. */
|
|
78
|
-
type TotallingOp =
|
|
74
|
+
type TotallingOp = SummingOp | AveragingOp;
|
|
79
75
|
/**
|
|
80
76
|
* Every aggregate op mapped to the argument it accepts: `$count` a field or `'*'` (`COUNT(*)`),
|
|
81
77
|
* the totalling ops a numeric field, `$min`/`$max`/`$countDistinct` any field.
|
|
@@ -83,19 +79,60 @@ type TotallingOp = OpsOf<'$sum' | '$avg' | '$sumDistinct' | '$avgDistinct'>;
|
|
|
83
79
|
type QueryAggregateArgMap<E> = Record<'$count', QueryAggregateArg<E>> & Record<TotallingOp, QueryFieldRef<E, NumericFieldKey<E>>> & Record<Exclude<AggregateOp, '$count' | TotallingOp>, QueryFieldRef<E>>;
|
|
84
80
|
/**
|
|
85
81
|
* An aggregate over one field, exactly one op per entry: `{ $sum: { amount: true } }` is `SUM("amount")`,
|
|
86
|
-
* `{ $countDistinct: { id: true } }` is `COUNT(DISTINCT "id")`, and only `$count` takes `'*'`.
|
|
82
|
+
* `{ $countDistinct: { id: true } }` is `COUNT(DISTINCT "id")`, and only `$count` takes `'*'`. Its own
|
|
83
|
+
* `$where` narrows the rows it reads to those matching, over the entity's own fields.
|
|
87
84
|
*/
|
|
88
|
-
export type QueryAggregateFn<E> = ExactlyOne<QueryAggregateArgMap<E
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
export type QueryAggregateFn<E> = ExactlyOne<QueryAggregateArgMap<E>> & {
|
|
86
|
+
readonly $where?: QueryAggregateWhere<E>;
|
|
87
|
+
};
|
|
88
|
+
/** What an aggregate's own `$where` reads: the entity's fields, since a relation there is a subquery inside it. */
|
|
89
|
+
type QueryAggregateWhere<E> = QueryWhere<E, QueryRaw, FieldKey<E>>;
|
|
90
|
+
/** An aggregate's `$where` naming a key it does not have, refused: a captured `$select` skips the excess-property check. */
|
|
91
|
+
type AggregateWhereKeys<E, Fn> = Fn extends {
|
|
92
|
+
readonly $where: infer W;
|
|
93
|
+
} ? {
|
|
94
|
+
readonly $where: RejectKeys<Exclude<keyof W, keyof QueryAggregateWhere<E>>>;
|
|
95
|
+
} : unknown;
|
|
96
|
+
/** A single-key `{ [op]: Arg }` shape for each op in `Ops`, matched to infer that op's result or its argument. */
|
|
97
|
+
type FnWithOp<Ops extends string, Arg = unknown> = {
|
|
91
98
|
[K in Ops]: {
|
|
92
|
-
readonly [P in K]:
|
|
99
|
+
readonly [P in K]: Arg;
|
|
93
100
|
};
|
|
94
101
|
}[Ops];
|
|
95
102
|
/** Ops that count rows. Alone among the ops they answer `0`, never NULL, over an empty group. */
|
|
96
103
|
type CountingOp = OpsOf<'$count' | '$countDistinct'>;
|
|
97
|
-
/**
|
|
98
|
-
|
|
104
|
+
/** Ops that read back as the column they aggregate, rather than widening or floating it. */
|
|
105
|
+
type ColumnTypedOp = SummingOp | OpsOf<'$min' | '$max'>;
|
|
106
|
+
/**
|
|
107
|
+
* A field a group key reads, by the path to it: `{ orderId: true }`, or through a to-one relation,
|
|
108
|
+
* `{ transaction: { orderId: true } }`. A to-many is `never`, since joining one multiplies the rows.
|
|
109
|
+
*/
|
|
110
|
+
export type QueryGroupRef<E, K extends keyof E = FieldKey<E> | RelationKey<E>> = ExactlyOne<Required<{
|
|
111
|
+
[P in K]: P extends RelationKey<E> ? IsMany<E[P]> extends true ? never : QueryGroupRef<RelationTarget<E[P]>> : true;
|
|
112
|
+
}>>;
|
|
113
|
+
/** The columns to group by: a field switched on, `{ status: true }`, or an alias for a field a path reads. */
|
|
114
|
+
export type QueryGroupMap<E> = {
|
|
115
|
+
readonly [key: string]: true | QueryGroupRef<E>;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* A captured `$group` against its schema: a field key takes `true` and keeps its link, any other key is an
|
|
119
|
+
* alias for a path. A key switched on that is no field is refused by name, which the intersection alone lets by.
|
|
120
|
+
*/
|
|
121
|
+
type QueryGroupSchema<E, G> = Readonly<QuerySelect<E, FieldKey<E>, true>> & {
|
|
122
|
+
readonly [K in Exclude<NamedKeys<G>, FieldKey<E>>]: QueryGroupRef<E>;
|
|
123
|
+
} & RejectKeys<Exclude<GroupedKeys<G>, FieldKey<E>>>;
|
|
124
|
+
/**
|
|
125
|
+
* The value a path reads: the field's own type, or through a relation, `null` where the row points nowhere.
|
|
126
|
+
* `any` answers `unknown`: TypeScript checks a deferred type by instantiating it with `any`, which would
|
|
127
|
+
* walk every relation of the entity on every aggregate call, quadrupling what one costs to check.
|
|
128
|
+
*/
|
|
129
|
+
type GroupRefValue<E, Ref> = 0 extends 1 & Ref ? unknown : {
|
|
130
|
+
[K in keyof Ref & keyof E]: Ref[K] extends true ? E[K] : NonNullable<GroupRefLeaf<RelationTarget<E[K]>, Ref[K]>> | null;
|
|
131
|
+
}[keyof Ref & keyof E];
|
|
132
|
+
/** The field at the end of a path, whose type {@link GroupRefValue} widens with `null`; `any` as it does. */
|
|
133
|
+
type GroupRefLeaf<E, Ref> = 0 extends 1 & Ref ? unknown : {
|
|
134
|
+
[K in keyof Ref & keyof E]: Ref[K] extends true ? E[K] : GroupRefLeaf<RelationTarget<E[K]>, Ref[K]>;
|
|
135
|
+
}[keyof Ref & keyof E];
|
|
99
136
|
/** Computed columns by the alias each is read back under: `{ count: { $count: '*' }, avgAge: { $avg: { age: true } } }`. */
|
|
100
137
|
export type QueryAggMap<E> = {
|
|
101
138
|
readonly [alias: string]: QueryAggregateFn<E>;
|
|
@@ -103,14 +140,11 @@ export type QueryAggMap<E> = {
|
|
|
103
140
|
/** The entity type of an aggregated field reference `F`, or `unknown` if it is not a known field. */
|
|
104
141
|
type FieldValueType<E, F> = F extends keyof E ? E[F] : unknown;
|
|
105
142
|
/**
|
|
106
|
-
* A computed column's type: a count is a `number`; every other aggregate is `null` over no rows, a
|
|
107
|
-
*
|
|
143
|
+
* A computed column's type: a count is a `number`; every other aggregate is `null` over no rows, a mean
|
|
144
|
+
* a `number` whatever it read, and a total, a `$min` or a `$max` the column's own type - so a `bigint`
|
|
145
|
+
* column totals to a `bigint`, which is what the driver decodes rather than rounding through a float.
|
|
108
146
|
*/
|
|
109
|
-
type QueryAggregateFnResult<E, Fn> = Fn extends FnWithOp<CountingOp> ? number : Fn extends FnWithOp<
|
|
110
|
-
readonly $min: infer F;
|
|
111
|
-
} | {
|
|
112
|
-
readonly $max: infer F;
|
|
113
|
-
} ? FieldValueType<E, keyof F> | null : unknown;
|
|
147
|
+
type QueryAggregateFnResult<E, Fn> = Fn extends FnWithOp<CountingOp> ? number : Fn extends FnWithOp<AveragingOp> ? number | null : Fn extends FnWithOp<ColumnTypedOp, infer F> ? FieldValueType<E, keyof F> | null : unknown;
|
|
114
148
|
/**
|
|
115
149
|
* Flattens an intersection into a single object literal for readable editor hovers.
|
|
116
150
|
* @internal
|
|
@@ -118,8 +152,10 @@ type QueryAggregateFnResult<E, Fn> = Fn extends FnWithOp<CountingOp> ? number :
|
|
|
118
152
|
type Simplify<T> = {
|
|
119
153
|
[K in keyof T]: T[K];
|
|
120
154
|
} & {};
|
|
121
|
-
/** An aggregate's row: each grouped column with its entity type, each computed one with its aggregate's. */
|
|
155
|
+
/** An aggregate's row: each grouped column with its entity type, each alias with its path's, each computed one with its aggregate's. */
|
|
122
156
|
export type QueryAggregateResult<E, G, A> = Simplify<Pick<E, GroupedKeys<G> & FieldKey<E>> & {
|
|
157
|
+
-readonly [K in Exclude<NamedKeys<G>, FieldKey<E>>]: GroupRefValue<E, G[K]>;
|
|
158
|
+
} & {
|
|
123
159
|
-readonly [K in keyof A]: QueryAggregateFnResult<E, A[K]>;
|
|
124
160
|
}>;
|
|
125
161
|
/** A `HAVING` as the dialects read it, erased; {@link QueryAggregate.$having} is where it is typed. `{ count: { $gt: 5 } }` */
|
|
@@ -136,19 +172,18 @@ export type QueryAggregate<E, G extends QueryGroupMap<E> = QueryGroupMap<E>, A e
|
|
|
136
172
|
*/
|
|
137
173
|
readonly $where?: QueryWhere<E>;
|
|
138
174
|
/**
|
|
139
|
-
* Columns to group by
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* map meets its schema, {@link QueryGroupMap}, so each key keeps its link to the entity property.
|
|
175
|
+
* Columns to group by: `{ status: true }`, or an alias for a to-one relation's field by the path to
|
|
176
|
+
* it, `{ orderId: { transaction: { orderId: true } } }`. The captured map meets its schema, so a field
|
|
177
|
+
* key keeps its link to the entity property, and a typo matches neither form.
|
|
143
178
|
*/
|
|
144
|
-
readonly $group?: G &
|
|
179
|
+
readonly $group?: G & QueryGroupSchema<E, G>;
|
|
145
180
|
/**
|
|
146
181
|
* The computed columns by alias, the captured map meeting its schema so field keys stay linked. An alias
|
|
147
182
|
* repeating a `$group` column is refused, since both would come back under one name.
|
|
148
183
|
*/
|
|
149
184
|
readonly $select?: A & {
|
|
150
|
-
readonly [K in keyof A]: QueryAggregateFn<E>;
|
|
151
|
-
} & RejectKeys<NamedKeys<A> &
|
|
185
|
+
readonly [K in keyof A]: QueryAggregateFn<E> & AggregateWhereKeys<E, A[K]>;
|
|
186
|
+
} & RejectKeys<NamedKeys<A> & NamedKeys<G>>;
|
|
152
187
|
/** Filtering after grouping, by a result column, each value typed as that column is. */
|
|
153
188
|
readonly $having?: {
|
|
154
189
|
readonly [K in keyof QueryAggregateResult<E, G, A>]?: QueryWhereFieldValue<QueryAggregateResult<E, G, A>[K]>;
|
|
@@ -7,29 +7,12 @@ export function isQueryAggregateOp(op) {
|
|
|
7
7
|
return QUERY_AGGREGATE_OPS.includes(op);
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* (not a nested `{ $distinct }` argument) so the op is self-documenting and greppable. `$min`/`$max`
|
|
13
|
-
* are omitted: DISTINCT is a no-op for them.
|
|
14
|
-
*/
|
|
15
|
-
const QUERY_AGGREGATE_DISTINCT_OP_BASE = {
|
|
16
|
-
$countDistinct: '$count',
|
|
17
|
-
$sumDistinct: '$sum',
|
|
18
|
-
$avgDistinct: '$avg',
|
|
19
|
-
};
|
|
20
|
-
/** Whether `key` is a DISTINCT-qualified aggregate operator (narrows for a cast-free base lookup). */
|
|
21
|
-
function isQueryAggregateDistinctOp(key) {
|
|
22
|
-
// `Object.hasOwn`, not `key in`: the latter matches inherited members like `toString`.
|
|
23
|
-
return Object.hasOwn(QUERY_AGGREGATE_DISTINCT_OP_BASE, key);
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Resolve an aggregate op key into its base op and whether it is DISTINCT-qualified. A flat distinct
|
|
27
|
-
* op resolves to its base op with `distinct: true`; a plain op to `distinct: false`. Throws otherwise
|
|
28
|
-
* (`$min`/`$max` have no distinct variant).
|
|
10
|
+
* Resolve an aggregate op key into its base op and whether it is DISTINCT-qualified: `$countDistinct`
|
|
11
|
+
* resolves to `$count` with `distinct: true`, a plain op to itself with `distinct: false`. Throws otherwise.
|
|
29
12
|
*/
|
|
30
13
|
export function resolveAggregateOp(key) {
|
|
31
|
-
if (
|
|
32
|
-
return { op:
|
|
14
|
+
if (key === '$countDistinct') {
|
|
15
|
+
return { op: '$count', distinct: true };
|
|
33
16
|
}
|
|
34
17
|
if (isQueryAggregateOp(key)) {
|
|
35
18
|
return { op: key, distinct: false };
|