uql-orm 0.56.0 → 0.57.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 +1 -1
- 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 +195 -32
- package/dist/dialect/abstractSqlDialect.js +406 -199
- 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 +30 -2
- package/dist/dialect/mysqlLikeSqlDialect.js +56 -4
- 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 +25 -11
- package/dist/dialect/vectorSqlDialect.d.ts +2 -2
- package/dist/dialect/vectorSqlDialect.js +2 -3
- package/dist/entity/metadata/definition.js +3 -3
- 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/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/migrator.d.ts +2 -2
- package/dist/migrate/schemaGenerator.d.ts +3 -3
- package/dist/migrate/schemaGenerator.js +5 -7
- package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
- package/dist/mongo/mongoDialect.d.ts +30 -17
- package/dist/mongo/mongoDialect.js +143 -101
- package/dist/mongo/mongodbQuerier.d.ts +10 -17
- package/dist/mongo/mongodbQuerier.js +31 -105
- 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 +34 -44
- package/dist/schema/canonicalType.js +4 -4
- package/dist/schema/indexDifferences.js +4 -4
- package/dist/schema/schemaASTBuilder.js +31 -2
- 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 +2 -2
- package/dist/type/dialect.d.ts +4 -5
- package/dist/type/entity.d.ts +2 -1
- package/dist/type/migratorDialect.d.ts +4 -0
- package/dist/type/querier.d.ts +6 -6
- package/dist/type/query.d.ts +25 -48
- package/dist/type/query.js +10 -5
- package/dist/type/queryAggregate.d.ts +10 -10
- package/dist/type/queryAggregate.js +1 -1
- package/dist/type/universalQuerier.d.ts +4 -4
- package/dist/util/dialect.util.d.ts +1 -1
- 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
|
@@ -2,7 +2,7 @@ import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
|
2
2
|
import { hasRequiredJoin } from '../dialect/queryJoins.js';
|
|
3
3
|
import { fieldOf, getMeta, idOf, namesKey, soleIdOf } from '../entity/index.js';
|
|
4
4
|
import { AbstractQuerier, enrichError } from '../querier/index.js';
|
|
5
|
-
import { clone, getKeys,
|
|
5
|
+
import { clone, getKeys, getSoftDeleteValue, hasKeys, idOnlyQuery, isPagedQuery, populatesRelations, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
6
6
|
/**
|
|
7
7
|
* `$limit: 0` asks for no rows, the way it does on every SQL dialect - but MongoDB reads `limit(0)`
|
|
8
8
|
* as *unlimited*, so a read that passed it straight to the driver came back with the whole
|
|
@@ -11,12 +11,6 @@ import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys,
|
|
|
11
11
|
function asksForNoRows(q) {
|
|
12
12
|
return q.$limit === 0;
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
15
|
-
* What MongoDB accepts in one pipeline. Bisected against a real server: 1000 top-level stages are
|
|
16
|
-
* accepted and 1001 refused (`Pipeline length must be no longer than 1000 stages`), and a
|
|
17
|
-
* `$unionWith`'s own sub-pipeline stages do not count toward it.
|
|
18
|
-
*/
|
|
19
|
-
const MAX_PIPELINE_STAGES = 1000;
|
|
20
14
|
export class MongodbQuerier extends AbstractQuerier {
|
|
21
15
|
dialect;
|
|
22
16
|
conn;
|
|
@@ -34,109 +28,20 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
34
28
|
});
|
|
35
29
|
}
|
|
36
30
|
async internalFindMany(entity, q, opts) {
|
|
37
|
-
this.dialect.assertNoLock(q);
|
|
38
31
|
if (asksForNoRows(q)) {
|
|
39
32
|
return [];
|
|
40
33
|
}
|
|
41
34
|
return this.timed('internalFindMany', undefined, async () => {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
let documents;
|
|
45
|
-
if (vectorSort) {
|
|
46
|
-
const pipeline = this.buildVectorPipeline(entity, q, vectorSort, opts);
|
|
47
|
-
documents = await this.runPipeline(entity, meta, pipeline);
|
|
48
|
-
// to-many relations need their own query, exactly as in the non-vector path
|
|
49
|
-
await this.fillToManyRelations(entity, documents, q.$populate);
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
// A relation condition needs `$lookup`, so it forces the aggregation path just like populating
|
|
53
|
-
// one does - and so does ordering by a relation, which reads what a lookup produced, and
|
|
54
|
-
// `$distinct`, which is a `$group`. A plain `find` cursor can express none of the four.
|
|
55
|
-
if (q.$distinct ||
|
|
56
|
-
populatesRelations(meta, q.$populate) ||
|
|
57
|
-
this.dialect.constrainsRelations(entity, q.$where) ||
|
|
58
|
-
this.dialect.sortsRelations(entity, q.$sort)) {
|
|
59
|
-
const pipeline = this.dialect.aggregationPipeline(entity, q, opts);
|
|
60
|
-
documents = await this.runPipeline(entity, meta, pipeline);
|
|
61
|
-
await this.fillToManyRelations(entity, documents, q.$populate);
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
const cursor = this.buildFindCursor(entity, q, opts);
|
|
65
|
-
documents = await this.execute(() => cursor.toArray());
|
|
66
|
-
documents = this.dialect.normalizeIds(meta, documents);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return documents;
|
|
35
|
+
const cursor = this.readCursor(entity, q, opts);
|
|
36
|
+
return this.dialect.normalizeIds(getMeta(entity), await this.execute(() => cursor.toArray()));
|
|
70
37
|
});
|
|
71
38
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Every parent's own bounded page. One `$unionWith` per parent after the first, so the whole page is
|
|
74
|
-
* one round trip - measured ~6x faster than a query each (11.0 ms -> 1.9 ms at 50 parents, 87.5 ms
|
|
75
|
-
* -> 14.1 ms at 500), because `execute` serializes on the session and a query each is N round trips
|
|
76
|
-
* rather than N concurrent ones.
|
|
77
|
-
*
|
|
78
|
-
* Both arms return documents with their own relations already filled, so this only chooses between
|
|
79
|
-
* them: leaving that to the caller once meant the arm that fills its own did it twice.
|
|
80
|
-
* [The design](../../../../architecture/populate-limits.md).
|
|
81
|
-
*/
|
|
82
|
-
async internalFindManyPerParent(entity, q, { joins, parents }) {
|
|
83
|
-
const queries = parents.map((parent) => queryChildrenOf(q, joins, parent));
|
|
84
|
-
// A vector sort is not a degraded fallback here, it is the only expressible form: `$vectorSearch`
|
|
85
|
-
// has to be the first stage of a pipeline, so it cannot be one of N `$unionWith` branches. Read a
|
|
86
|
-
// parent at a time it stays correct, because `buildVectorSearchStage` passes the query's `$where`
|
|
87
|
-
// - which carries this parent's key - into the search as its filter, so each parent gets its own
|
|
88
|
-
// nearest rather than a share of the collection's.
|
|
89
|
-
if (this.dialect.extractVectorSort(q.$sort)) {
|
|
90
|
-
return this.readEachInTurn(entity, queries);
|
|
91
|
-
}
|
|
92
|
-
const pipelines = queries.map((it) => this.dialect.aggregationPipeline(entity, it));
|
|
93
|
-
// Counted, not estimated: the leading branch's own length grows with every `$lookup` a populate
|
|
94
|
-
// adds, so a fixed parent budget would let a richer query overflow at the server instead.
|
|
95
|
-
const stages = pipelines[0].length + pipelines.length - 1;
|
|
96
|
-
return stages > MAX_PIPELINE_STAGES
|
|
97
|
-
? this.readEachInTurn(entity, queries)
|
|
98
|
-
: this.readInOnePipeline(entity, q, pipelines);
|
|
99
|
-
}
|
|
100
|
-
/** Every parent's page as one `$unionWith` pipeline. */
|
|
101
|
-
async readInOnePipeline(entity, q, pipelines) {
|
|
102
|
-
const meta = getMeta(entity);
|
|
103
|
-
const [first, ...rest] = pipelines;
|
|
104
|
-
const documents = await this.runPipeline(entity, meta, [
|
|
105
|
-
...first,
|
|
106
|
-
...rest.map((pipeline) => ({ $unionWith: { coll: meta.name, pipeline } })),
|
|
107
|
-
]);
|
|
108
|
-
await this.fillToManyRelations(entity, documents, q.$populate);
|
|
109
|
-
return documents;
|
|
110
|
-
}
|
|
111
|
-
/** A query each, for what one pipeline cannot carry. `internalFindMany` fills its own relations. */
|
|
112
|
-
async readEachInTurn(entity, queries) {
|
|
113
|
-
const documents = [];
|
|
114
|
-
for (const query of queries) {
|
|
115
|
-
documents.push(...(await this.internalFindMany(entity, query)));
|
|
116
|
-
}
|
|
117
|
-
return documents;
|
|
118
|
-
}
|
|
119
39
|
async *internalFindManyStream(entity, q, opts) {
|
|
120
40
|
if (asksForNoRows(q)) {
|
|
121
41
|
return;
|
|
122
42
|
}
|
|
123
43
|
const meta = getMeta(entity);
|
|
124
|
-
const
|
|
125
|
-
if (joinableKeys.length || toManyKeys.length) {
|
|
126
|
-
const parts = [];
|
|
127
|
-
if (joinableKeys.length)
|
|
128
|
-
parts.push(`joinable: ${joinableKeys.join(', ')}`);
|
|
129
|
-
if (toManyKeys.length)
|
|
130
|
-
parts.push(`toMany: ${toManyKeys.join(', ')}`);
|
|
131
|
-
throw new TypeError(`findManyStream does not load relations on MongoDB (${parts.join('; ')}). Use findMany with $populate (or legacy relation keys in $select) so aggregation and fill logic can run.`);
|
|
132
|
-
}
|
|
133
|
-
// An ordering that names a relation reads a field only a `$lookup` produces, and a stream has no
|
|
134
|
-
// pipeline to add one: MongoDB ranks every document equal and hands back natural order. `findMany`
|
|
135
|
-
// takes the aggregation path for exactly this case, so it is the one that can honor the clause.
|
|
136
|
-
if (this.dialect.sortsRelations(entity, q.$sort)) {
|
|
137
|
-
throw new TypeError('findManyStream does not order by a relation on MongoDB. Use findMany, whose aggregation pipeline adds the $lookup the ordering reads.');
|
|
138
|
-
}
|
|
139
|
-
const cursor = this.buildFindCursor(entity, q, opts);
|
|
44
|
+
const cursor = this.readCursor(entity, q, opts);
|
|
140
45
|
try {
|
|
141
46
|
for await (const doc of cursor) {
|
|
142
47
|
const [normalized] = this.dialect.normalizeIds(meta, [doc]);
|
|
@@ -147,6 +52,32 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
147
52
|
throw enrichError(err, this.logger, 'internalFindManyStream');
|
|
148
53
|
}
|
|
149
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* The cursor a read runs on: the aggregation pipeline for a clause only a stage can express - a
|
|
57
|
+
* lookup, a grouping, a vector search - and the plain `find` cursor for everything else. One routing
|
|
58
|
+
* for a read and a stream alike, so both load the same relations.
|
|
59
|
+
*/
|
|
60
|
+
readCursor(entity, q, opts) {
|
|
61
|
+
this.dialect.assertNoLock(q);
|
|
62
|
+
const vectorSort = this.dialect.extractVectorSort(q.$sort);
|
|
63
|
+
const pipeline = vectorSort
|
|
64
|
+
? this.buildVectorPipeline(entity, q, vectorSort, opts)
|
|
65
|
+
: this.readsThroughPipeline(entity, q) && this.dialect.aggregationPipeline(entity, q, opts);
|
|
66
|
+
return pipeline
|
|
67
|
+
? this.collection(entity).aggregate(pipeline, { session: this.session })
|
|
68
|
+
: this.buildFindCursor(entity, q, opts);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether a read needs stages a `find` cursor cannot express: a lookup to populate, count, filter
|
|
72
|
+
* or order by a relation, and the grouping `$distinct` is.
|
|
73
|
+
*/
|
|
74
|
+
readsThroughPipeline(entity, q) {
|
|
75
|
+
return (!!q.$distinct ||
|
|
76
|
+
hasKeys(q.$count) ||
|
|
77
|
+
populatesRelations(getMeta(entity), q.$populate) ||
|
|
78
|
+
this.dialect.constrainsRelations(entity, q.$where) ||
|
|
79
|
+
this.dialect.sortsRelations(entity, q.$sort));
|
|
80
|
+
}
|
|
150
81
|
buildScalarProjection(entity, q) {
|
|
151
82
|
return this.dialect.select(entity, q.$select, q.$exclude);
|
|
152
83
|
}
|
|
@@ -174,11 +105,6 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
174
105
|
}
|
|
175
106
|
return cursor;
|
|
176
107
|
}
|
|
177
|
-
/** Execute an aggregation pipeline and normalize `_id` → `id`. */
|
|
178
|
-
async runPipeline(entity, meta, pipeline) {
|
|
179
|
-
const documents = await this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
|
|
180
|
-
return this.dialect.normalizeIds(meta, documents);
|
|
181
|
-
}
|
|
182
108
|
/**
|
|
183
109
|
* Build an aggregation pipeline for vector similarity search.
|
|
184
110
|
* `$vectorSearch` is always the first stage; `$where` is merged into its `filter`.
|
|
@@ -191,7 +117,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
191
117
|
// that follow treat it like any other - and a query with no projection keeps its own columns.
|
|
192
118
|
...(scoreAlias ? [{ $addFields: { [scoreAlias]: { $meta: 'vectorSearchScore' } } }] : []),
|
|
193
119
|
// `$vectorSearch` has already applied `$limit`, so the pager is its own.
|
|
194
|
-
...this.dialect.readStages(entity, q,
|
|
120
|
+
...this.dialect.readStages(entity, q, {
|
|
195
121
|
sort: this.dialect.sort(entity, vectorSort.regularSort, q.$populate),
|
|
196
122
|
project: scoreAlias ? { [scoreAlias]: 1 } : undefined,
|
|
197
123
|
}),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RelationRows } from '../dialect/abstractSqlDialect.js';
|
|
1
2
|
import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
|
|
2
3
|
import type { DialectFeatures, EntityMeta, FieldOptions, InsertIdSource, Query, QueryContext, QueryOptions, QueryPager, QuerySizeComparisonOps, Type, VectorDistance, VectorMetric } from '../type/index.js';
|
|
3
4
|
/**
|
|
@@ -65,6 +66,21 @@ export declare class MsSqlDialect extends MergeSqlDialect {
|
|
|
65
66
|
* with room for the sign and the point.
|
|
66
67
|
*/
|
|
67
68
|
protected selectFieldExpr(escapedColumn: string, field: FieldOptions): string;
|
|
69
|
+
/**
|
|
70
|
+
* The rows read as they are, `FOR JSON PATH` making the array. It nests a dotted key and leaves a
|
|
71
|
+
* null out, which is what unflattening a row with a joined column does, so a row with none keeps its
|
|
72
|
+
* nulls. `JSON_QUERY` keeps the array JSON inside a parent's own `FOR JSON`.
|
|
73
|
+
*/
|
|
74
|
+
protected appendRelationArray(ctx: QueryContext, rows: RelationRows): void;
|
|
75
|
+
/**
|
|
76
|
+
* What JSON would round or misread crosses it as text: a number exactly, style 3 keeping a float's
|
|
77
|
+
* 17 digits, bytes as hex, and a date in UTC with its offset, which is how `tedious` reads one.
|
|
78
|
+
*/
|
|
79
|
+
protected readonly carriedFields: {
|
|
80
|
+
numeric: (expr: string) => string;
|
|
81
|
+
blob: (expr: string) => string;
|
|
82
|
+
date: (expr: string) => string;
|
|
83
|
+
};
|
|
68
84
|
/** Named parameters, which `tedious` binds by name rather than by position. */
|
|
69
85
|
placeholder(index: number): string;
|
|
70
86
|
/** `OUTPUT` reads the written row out of the `INSERTED` pseudo-table rather than `RETURNING` it. */
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { relationTermKey } from '../dialect/abstractSqlDialect.js';
|
|
2
|
+
import { COUNT_ALIAS, JSON_ELEM_ALIAS } from '../dialect/aliases.js';
|
|
3
|
+
import { BYTES_PREFIX } from '../dialect/hydrateColumn.js';
|
|
2
4
|
import { jsonPath } from '../dialect/jsonSql.js';
|
|
3
5
|
import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
|
|
4
6
|
import { getMeta } from '../entity/index.js';
|
|
@@ -127,6 +129,26 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
127
129
|
const exactDecimal = field.type === String && fieldOptionsToCanonical(field).category === 'decimal';
|
|
128
130
|
return exactDecimal ? `CONVERT(NVARCHAR(41), ${escapedColumn})` : escapedColumn;
|
|
129
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* The rows read as they are, `FOR JSON PATH` making the array. It nests a dotted key and leaves a
|
|
134
|
+
* null out, which is what unflattening a row with a joined column does, so a row with none keeps its
|
|
135
|
+
* nulls. `JSON_QUERY` keeps the array JSON inside a parent's own `FOR JSON`.
|
|
136
|
+
*/
|
|
137
|
+
appendRelationArray(ctx, rows) {
|
|
138
|
+
const rowsCtx = ctx.createFragment();
|
|
139
|
+
const { terms } = this.read(rowsCtx, rows.entity, rows.query, { alias: rows.alias, json: true }, rows.joins);
|
|
140
|
+
const nulls = terms.some((term) => relationTermKey(term).includes('.')) ? '' : ', INCLUDE_NULL_VALUES';
|
|
141
|
+
ctx.append(`JSON_QUERY(COALESCE((${rowsCtx.sql} FOR JSON PATH${nulls}), '[]'))`);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* What JSON would round or misread crosses it as text: a number exactly, style 3 keeping a float's
|
|
145
|
+
* 17 digits, bytes as hex, and a date in UTC with its offset, which is how `tedious` reads one.
|
|
146
|
+
*/
|
|
147
|
+
carriedFields = {
|
|
148
|
+
numeric: (expr) => `CONVERT(VARCHAR(40), ${expr}, 3)`,
|
|
149
|
+
blob: (expr) => `${this.escape(BYTES_PREFIX)} + CONVERT(VARCHAR(MAX), ${expr}, 2)`,
|
|
150
|
+
date: (expr) => `CONVERT(VARCHAR(33), CAST(${expr} AS DATETIMEOFFSET), 127)`,
|
|
151
|
+
};
|
|
130
152
|
/** Named parameters, which `tedious` binds by name rather than by position. */
|
|
131
153
|
placeholder(index) {
|
|
132
154
|
return `@p${index}`;
|
|
@@ -293,12 +315,12 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
293
315
|
: `JSON_VALUE(${alias}.${this.#elem.value}, ${jsonPath(field)})`;
|
|
294
316
|
}
|
|
295
317
|
jsonAll(ctx, jsonField, value) {
|
|
296
|
-
const alias = ctx.
|
|
318
|
+
const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
|
|
297
319
|
const conditions = value.map((val) => `EXISTS (SELECT 1 FROM OPENJSON(${jsonField}) ${alias} WHERE ${alias}.${this.#elem.value} = ${this.jsonScalarParam(ctx, val)})`);
|
|
298
320
|
return `(${conditions.join(' AND ')})`;
|
|
299
321
|
}
|
|
300
322
|
jsonSize(ctx, jsonField, value) {
|
|
301
|
-
const alias = ctx.
|
|
323
|
+
const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
|
|
302
324
|
return this.buildFragment(ctx, (fragmentCtx) => this.buildSizeComparison(fragmentCtx, () => fragmentCtx.append(`(SELECT COUNT(*) FROM OPENJSON(${jsonField}) ${alias})`), value));
|
|
303
325
|
}
|
|
304
326
|
/** `JSON_MODIFY` takes one path per call, so several keys chain into one expression. */
|
|
@@ -323,7 +345,7 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
323
345
|
* remove-by-value. `JSON_QUERY` is what marks the rebuilt text as JSON rather than a string.
|
|
324
346
|
*/
|
|
325
347
|
jsonPullKey(ctx, expr, escapedCol, key, value) {
|
|
326
|
-
const alias = ctx.
|
|
348
|
+
const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
|
|
327
349
|
const val = `${alias}.${this.#elem.value}`;
|
|
328
350
|
// `OPENJSON` hands back a string element unquoted and a null one as SQL NULL, so each survivor is
|
|
329
351
|
// re-encoded from its reported `type` before the array is put back together - concatenated raw,
|
|
@@ -6,4 +6,6 @@ export declare class MySqlDialect extends MysqlLikeSqlDialect {
|
|
|
6
6
|
* "subject to removal in a future version"; aliasing the inserted row (8.0.19+) is its replacement.
|
|
7
7
|
*/
|
|
8
8
|
protected readonly upsertNewRowAlias = "_uql_new";
|
|
9
|
+
/** A `SET_VAR` hint, which MySQL reads only in a statement's first `SELECT`. */
|
|
10
|
+
protected applySettings(sql: string, settings: readonly string[]): string;
|
|
9
11
|
}
|
|
@@ -7,4 +7,8 @@ export class MySqlDialect extends MysqlLikeSqlDialect {
|
|
|
7
7
|
* "subject to removal in a future version"; aliasing the inserted row (8.0.19+) is its replacement.
|
|
8
8
|
*/
|
|
9
9
|
upsertNewRowAlias = UPSERT_NEW_ROW_ALIAS;
|
|
10
|
+
/** A `SET_VAR` hint, which MySQL reads only in a statement's first `SELECT`. */
|
|
11
|
+
applySettings(sql, settings) {
|
|
12
|
+
return sql.replace('SELECT ', `SELECT /*+ ${settings.map((setting) => `SET_VAR(${setting})`).join(' ')} */ `);
|
|
13
|
+
}
|
|
10
14
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { EntityData, EntityId, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage,
|
|
2
|
-
import { LoggerWrapper
|
|
1
|
+
import type { EntityData, EntityId, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, PrimaryKey, QueryUpdateResult, QueryUpsertOneResult, QueryUpsertManyResult, RelationKey, TransactionOptions, Type, UpdatePayload, WrittenId } from '../type/index.js';
|
|
2
|
+
import { LoggerWrapper } from '../util/index.js';
|
|
3
3
|
/** Base class for all database queriers. */
|
|
4
4
|
export declare abstract class AbstractQuerier implements Querier {
|
|
5
5
|
readonly extra?: ExtraOptions | undefined;
|
|
@@ -44,31 +44,20 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
44
44
|
$entity: Type<E>;
|
|
45
45
|
}, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C>[]>;
|
|
46
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>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C>[]>;
|
|
47
|
-
protected abstract internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
|
|
48
47
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* **SQL:** Joinable relations (e.g. m1 / one-to-one) are still emitted in the streamed SQL; **to-many**
|
|
53
|
-
* relations are not filled (no second query) - requesting them throws a clear `TypeError`.
|
|
54
|
-
*
|
|
55
|
-
* **MongoDB:** Relation loading uses aggregation + follow-up queries in `findMany`; **streams use a plain
|
|
56
|
-
* find cursor**, so any requested relation keys in `$select` / `$populate` throw a `TypeError`.
|
|
57
|
-
*
|
|
58
|
-
* No `afterLoad` hooks on streamed rows.
|
|
48
|
+
* The rows matching `q`, each populated relation and `$count` read with them in the same statement
|
|
49
|
+
* or pipeline. [The design](../../../../architecture/relations-in-one-statement.md).
|
|
59
50
|
*/
|
|
60
|
-
|
|
61
|
-
$entity: Type<E>;
|
|
62
|
-
}, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
|
|
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>(entity: Type<E>, q: QueryStreamProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
|
|
51
|
+
protected abstract internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
|
|
64
52
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* Abstract rather than defaulted: a default would be N queries, which is the N+1 that batched
|
|
69
|
-
* population exists to prevent, and it would be invisible to whichever backend forgot to override.
|
|
53
|
+
* Stream records as an async iterable, in both the entity-as-argument and entity-as-field patterns.
|
|
54
|
+
* Each row streams with its populated relations and counts, read by the same statement or pipeline
|
|
55
|
+
* `findMany` runs. No `afterLoad` hooks on streamed rows.
|
|
70
56
|
*/
|
|
71
|
-
|
|
57
|
+
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, const C extends RelationKey<E> = never>(q: QueryProjected<E, S, V, X, P, C> & {
|
|
58
|
+
$entity: Type<E>;
|
|
59
|
+
}, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P, C>>;
|
|
60
|
+
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, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P, C>>;
|
|
72
61
|
protected abstract internalFindManyStream<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): AsyncIterable<E>;
|
|
73
62
|
/**
|
|
74
63
|
* Find multiple records and return both the records and total count.
|
|
@@ -157,19 +146,6 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
157
146
|
* inserts. A composite is always named. The hooks follow the statement: a named row fires the upsert pair.
|
|
158
147
|
*/
|
|
159
148
|
saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(WrittenId<E> | undefined)[]>;
|
|
160
|
-
protected fillToManyRelations<E>(entity: Type<E>, payload: E[], populate?: QueryPopulate<E>): Promise<void>;
|
|
161
|
-
private fillToManyThroughRelation;
|
|
162
|
-
private fillToManyOneToMany;
|
|
163
|
-
/**
|
|
164
|
-
* The children of a whole page of parents, however the relation asked for them: one bounded branch
|
|
165
|
-
* per parent when it wants a share of its own, otherwise a single flat statement over an `IN (...)`
|
|
166
|
-
* list, which is both correct and cheaper.
|
|
167
|
-
*
|
|
168
|
-
* The one place that decision is made - a one-to-many and the junction of a many-to-many differ in
|
|
169
|
-
* what they query, never in how the page is spread over its parents.
|
|
170
|
-
*/
|
|
171
|
-
private findChildrenOf;
|
|
172
|
-
protected putChildrenInParents<E>(parents: E[], children: RawRow[], joins: readonly ParentJoin[], relKey: keyof E & string): void;
|
|
173
149
|
protected insertRelations<E extends object>(entity: Type<E>, payload: E[]): Promise<void>;
|
|
174
150
|
protected updateRelations<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<void>;
|
|
175
151
|
/**
|
|
@@ -207,6 +183,14 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
207
183
|
transaction<T>(callback: () => Promise<T>, opts?: TransactionOptions): Promise<T>;
|
|
208
184
|
/** Whether anything at all - a global listener or the entity itself - handles `event`. */
|
|
209
185
|
private hasHook;
|
|
186
|
+
/** Whether an `afterLoad` listens on the entity a read returns or on any relation it populated. */
|
|
187
|
+
private listensForLoad;
|
|
188
|
+
/**
|
|
189
|
+
* `afterLoad` for every row a read loaded, a populated relation's before the rows holding them, so a
|
|
190
|
+
* parent's hook sees its children as their own hooks left them. Rows are walked only where a hook
|
|
191
|
+
* listens.
|
|
192
|
+
*/
|
|
193
|
+
private emitLoaded;
|
|
210
194
|
/**
|
|
211
195
|
* The ids of `rows`, read back by the columns an upsert matched them on, for a statement that could
|
|
212
196
|
* not report them in payload order. A row that no read row matches, or that two do, keeps
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { assertSoleId, getMeta, idOf, namesKey, relationOf, soleIdOf } from '../entity/index.js';
|
|
2
|
-
import {
|
|
2
|
+
import { childrenOf, clone, entityName, fillOnFields, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, LoggerWrapper, parentJoins, queryLoggerFor, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
3
|
import { enrichError } from './queryError.js';
|
|
4
|
-
import { fillRelationCounts, withIdForCounts } from './relationCount.js';
|
|
5
4
|
/**
|
|
6
5
|
* Rejects a nullish primary key before it reaches a statement. The by-id methods reduce to
|
|
7
6
|
* `{ $where: { id } }`, and a key compared to `undefined` is *no filter*, so an unchecked one
|
|
@@ -130,14 +129,11 @@ export class AbstractQuerier {
|
|
|
130
129
|
async findMany(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
|
|
131
130
|
const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
|
|
132
131
|
this.validateProjectionQuery(entity, q);
|
|
133
|
-
const founds = await this.internalFindMany(entity,
|
|
132
|
+
const founds = await this.internalFindMany(entity, q, opts);
|
|
134
133
|
// Guarded here rather than only inside: awaiting a call that returns at once still costs every read
|
|
135
|
-
// a promise and a turn of the microtask queue, and most reads
|
|
136
|
-
if (q.$
|
|
137
|
-
await
|
|
138
|
-
}
|
|
139
|
-
if (this.hasHook(entity, 'afterLoad')) {
|
|
140
|
-
await this.emitHook(entity, 'afterLoad', founds);
|
|
134
|
+
// a promise and a turn of the microtask queue, and most reads hook nothing.
|
|
135
|
+
if (this.listensForLoad(entity, q.$populate)) {
|
|
136
|
+
await this.emitLoaded(entity, founds, q.$populate);
|
|
141
137
|
}
|
|
142
138
|
return founds;
|
|
143
139
|
}
|
|
@@ -149,9 +145,10 @@ export class AbstractQuerier {
|
|
|
149
145
|
async findManyAndCount(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
|
|
150
146
|
const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
|
|
151
147
|
this.validateProjectionQuery(entity, q);
|
|
152
|
-
const [founds, count] = await this.internalFindManyAndCount(entity,
|
|
153
|
-
|
|
154
|
-
|
|
148
|
+
const [founds, count] = await this.internalFindManyAndCount(entity, q, opts);
|
|
149
|
+
if (this.listensForLoad(entity, q.$populate)) {
|
|
150
|
+
await this.emitLoaded(entity, founds, q.$populate);
|
|
151
|
+
}
|
|
155
152
|
return [founds, count];
|
|
156
153
|
}
|
|
157
154
|
/**
|
|
@@ -342,123 +339,6 @@ export class AbstractQuerier {
|
|
|
342
339
|
await (toInsert.length && toUpsert.length ? this.transaction(write) : write());
|
|
343
340
|
return ids;
|
|
344
341
|
}
|
|
345
|
-
async fillToManyRelations(entity, payload, populate) {
|
|
346
|
-
if (!payload.length) {
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
const meta = getMeta(entity);
|
|
350
|
-
const relKeys = getRelationRequestSummary(meta, populate).toManyKeys;
|
|
351
|
-
for (const relKey of relKeys) {
|
|
352
|
-
const relOpts = relationOf(meta, relKey);
|
|
353
|
-
const relEntity = relOpts.entity();
|
|
354
|
-
const relationQuery = clone(parseRelationAtKey(relKey, populate).query);
|
|
355
|
-
if (relOpts.through) {
|
|
356
|
-
await this.fillToManyThroughRelation(payload, meta, relKey, relOpts, relationQuery);
|
|
357
|
-
}
|
|
358
|
-
else if (relOpts.cardinality === '1m') {
|
|
359
|
-
await this.fillToManyOneToMany(payload, meta, relKey, relOpts, relationQuery, relEntity);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
async fillToManyThroughRelation(payload, meta, relKey, relOpts, relationQuery) {
|
|
364
|
-
const joins = parentJoins(relOpts, meta.ids.length);
|
|
365
|
-
const [targetColumn] = targetKeyColumns(relOpts, meta.ids.length);
|
|
366
|
-
const throughEntity = relOpts.through();
|
|
367
|
-
const throughMeta = getMeta(throughEntity);
|
|
368
|
-
const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === targetColumn));
|
|
369
|
-
if (!targetRelKey) {
|
|
370
|
-
// Asserted rather than assumed: used as a key regardless, it spells the literal string
|
|
371
|
-
// `undefined`, and the statement asks the junction for a relation of that name.
|
|
372
|
-
throw new TypeError(`'${meta.name}.${relKey}' goes through '${throughMeta.name}', which declares no relation on its ` +
|
|
373
|
-
`'${targetColumn}' column. Give it one, so the target's rows can be read through it.`);
|
|
374
|
-
}
|
|
375
|
-
// A relation query names the target's columns, not the junction's, so its projection and filter
|
|
376
|
-
// belong on the populate below, resolved against the entity that has them. Spread onto the
|
|
377
|
-
// junction query instead they asked `ItemTag` for `Tag`'s columns and failed with "no such
|
|
378
|
-
// column".
|
|
379
|
-
//
|
|
380
|
-
// Ordering and paging split the other way: they describe the statement with one row per pairing,
|
|
381
|
-
// which is the junction's. Left on the populate they reached a to-one join, which rejects all
|
|
382
|
-
// four by name - so a many-to-many carrying any of them threw rather than paging.
|
|
383
|
-
//
|
|
384
|
-
// Those four are not a coincidence: they are exactly the clauses a joined relation rejects, for
|
|
385
|
-
// the same reason - each needs a statement with many rows per parent, which only the junction's
|
|
386
|
-
// is. The `satisfies` ties the two lists together, so a fifth clause added there fails to compile
|
|
387
|
-
// here rather than quietly staying on the populate and throwing again.
|
|
388
|
-
const { $sort, $limit, $skip, $distinct, ...targetQuery } = relationQuery;
|
|
389
|
-
const junctionClauses = {
|
|
390
|
-
$limit,
|
|
391
|
-
$skip,
|
|
392
|
-
$distinct,
|
|
393
|
-
// Qualified by the relation that reaches them, since the columns it names are the target's.
|
|
394
|
-
$sort: $sort && { [targetRelKey]: $sort },
|
|
395
|
-
};
|
|
396
|
-
const junctionQuery = {
|
|
397
|
-
$select: joinedColumns(joins),
|
|
398
|
-
...junctionClauses,
|
|
399
|
-
$populate: {
|
|
400
|
-
[targetRelKey]: {
|
|
401
|
-
...targetQuery,
|
|
402
|
-
$required: true,
|
|
403
|
-
},
|
|
404
|
-
},
|
|
405
|
-
};
|
|
406
|
-
const throughFounds = await this.findChildrenOf(throughEntity, junctionQuery, joins, payload, meta.fields);
|
|
407
|
-
// The junction's own columns carried onto the target's row, which is where `putChildrenInParents`
|
|
408
|
-
// reads them back from - a junction row holds the parent's key under `joined`, not under `parent`.
|
|
409
|
-
const founds = throughFounds.map((it) => ({
|
|
410
|
-
...it[targetRelKey],
|
|
411
|
-
...Object.fromEntries(joins.map(({ joined }) => [joined, it[joined]])),
|
|
412
|
-
}));
|
|
413
|
-
this.putChildrenInParents(payload, founds, joins, relKey);
|
|
414
|
-
}
|
|
415
|
-
async fillToManyOneToMany(payload, meta, relKey, relOpts, relationQuery, relEntity) {
|
|
416
|
-
const joins = parentJoins(relOpts, meta.ids.length);
|
|
417
|
-
// The FK is what putChildrenInParents groups on, so it outlives the relation's projection
|
|
418
|
-
// either way: added to a whitelisting `$select` (the raw-array form has nothing to augment),
|
|
419
|
-
// dropped from a subtractive `$exclude`. `relationQuery` is already a clone.
|
|
420
|
-
const select = asSelectMap(relationQuery.$select);
|
|
421
|
-
const exclude = relationQuery.$exclude;
|
|
422
|
-
for (const { joined } of joins) {
|
|
423
|
-
if (select && !select[joined]) {
|
|
424
|
-
select[joined] = true;
|
|
425
|
-
}
|
|
426
|
-
delete exclude?.[joined];
|
|
427
|
-
}
|
|
428
|
-
this.putChildrenInParents(payload, await this.findChildrenOf(relEntity, relationQuery, joins, payload, meta.fields), joins, relKey);
|
|
429
|
-
}
|
|
430
|
-
/**
|
|
431
|
-
* The children of a whole page of parents, however the relation asked for them: one bounded branch
|
|
432
|
-
* per parent when it wants a share of its own, otherwise a single flat statement over an `IN (...)`
|
|
433
|
-
* list, which is both correct and cheaper.
|
|
434
|
-
*
|
|
435
|
-
* The one place that decision is made - a one-to-many and the junction of a many-to-many differ in
|
|
436
|
-
* what they query, never in how the page is spread over its parents.
|
|
437
|
-
*/
|
|
438
|
-
async findChildrenOf(entity, query, joins, parents, parentFields) {
|
|
439
|
-
const founds = isBoundedPerParent(query)
|
|
440
|
-
? await this.internalFindManyPerParent(entity, query, { joins, parents, parentFields })
|
|
441
|
-
: await this.findMany(entity, queryChildrenOfAll(query, joins, parents));
|
|
442
|
-
// Read back as rows rather than as the entity they hydrate to: what follows regroups them by the
|
|
443
|
-
// join columns, which a projected entity type does not carry.
|
|
444
|
-
return founds;
|
|
445
|
-
}
|
|
446
|
-
putChildrenInParents(parents, children, joins, relKey) {
|
|
447
|
-
const childrenByParentId = dataKeyed();
|
|
448
|
-
// Every joined column, so two children agreeing on one column of a composite key are not
|
|
449
|
-
// gathered under the same parent. Both column lists are read once, not once per row.
|
|
450
|
-
const joinedKeys = keyColumns(joins, 'joined');
|
|
451
|
-
const parentKeys = keyColumns(joins, 'parent');
|
|
452
|
-
for (const child of children) {
|
|
453
|
-
(childrenByParentId[rowKey(child, joinedKeys)] ??= []).push(child);
|
|
454
|
-
}
|
|
455
|
-
for (const parent of parents) {
|
|
456
|
-
// `[]` rather than nothing for a parent with no children: a populated to-many is a list the
|
|
457
|
-
// caller asked for, so it maps and counts without a guard, and its type can say so. An
|
|
458
|
-
// unpopulated one stays absent, which is what tells the two apart.
|
|
459
|
-
parent[relKey] = (childrenByParentId[rowKey(parent, parentKeys)] ?? []);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
342
|
async insertRelations(entity, payload) {
|
|
463
343
|
const meta = getMeta(entity);
|
|
464
344
|
const entries = payload.reduce((acc, it) => {
|
|
@@ -625,6 +505,32 @@ export class AbstractQuerier {
|
|
|
625
505
|
hasHook(entity, event) {
|
|
626
506
|
return (this.extra?.listeners?.some((listener) => listener[event]) || (getMeta(entity).hooks?.[event]?.length ?? 0) > 0);
|
|
627
507
|
}
|
|
508
|
+
/** Whether an `afterLoad` listens on the entity a read returns or on any relation it populated. */
|
|
509
|
+
listensForLoad(entity, populate) {
|
|
510
|
+
if (this.hasHook(entity, 'afterLoad')) {
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
const meta = getMeta(entity);
|
|
514
|
+
return getRelationRequestSummary(meta, populate).requestedKeys.some((relKey) => this.listensForLoad(relationOf(meta, relKey).entity(), parseRelationAtKey(relKey, populate).query.$populate));
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* `afterLoad` for every row a read loaded, a populated relation's before the rows holding them, so a
|
|
518
|
+
* parent's hook sees its children as their own hooks left them. Rows are walked only where a hook
|
|
519
|
+
* listens.
|
|
520
|
+
*/
|
|
521
|
+
async emitLoaded(entity, rows, populate) {
|
|
522
|
+
const meta = getMeta(entity);
|
|
523
|
+
for (const relKey of getRelationRequestSummary(meta, populate).requestedKeys) {
|
|
524
|
+
const relEntity = relationOf(meta, relKey).entity();
|
|
525
|
+
const relPopulate = parseRelationAtKey(relKey, populate).query.$populate;
|
|
526
|
+
if (this.listensForLoad(relEntity, relPopulate)) {
|
|
527
|
+
// A to-many holds a list and a to-one a row, which `flatMap` takes alike; an absent one adds none.
|
|
528
|
+
const loaded = rows.flatMap((row) => row[relKey] ?? []);
|
|
529
|
+
await this.emitLoaded(relEntity, loaded, relPopulate);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
await this.emitHook(entity, 'afterLoad', rows);
|
|
533
|
+
}
|
|
628
534
|
/**
|
|
629
535
|
* The ids of `rows`, read back by the columns an upsert matched them on, for a statement that could
|
|
630
536
|
* not report them in payload order. A row that no read row matches, or that two do, keeps
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AbstractDialect } from '../dialect/index.js';
|
|
2
|
-
import type { EntityData, EntityId, ExtraOptions, FieldKey, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch,
|
|
2
|
+
import type { EntityData, EntityId, ExtraOptions, FieldKey, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryUpsertOneResult, QueryUpsertManyResult, RelationKey, TransactionOptions, Type, UpdatePayload, 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
|
*/
|
|
@@ -31,7 +31,7 @@ export declare abstract class AbstractQuerierPool<Q extends Querier, D extends A
|
|
|
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:
|
|
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, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): AsyncGenerator<QueryFindResult<E, S, V, X, P, C>>;
|
|
35
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
36
|
count<E extends object>(entity: Type<E>, q?: QueryPage<E>, opts?: QueryOptions): Promise<number>;
|
|
37
37
|
exists<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<boolean>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { AbstractSqlDialect } from '../dialect/index.js';
|
|
2
2
|
import type { EntityData, ExtraOptions, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, QueryUpdateResult, SqlQuerier, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
3
|
-
import { type ParentPartition } from '../util/index.js';
|
|
4
3
|
import type { BuildUpdateResultPayload } from '../util/sql.util.js';
|
|
5
4
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
6
5
|
export declare abstract class AbstractSqlQuerier extends AbstractQuerier implements SqlQuerier {
|
|
@@ -58,19 +57,6 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
58
57
|
*/
|
|
59
58
|
private applyVectorTuning;
|
|
60
59
|
protected internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
|
|
61
|
-
/**
|
|
62
|
-
* One bounded subquery per parent, concatenated with `UNION ALL`, so each parent gets its own
|
|
63
|
-
* `$limit` rather than a share of one. Universal, and reads `parents x (skip + limit)` rows where a
|
|
64
|
-
* `ROW_NUMBER` window reads every matching child. [The design](../../../../architecture/populate-limits.md).
|
|
65
|
-
*
|
|
66
|
-
* Each branch is a wrapped derived table: SQLite rejects `ORDER BY`/`LIMIT` on a bare parenthesised
|
|
67
|
-
* compound branch, and the wrapper costs nothing elsewhere.
|
|
68
|
-
*
|
|
69
|
-
* Unlike {@link selectRows} this asserts no lock and tunes no vector search: `$lock` and
|
|
70
|
-
* `$candidates` describe the statement, and `parseRelationQueryValue` refuses both on a relation
|
|
71
|
-
* query, so neither can reach here.
|
|
72
|
-
*/
|
|
73
|
-
protected internalFindManyPerParent<E extends object>(entity: Type<E>, q: Query<E>, partition: ParentPartition): Promise<E[]>;
|
|
74
60
|
/**
|
|
75
61
|
* How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
|
|
76
62
|
* `undefined` when it can. Two clauses rule the window out: `$distinct`, because a window counts
|
|
@@ -107,9 +93,10 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
107
93
|
*/
|
|
108
94
|
private hydrateAll;
|
|
109
95
|
/**
|
|
110
|
-
* One row of {@link hydrateAll}.
|
|
111
|
-
*
|
|
112
|
-
*
|
|
96
|
+
* One row of {@link hydrateAll}. A related row arrives as its parent's statement read it: a to-one
|
|
97
|
+
* joined and unflattened, there only when its key is, since an unmatched join still fills a computed
|
|
98
|
+
* column or a to-many's empty array; a to-many as a JSON array, which a driver may hand over as text.
|
|
99
|
+
* Each is an object of its own, so the walk reaches none twice.
|
|
113
100
|
*/
|
|
114
101
|
private hydrateFields;
|
|
115
102
|
/**
|