uql-orm 0.56.0 → 0.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -9
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/cockroachdb/cockroachDialect.d.ts +5 -2
- package/dist/cockroachdb/cockroachDialect.js +2 -10
- package/dist/d1/d1SqliteDialect.d.ts +1 -0
- package/dist/d1/d1SqliteDialect.js +2 -0
- package/dist/dialect/abstractSqlDialect.d.ts +196 -33
- package/dist/dialect/abstractSqlDialect.js +410 -203
- package/dist/dialect/aliases.d.ts +10 -7
- package/dist/dialect/aliases.js +12 -7
- package/dist/dialect/hydrateColumn.d.ts +8 -2
- package/dist/dialect/hydrateColumn.js +33 -1
- package/dist/dialect/jsonSql.d.ts +13 -5
- package/dist/dialect/jsonSql.js +24 -7
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +31 -3
- package/dist/dialect/mysqlLikeSqlDialect.js +57 -5
- package/dist/dialect/pgLikeSqlDialect.d.ts +20 -20
- package/dist/dialect/pgLikeSqlDialect.js +23 -48
- package/dist/dialect/pgVectorMetrics.d.ts +13 -0
- package/dist/dialect/pgVectorMetrics.js +17 -0
- package/dist/dialect/queryContext.d.ts +3 -7
- package/dist/dialect/queryContext.js +13 -8
- package/dist/dialect/queryJoins.d.ts +8 -4
- package/dist/dialect/queryJoins.js +26 -11
- package/dist/dialect/vectorSqlDialect.d.ts +2 -2
- package/dist/dialect/vectorSqlDialect.js +2 -3
- package/dist/entity/decorator/bag.d.ts +2 -2
- package/dist/entity/decorator/entity.d.ts +8 -9
- package/dist/entity/decorator/entity.js +6 -7
- package/dist/entity/decorator/members.d.ts +7 -6
- package/dist/entity/decorator/members.js +2 -1
- package/dist/entity/metadata/definition.d.ts +16 -11
- package/dist/entity/metadata/definition.js +54 -42
- package/dist/http/handler.d.ts +2 -2
- package/dist/http/handler.js +0 -1
- package/dist/maria/mariaDialect.d.ts +13 -6
- package/dist/maria/mariaDialect.js +29 -9
- package/dist/migrate/cli.d.ts +2 -3
- package/dist/migrate/cli.js +2 -2
- package/dist/migrate/codegen/entityCodeGenerator.js +6 -4
- package/dist/migrate/codegen/entityTypes.d.ts +1 -1
- package/dist/migrate/codegen/entityTypes.js +4 -3
- package/dist/migrate/codegen/indexDecoratorSource.d.ts +5 -4
- package/dist/migrate/codegen/indexDecoratorSource.js +17 -13
- package/dist/migrate/codegen/sourceLiteral.d.ts +2 -0
- package/dist/migrate/codegen/sourceLiteral.js +4 -0
- package/dist/migrate/ddl/index.d.ts +1 -5
- package/dist/migrate/ddl/index.js +14 -25
- package/dist/migrate/ddl/indexDdl.d.ts +11 -2
- package/dist/migrate/ddl/indexDdl.js +17 -1
- package/dist/migrate/ddl/mssqlIndexDdl.d.ts +10 -0
- package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +10 -17
- package/dist/migrate/ddl/mysqlIndexDdl.js +16 -27
- package/dist/migrate/ddl/pgIndexDdl.d.ts +18 -8
- package/dist/migrate/ddl/pgIndexDdl.js +29 -12
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +3 -3
- package/dist/migrate/migrator.d.ts +4 -4
- package/dist/migrate/schemaGenerator.d.ts +8 -8
- package/dist/migrate/schemaGenerator.js +5 -7
- package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
- package/dist/mongo/mongoDialect.d.ts +31 -18
- package/dist/mongo/mongoDialect.js +146 -104
- package/dist/mongo/mongodbQuerier.d.ts +10 -17
- package/dist/mongo/mongodbQuerier.js +31 -106
- package/dist/mssql/mssqlDialect.d.ts +16 -0
- package/dist/mssql/mssqlDialect.js +26 -4
- package/dist/mysql/mysqlDialect.d.ts +2 -0
- package/dist/mysql/mysqlDialect.js +4 -0
- package/dist/querier/abstractQuerier.d.ts +20 -36
- package/dist/querier/abstractQuerier.js +35 -129
- package/dist/querier/abstractQuerierPool.d.ts +2 -2
- package/dist/querier/abstractSqlQuerier.d.ts +4 -17
- package/dist/querier/abstractSqlQuerier.js +40 -50
- package/dist/schema/canonicalType.js +4 -4
- package/dist/schema/indexDifferences.js +4 -4
- package/dist/schema/schemaASTBuilder.d.ts +3 -3
- package/dist/schema/schemaASTBuilder.js +32 -3
- package/dist/schema/schemaASTDiffer.js +5 -5
- package/dist/sqlite/sqliteDialect.d.ts +20 -1
- package/dist/sqlite/sqliteDialect.js +38 -7
- package/dist/turso/tursoDialect.d.ts +2 -0
- package/dist/turso/tursoDialect.js +2 -0
- package/dist/type/config.d.ts +3 -3
- package/dist/type/dialect.d.ts +4 -5
- package/dist/type/entity.d.ts +110 -69
- package/dist/type/migration.d.ts +7 -7
- package/dist/type/migratorDialect.d.ts +4 -0
- package/dist/type/querier.d.ts +6 -6
- package/dist/type/querierPool.d.ts +2 -2
- package/dist/type/query.d.ts +41 -72
- package/dist/type/query.js +10 -5
- package/dist/type/queryAggregate.d.ts +43 -34
- package/dist/type/queryAggregate.js +1 -1
- package/dist/type/queryWhere.d.ts +12 -9
- package/dist/type/universalQuerier.d.ts +4 -4
- package/dist/util/dialect.util.d.ts +4 -4
- package/dist/util/dialect.util.js +24 -15
- package/dist/util/field.util.d.ts +5 -0
- package/dist/util/field.util.js +19 -0
- package/dist/util/object.util.d.ts +2 -0
- package/dist/util/object.util.js +4 -0
- package/dist/util/relationQuery.util.d.ts +12 -65
- package/dist/util/relationQuery.util.js +27 -81
- package/dist/util/rowKey.util.d.ts +1 -11
- package/dist/util/rowKey.util.js +1 -13
- package/package.json +1 -1
- package/dist/querier/relationCount.d.ts +0 -16
- package/dist/querier/relationCount.js +0 -121
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES,
|
|
2
|
-
import { getKeys, someKey } from './object.util.js';
|
|
1
|
+
import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUERY_STATEMENT_CLAUSES, } from '../type/query.js';
|
|
2
|
+
import { getKeys, isRecord, someKey } from './object.util.js';
|
|
3
3
|
/** What a query populating nothing requests, shared: most reads populate nothing, and ask on every one. */
|
|
4
4
|
const NOTHING_REQUESTED = Object.freeze({
|
|
5
5
|
requestedKeys: Object.freeze([]),
|
|
@@ -40,86 +40,19 @@ export function parentJoins(relOpts, parentKeyCount) {
|
|
|
40
40
|
export function targetKeyColumns(relOpts, parentKeyCount) {
|
|
41
41
|
return relOpts.references.slice(parentKeyCount).map(({ local }) => local);
|
|
42
42
|
}
|
|
43
|
-
/** `{ joined column: true }`: the projection or grouping that keeps a parent's key on the rows read. */
|
|
44
|
-
export function joinedColumns(joins) {
|
|
45
|
-
return Object.fromEntries(keyColumns(joins, 'joined').map((column) => [column, true]));
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* One side's columns: `'parent'` for the columns a parent is keyed by, `'joined'` for the ones a
|
|
49
|
-
* child or tally row carries that key in. Matching the two halves means agreeing on every column, so
|
|
50
|
-
* each side is read through `joins` rather than through the parent's own key list - the same set only
|
|
51
|
-
* for a to-many, and silently a different one otherwise. `keyof ParentJoin` is what keeps the two
|
|
52
|
-
* sides from being spelled apart.
|
|
53
|
-
*
|
|
54
|
-
* Lifted out of `joins` once per relation, not once per row: {@link rowKey} takes the list and reads
|
|
55
|
-
* each row itself, so a page of children costs one key each and nothing else.
|
|
56
|
-
*/
|
|
57
|
-
export function keyColumns(joins, side) {
|
|
58
|
-
return joins.map((join) => join[side]);
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* `{ joined column: every parent's value for it }`, the filter that fetches a whole page of parents'
|
|
62
|
-
* children in one statement.
|
|
63
|
-
*
|
|
64
|
-
* A composite over-selects, because the lists are independent and a pairing no parent has can still
|
|
65
|
-
* match. Regrouping the rows keys on every column, so those rows find no parent and are dropped -
|
|
66
|
-
* cheaper than the row-value comparison no engine spells the same way.
|
|
67
|
-
*/
|
|
68
|
-
export function parentsIn(joins, parents) {
|
|
69
|
-
return Object.fromEntries(joins.map(({ parent, joined }) => [joined, parents.map((it) => read(it, parent))]));
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Whether a to-many's own query asks for a share *per parent* rather than a slice of the whole page.
|
|
73
|
-
* Only `$limit`/`$skip` do: without one, a single flat statement over an `IN (...)` list is both
|
|
74
|
-
* correct and cheaper.
|
|
75
|
-
*/
|
|
76
|
-
export function isBoundedPerParent(query) {
|
|
77
|
-
return query.$limit !== undefined || query.$skip !== undefined;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* The `$where` naming exactly one parent's children: every joined column equal to that parent's value.
|
|
81
|
-
* What a per-parent bounded read filters each of its branches by, and the composite half of
|
|
82
|
-
* {@link childrenOf}.
|
|
83
|
-
*/
|
|
84
|
-
function childOf(joins, parent) {
|
|
85
|
-
return Object.fromEntries(joins.map(({ parent: key, joined }) => [joined, read(parent, key)]));
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* `query` narrowed to one parent's children: what a single branch of a bounded per-parent read asks
|
|
89
|
-
* for. Shared by the backends so how the parent's filter merges into the relation's own is decided
|
|
90
|
-
* once - both spelled it out, and a rule that ever needs more than a spread would have to change twice.
|
|
91
|
-
*/
|
|
92
|
-
export function queryChildrenOf(query, joins, parent) {
|
|
93
|
-
return queryNarrowedTo(query, childOf(joins, parent));
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* `query` narrowed to the children of a whole page of parents, which is the flat read a relation with
|
|
97
|
-
* no share of its own takes. Over-selects on a composite key exactly as {@link parentsIn} does.
|
|
98
|
-
*/
|
|
99
|
-
export function queryChildrenOfAll(query, joins, parents) {
|
|
100
|
-
return queryNarrowedTo(query, parentsIn(joins, parents));
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* `query` with `filter` merged into its own `$where`: the one rule for narrowing a relation's query to
|
|
104
|
-
* the parents it is being read for, whether the filter names their keys as values or, for a correlated
|
|
105
|
-
* shape, as a reference to a row source.
|
|
106
|
-
*/
|
|
107
|
-
export function queryNarrowedTo(query, filter) {
|
|
108
|
-
return { ...query, $where: { ...query.$where, ...filter } };
|
|
109
|
-
}
|
|
110
43
|
/**
|
|
111
44
|
* The `$where` naming exactly the children of the rows `parentIds` identifies: an `IN` over the one
|
|
112
|
-
* column a single key contributes, an OR of key maps for several
|
|
113
|
-
*
|
|
114
|
-
* Exact, unlike {@link parentsIn}: a read absorbs over-selection by regrouping its rows, and a write
|
|
115
|
-
* has nothing to regroup - a pairing no parent has would delete a child of a parent that survives.
|
|
45
|
+
* column a single key contributes, an OR of whole key maps for several - lists of each column apart
|
|
46
|
+
* would pair values no parent has, and a delete would take a child of a parent that survives.
|
|
116
47
|
*/
|
|
117
48
|
export function childrenOf(joins, parentIds) {
|
|
118
49
|
const [first] = joins;
|
|
119
50
|
if (joins.length === 1) {
|
|
120
51
|
return { [first.joined]: parentIds };
|
|
121
52
|
}
|
|
122
|
-
return {
|
|
53
|
+
return {
|
|
54
|
+
$or: parentIds.map((id) => Object.fromEntries(joins.map(({ parent, joined }) => [joined, read(id, parent)]))),
|
|
55
|
+
};
|
|
123
56
|
}
|
|
124
57
|
function read(row, key) {
|
|
125
58
|
return row[key];
|
|
@@ -178,8 +111,24 @@ export function populatesRelations(meta, populate) {
|
|
|
178
111
|
return false;
|
|
179
112
|
return someKey(populate, (key) => !!populate[key] && key in meta.relations);
|
|
180
113
|
}
|
|
181
|
-
/**
|
|
182
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Each relation a `$count` tallies, and the filter narrowing what it counts: the target's, whose type
|
|
116
|
+
* only the metadata knows this far down, as for a relation filter reaching the same subquery.
|
|
117
|
+
*/
|
|
118
|
+
export function countedRelations(meta, counts) {
|
|
119
|
+
if (!counts) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
return getKeys(counts).flatMap((relKey) => {
|
|
123
|
+
const count = counts[relKey];
|
|
124
|
+
const relation = meta.relations[relKey];
|
|
125
|
+
if (!count || !relation) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const where = typeof count === 'object' ? count.$where : undefined;
|
|
129
|
+
return [{ relKey, relation, where: where ?? {} }];
|
|
130
|
+
});
|
|
131
|
+
}
|
|
183
132
|
// Taken from the clause groups declared beside `Query` itself, so a renamed clause fails to compile
|
|
184
133
|
// here instead of quietly narrowing what a relation query accepts. `$required` is the one key that
|
|
185
134
|
// is not a `Query` clause at all - it says how the relation joins, not what it selects.
|
|
@@ -198,7 +147,7 @@ export function parseRelationQueryValue(value) {
|
|
|
198
147
|
// Caught before the shape check so the message names the key, rather than reporting the whole
|
|
199
148
|
// object as an unrecognized relation query value.
|
|
200
149
|
if (isRecord(value)) {
|
|
201
|
-
const statementOnly =
|
|
150
|
+
const statementOnly = QUERY_STATEMENT_CLAUSES.find((clause) => clause in value);
|
|
202
151
|
if (statementOnly) {
|
|
203
152
|
throw new TypeError(`'${statementOnly}' applies to the whole statement, not to a populated relation. Move it to the top level of the query.`);
|
|
204
153
|
}
|
|
@@ -227,9 +176,6 @@ export function forEachRequestedRelation(meta, populate, fn) {
|
|
|
227
176
|
fn(relKey, populate?.[relKey]);
|
|
228
177
|
}
|
|
229
178
|
}
|
|
230
|
-
function isRecord(value) {
|
|
231
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
232
|
-
}
|
|
233
179
|
function isBooleanLikeValue(value) {
|
|
234
180
|
return value === true || value === false || value === 0 || value === 1;
|
|
235
181
|
}
|
|
@@ -243,7 +189,7 @@ function isValidRelationQueryShape(query) {
|
|
|
243
189
|
if (RELATION_QUERY_BOOLEAN_KEYS.has(key) && !isBooleanLikeValue(value)) {
|
|
244
190
|
return false;
|
|
245
191
|
}
|
|
246
|
-
if (RELATION_QUERY_OBJECT_KEYS.has(key) && !isRecord(value)) {
|
|
192
|
+
if (RELATION_QUERY_OBJECT_KEYS.has(key) && !isRecord(value) && !(key === '$select' && Array.isArray(value))) {
|
|
247
193
|
return false;
|
|
248
194
|
}
|
|
249
195
|
if (RELATION_QUERY_NUMBER_KEYS.has(key) && (typeof value !== 'number' || !Number.isFinite(value))) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A row's key as a string, for matching rows to each other
|
|
2
|
+
* A row's key as a string, for matching rows to each other.
|
|
3
3
|
*
|
|
4
4
|
* Reads the columns off the row rather than taking their values, because every caller matches a
|
|
5
5
|
* whole page of rows against one fixed column list: taking an array would make each of them build
|
|
@@ -11,13 +11,3 @@
|
|
|
11
11
|
* key are not treated as one row.
|
|
12
12
|
*/
|
|
13
13
|
export declare function rowKey(row: unknown, columns: readonly string[]): string;
|
|
14
|
-
/**
|
|
15
|
-
* A lookup keyed by data rather than by a name this code chose, so a key that spells `__proto__` or
|
|
16
|
-
* `constructor` is an ordinary entry instead of the prototype: on `{}` those threw when a bucket was
|
|
17
|
-
* pushed to, and a `_count` tally under one silently read back as an object. Cheaper than a `Map`
|
|
18
|
-
* here, and faster than `{}`, which walks the prototype chain on every miss.
|
|
19
|
-
*
|
|
20
|
-
* It carries none of `Object.prototype`, which no type can say: index it and spread it, but calling
|
|
21
|
-
* `hasOwnProperty` on one type-checks and throws.
|
|
22
|
-
*/
|
|
23
|
-
export declare function dataKeyed<V>(): Record<string, V>;
|
package/dist/util/rowKey.util.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Separates the parts of a composite key: a unit separator, which no column value carries. */
|
|
2
2
|
const KEY_SEPARATOR = '\u001f';
|
|
3
3
|
/**
|
|
4
|
-
* A row's key as a string, for matching rows to each other
|
|
4
|
+
* A row's key as a string, for matching rows to each other.
|
|
5
5
|
*
|
|
6
6
|
* Reads the columns off the row rather than taking their values, because every caller matches a
|
|
7
7
|
* whole page of rows against one fixed column list: taking an array would make each of them build
|
|
@@ -23,18 +23,6 @@ export function rowKey(row, columns) {
|
|
|
23
23
|
}
|
|
24
24
|
return key;
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
27
|
-
* A lookup keyed by data rather than by a name this code chose, so a key that spells `__proto__` or
|
|
28
|
-
* `constructor` is an ordinary entry instead of the prototype: on `{}` those threw when a bucket was
|
|
29
|
-
* pushed to, and a `_count` tally under one silently read back as an object. Cheaper than a `Map`
|
|
30
|
-
* here, and faster than `{}`, which walks the prototype chain on every miss.
|
|
31
|
-
*
|
|
32
|
-
* It carries none of `Object.prototype`, which no type can say: index it and spread it, but calling
|
|
33
|
-
* `hasOwnProperty` on one type-checks and throws.
|
|
34
|
-
*/
|
|
35
|
-
export function dataKeyed() {
|
|
36
|
-
return Object.create(null);
|
|
37
|
-
}
|
|
38
26
|
function keyPart(value) {
|
|
39
27
|
if (value instanceof Date) {
|
|
40
28
|
return value.toISOString();
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
4
|
"description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.58.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|
|
@@ -1,16 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
2
|
-
import { getMeta, soleIdOf } from '../entity/index.js';
|
|
3
|
-
import { COUNT_RESULT_KEY } from '../type/index.js';
|
|
4
|
-
import { asSelectMap, dataKeyed, getKeys, joinedColumns, keyColumns, parentJoins, parentsIn, rowKey, targetKeyColumns, } from '../util/index.js';
|
|
5
|
-
/**
|
|
6
|
-
* A `$count` groups its tallies by the parent's id, so the id has to outlive the projection - the
|
|
7
|
-
* same reason populating a relation keeps it. A whitelisting `$select` gains the key and an
|
|
8
|
-
* `$exclude` loses it; the raw-array `$select` form has nothing to augment, so it is refused.
|
|
9
|
-
*/
|
|
10
|
-
export function withIdForCounts(entity, q) {
|
|
11
|
-
const meta = getMeta(entity);
|
|
12
|
-
if (!q.$count) {
|
|
13
|
-
return q;
|
|
14
|
-
}
|
|
15
|
-
const { ids } = meta;
|
|
16
|
-
const named = ids.join(', ');
|
|
17
|
-
if (q.$distinct) {
|
|
18
|
-
// The keys would have to join the projection for the tallies to group by, and that is the set
|
|
19
|
-
// `$distinct` collapses on: adding them silently stops the grouping collapsing anything at all.
|
|
20
|
-
throw new TypeError(`$count cannot be combined with $distinct: the tallies group by each row's '${named}', which the grouping does not keep.`);
|
|
21
|
-
}
|
|
22
|
-
if (Array.isArray(q.$select)) {
|
|
23
|
-
// Nothing to add the keys to, and without them every tally would be looked up by `undefined` and
|
|
24
|
-
// come back zero. Refused rather than answered wrong, the way an unorderable clause is.
|
|
25
|
-
throw new TypeError(`$count needs the '${named}' of each row to group its tallies by, which a raw $select cannot carry. Use a $select map, or drop the $count.`);
|
|
26
|
-
}
|
|
27
|
-
const next = { ...q };
|
|
28
|
-
const select = asSelectMap(q.$select);
|
|
29
|
-
const missing = select ? ids.filter((key) => !select[key]) : [];
|
|
30
|
-
if (missing.length) {
|
|
31
|
-
next.$select = { ...select, ...Object.fromEntries(missing.map((key) => [key, true])) };
|
|
32
|
-
}
|
|
33
|
-
const excluded = ids.filter((key) => q.$exclude?.[key]);
|
|
34
|
-
if (excluded.length) {
|
|
35
|
-
const kept = { ...q.$exclude };
|
|
36
|
-
for (const key of excluded) {
|
|
37
|
-
delete kept[key];
|
|
38
|
-
}
|
|
39
|
-
next.$exclude = kept;
|
|
40
|
-
}
|
|
41
|
-
return next;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* How many rows each counted relation holds, under `_count` on every parent. One grouped aggregate
|
|
45
|
-
* per relation over every parent at once - the batching a populated to-many already gets - so the
|
|
46
|
-
* cost stays flat in the number of rows the read returned rather than one statement per row.
|
|
47
|
-
*/
|
|
48
|
-
export async function fillRelationCounts(querier, entity, payload, count) {
|
|
49
|
-
if (!payload.length || !count) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const meta = getMeta(entity);
|
|
53
|
-
// The tallies come back keyed by the columns *this relation* joins from, so those columns are kept
|
|
54
|
-
// beside them: reading the parent through `meta.ids` instead matches only where the two coincide,
|
|
55
|
-
// which is a to-many and nothing else.
|
|
56
|
-
const counted = new Map();
|
|
57
|
-
for (const relKey of getKeys(count)) {
|
|
58
|
-
const value = count[relKey];
|
|
59
|
-
const relOpts = meta.relations[relKey];
|
|
60
|
-
if (!value || !relOpts) {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
const where = typeof value === 'object' ? value.$where : undefined;
|
|
64
|
-
const joins = parentJoins(relOpts, meta.ids.length);
|
|
65
|
-
counted.set(relKey, {
|
|
66
|
-
parentKeys: keyColumns(joins, 'parent'),
|
|
67
|
-
byParent: await countPerParent(querier, relOpts, joins, payload, where),
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
for (const parent of payload) {
|
|
71
|
-
// A plain object, unlike the tallies below: this one is keyed by relation names the entity
|
|
72
|
-
// declares, not by data, and it is handed to the caller - who would meet a null prototype.
|
|
73
|
-
const row = {};
|
|
74
|
-
for (const [relKey, { parentKeys, byParent }] of counted) {
|
|
75
|
-
// A parent the grouped result has no row for matched nothing, which is a zero rather than a
|
|
76
|
-
// gap: `_count` names what the caller asked to count, so every key it asked for is present.
|
|
77
|
-
row[relKey] = byParent[rowKey(parent, parentKeys)] ?? 0;
|
|
78
|
-
}
|
|
79
|
-
parent[COUNT_RESULT_KEY] = row;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
async function countPerParent(querier, relOpts, joins, parents, where) {
|
|
83
|
-
const through = relOpts.through;
|
|
84
|
-
if (through) {
|
|
85
|
-
return countThroughPerParent(querier, relOpts, through(), joins, parents, where);
|
|
86
|
-
}
|
|
87
|
-
return groupedCount(querier, relOpts.entity(), joins, {
|
|
88
|
-
...where,
|
|
89
|
-
...parentsIn(joins, parents),
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* A many-to-many counts its junction rows, one per pairing. A filter names the target's columns,
|
|
94
|
-
* which the junction does not have, so the matching targets are resolved first and the junction
|
|
95
|
-
* counted against them - the one shape here that costs a second statement, and only when filtered.
|
|
96
|
-
*/
|
|
97
|
-
async function countThroughPerParent(querier, relOpts, throughEntity, joins, parents, where) {
|
|
98
|
-
const throughWhere = parentsIn(joins, parents);
|
|
99
|
-
if (where) {
|
|
100
|
-
const target = relOpts.entity();
|
|
101
|
-
const targetId = soleIdOf(getMeta(target), 'a many-to-many target');
|
|
102
|
-
const targets = await querier.findMany(target, { $select: { [targetId]: true }, $where: where });
|
|
103
|
-
const [targetColumn] = targetKeyColumns(relOpts, joins.length);
|
|
104
|
-
throughWhere[targetColumn] = targets.map((it) => it[targetId]);
|
|
105
|
-
}
|
|
106
|
-
return groupedCount(querier, throughEntity, joins, throughWhere);
|
|
107
|
-
}
|
|
108
|
-
/** `SELECT <keys>, COUNT(*) ... GROUP BY <keys>`, as a lookup from parent key to tally. */
|
|
109
|
-
async function groupedCount(querier, entity, joins, where) {
|
|
110
|
-
const $agg = { [COUNT_ALIAS]: { $count: '*' } };
|
|
111
|
-
const $group = joinedColumns(joins);
|
|
112
|
-
const rows = await querier.aggregate(entity, { $group, $agg, $where: where });
|
|
113
|
-
const byParent = dataKeyed();
|
|
114
|
-
// Keyed by every joined column, which is how a tally finds the one parent whose whole key it
|
|
115
|
-
// matches - and how the rows an over-selecting `IN` brought back find no parent at all.
|
|
116
|
-
const joinedKeys = keyColumns(joins, 'joined');
|
|
117
|
-
for (const row of rows) {
|
|
118
|
-
byParent[rowKey(row, joinedKeys)] = Number(row[COUNT_ALIAS]);
|
|
119
|
-
}
|
|
120
|
-
return byParent;
|
|
121
|
-
}
|