@mikro-orm/knex 7.0.0-dev.4 → 7.0.0-dev.41
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/AbstractSqlConnection.d.ts +8 -4
- package/AbstractSqlConnection.js +75 -31
- package/AbstractSqlDriver.d.ts +14 -9
- package/AbstractSqlDriver.js +273 -213
- package/AbstractSqlPlatform.js +3 -3
- package/PivotCollectionPersister.d.ts +3 -2
- package/PivotCollectionPersister.js +6 -2
- package/README.md +3 -2
- package/SqlEntityManager.d.ts +9 -2
- package/SqlEntityManager.js +2 -2
- package/dialects/mssql/MsSqlNativeQueryBuilder.d.ts +2 -0
- package/dialects/mssql/MsSqlNativeQueryBuilder.js +43 -2
- package/dialects/mysql/MySqlPlatform.js +2 -1
- package/dialects/postgresql/PostgreSqlTableCompiler.d.ts +1 -0
- package/dialects/postgresql/PostgreSqlTableCompiler.js +1 -0
- package/dialects/sqlite/BaseSqliteConnection.d.ts +1 -1
- package/dialects/sqlite/BaseSqliteConnection.js +8 -2
- package/dialects/sqlite/BaseSqlitePlatform.d.ts +0 -1
- package/dialects/sqlite/BaseSqlitePlatform.js +0 -4
- package/dialects/sqlite/SqliteSchemaHelper.js +1 -1
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +4 -4
- package/query/ArrayCriteriaNode.d.ts +1 -0
- package/query/ArrayCriteriaNode.js +3 -0
- package/query/CriteriaNode.d.ts +4 -2
- package/query/CriteriaNode.js +11 -6
- package/query/CriteriaNodeFactory.js +11 -6
- package/query/NativeQueryBuilder.js +1 -1
- package/query/ObjectCriteriaNode.d.ts +1 -0
- package/query/ObjectCriteriaNode.js +38 -7
- package/query/QueryBuilder.d.ts +67 -6
- package/query/QueryBuilder.js +195 -43
- package/query/QueryBuilderHelper.d.ts +1 -1
- package/query/QueryBuilderHelper.js +13 -6
- package/query/ScalarCriteriaNode.d.ts +3 -3
- package/query/ScalarCriteriaNode.js +7 -5
- package/query/index.d.ts +1 -0
- package/query/index.js +1 -0
- package/query/raw.d.ts +58 -0
- package/query/raw.js +72 -0
- package/schema/DatabaseSchema.js +25 -4
- package/schema/DatabaseTable.d.ts +5 -4
- package/schema/DatabaseTable.js +80 -34
- package/schema/SchemaComparator.js +2 -2
- package/schema/SchemaHelper.d.ts +2 -0
- package/schema/SchemaHelper.js +8 -4
- package/schema/SqlSchemaGenerator.d.ts +6 -1
- package/schema/SqlSchemaGenerator.js +22 -3
- package/typings.d.ts +86 -3
package/query/QueryBuilder.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { inspect } from 'node:util';
|
|
2
|
-
import { type AnyEntity, type ConnectionType, type Dictionary, type EntityData, type EntityKey, type EntityMetadata, type EntityName, type EntityProperty, type ExpandProperty, type FlushMode, type GroupOperator, type Loaded, LockMode, type LoggingOptions, type MetadataStorage, type ObjectQuery, PopulateHint, type PopulateOptions, type QBFilterQuery, type QBQueryOrderMap, QueryFlag, type QueryOrderMap, type QueryResult, RawQueryFragment, type RequiredEntityData, type Transaction } from '@mikro-orm/core';
|
|
2
|
+
import { type AnyEntity, type ConnectionType, type Dictionary, type EntityData, type EntityKey, type EntityManager, type EntityMetadata, type EntityName, type EntityProperty, type ExpandProperty, type FilterOptions, type FlushMode, type GroupOperator, type Loaded, LockMode, type LoggingOptions, type MetadataStorage, type ObjectQuery, PopulateHint, type PopulateOptions, type QBFilterQuery, type QBQueryOrderMap, QueryFlag, type QueryOrderMap, type QueryResult, RawQueryFragment, type RequiredEntityData, type Transaction } from '@mikro-orm/core';
|
|
3
3
|
import { JoinType, QueryType } from './enums.js';
|
|
4
4
|
import type { AbstractSqlDriver } from '../AbstractSqlDriver.js';
|
|
5
5
|
import { type Alias, type OnConflictClause, QueryBuilderHelper } from './QueryBuilderHelper.js';
|
|
@@ -11,6 +11,30 @@ export interface ExecuteOptions {
|
|
|
11
11
|
mapResults?: boolean;
|
|
12
12
|
mergeResults?: boolean;
|
|
13
13
|
}
|
|
14
|
+
export interface QBStreamOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Results are mapped to entities, if you set `mapResults: false` you will get POJOs instead.
|
|
17
|
+
*
|
|
18
|
+
* @default true
|
|
19
|
+
*/
|
|
20
|
+
mapResults?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* When populating to-many relations, the ORM streams fully merged entities instead of yielding every row.
|
|
23
|
+
* You can opt out of this behavior by specifying `mergeResults: false`. This will yield every row from
|
|
24
|
+
* the SQL result, but still mapped to entities, meaning that to-many collections will contain at most
|
|
25
|
+
* one item, and you will get duplicate root entities when they have multiple items in the populated
|
|
26
|
+
* collection.
|
|
27
|
+
*
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
mergeResults?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* When enabled, the driver will return the raw database results without renaming the fields to match the entity property names.
|
|
33
|
+
*
|
|
34
|
+
* @default false
|
|
35
|
+
*/
|
|
36
|
+
rawResults?: boolean;
|
|
37
|
+
}
|
|
14
38
|
type AnyString = string & {};
|
|
15
39
|
type Compute<T> = {
|
|
16
40
|
[K in keyof T]: T[K];
|
|
@@ -140,7 +164,16 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
140
164
|
/**
|
|
141
165
|
* Apply filters to the QB where condition.
|
|
142
166
|
*/
|
|
143
|
-
applyFilters(filterOptions?:
|
|
167
|
+
applyFilters(filterOptions?: FilterOptions): Promise<void>;
|
|
168
|
+
private readonly autoJoinedPaths;
|
|
169
|
+
/**
|
|
170
|
+
* @internal
|
|
171
|
+
*/
|
|
172
|
+
scheduleFilterCheck(path: string): void;
|
|
173
|
+
/**
|
|
174
|
+
* @internal
|
|
175
|
+
*/
|
|
176
|
+
applyJoinedFilters(em: EntityManager, filterOptions: FilterOptions | undefined): Promise<void>;
|
|
144
177
|
withSubQuery(subQuery: RawQueryFragment | NativeQueryBuilder, alias: string): this;
|
|
145
178
|
where(cond: QBFilterQuery<Entity>, operator?: keyof typeof GroupOperator): this;
|
|
146
179
|
where(cond: string, params?: any[], operator?: keyof typeof GroupOperator): this;
|
|
@@ -149,6 +182,8 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
149
182
|
orWhere(cond: QBFilterQuery<Entity>): this;
|
|
150
183
|
orWhere(cond: string, params?: any[]): this;
|
|
151
184
|
orderBy(orderBy: QBQueryOrderMap<Entity> | QBQueryOrderMap<Entity>[]): SelectQueryBuilder<Entity, RootAlias, Hint, Context>;
|
|
185
|
+
andOrderBy(orderBy: QBQueryOrderMap<Entity> | QBQueryOrderMap<Entity>[]): SelectQueryBuilder<Entity, RootAlias, Hint, Context>;
|
|
186
|
+
private processOrderBy;
|
|
152
187
|
groupBy(fields: EntityKeyOrString<Entity> | readonly EntityKeyOrString<Entity>[]): SelectQueryBuilder<Entity, RootAlias, Hint, Context>;
|
|
153
188
|
having(cond?: QBFilterQuery | string, params?: any[], operator?: keyof typeof GroupOperator): SelectQueryBuilder<Entity, RootAlias, Hint, Context>;
|
|
154
189
|
andHaving(cond?: QBFilterQuery | string, params?: any[]): SelectQueryBuilder<Entity, RootAlias, Hint, Context>;
|
|
@@ -173,17 +208,17 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
173
208
|
/**
|
|
174
209
|
* Adds index hint to the FROM clause.
|
|
175
210
|
*/
|
|
176
|
-
indexHint(sql: string): this;
|
|
211
|
+
indexHint(sql: string | undefined): this;
|
|
177
212
|
/**
|
|
178
213
|
* Prepend comment to the sql query using the syntax `/* ... *‍/`. Some characters are forbidden such as `/*, *‍/` and `?`.
|
|
179
214
|
*/
|
|
180
|
-
comment(comment: string | string[]): this;
|
|
215
|
+
comment(comment: string | string[] | undefined): this;
|
|
181
216
|
/**
|
|
182
217
|
* Add hints to the query using comment-like syntax `/*+ ... *‍/`. MySQL and Oracle use this syntax for optimizer hints.
|
|
183
218
|
* Also various DB proxies and routers use this syntax to pass hints to alter their behavior. In other dialects the hints
|
|
184
219
|
* are ignored as simple comments.
|
|
185
220
|
*/
|
|
186
|
-
hintComment(comment: string | string[]): this;
|
|
221
|
+
hintComment(comment: string | string[] | undefined): this;
|
|
187
222
|
/**
|
|
188
223
|
* Specifies FROM which entity's table select/update/delete will be executed, removing all previously set FROM-s.
|
|
189
224
|
* Allows setting a main string alias of the selection data.
|
|
@@ -236,14 +271,35 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
236
271
|
* Use `method` to specify what kind of result you want to get (array/single/meta).
|
|
237
272
|
*/
|
|
238
273
|
execute<U = any>(method?: 'all' | 'get' | 'run', options?: ExecuteOptions | boolean): Promise<U>;
|
|
274
|
+
private getConnection;
|
|
275
|
+
/**
|
|
276
|
+
* Executes the query and returns an async iterable (async generator) that yields results one by one.
|
|
277
|
+
* By default, the results are merged and mapped to entity instances, without adding them to the identity map.
|
|
278
|
+
* You can disable merging and mapping by passing the options `{ mergeResults: false, mapResults: false }`.
|
|
279
|
+
* This is useful for processing large datasets without loading everything into memory at once.
|
|
280
|
+
*
|
|
281
|
+
* ```ts
|
|
282
|
+
* const qb = em.createQueryBuilder(Book, 'b');
|
|
283
|
+
* qb.select('*').where({ title: '1984' }).leftJoinAndSelect('b.author', 'a');
|
|
284
|
+
*
|
|
285
|
+
* for await (const book of qb.stream()) {
|
|
286
|
+
* // book is an instance of Book entity
|
|
287
|
+
* console.log(book.title, book.author.name);
|
|
288
|
+
* }
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
stream(options?: QBStreamOptions): AsyncIterableIterator<Loaded<Entity, Hint>>;
|
|
239
292
|
/**
|
|
240
293
|
* Alias for `qb.getResultList()`
|
|
241
294
|
*/
|
|
242
295
|
getResult(): Promise<Loaded<Entity, Hint>[]>;
|
|
243
296
|
/**
|
|
244
|
-
* Executes the query, returning array of results
|
|
297
|
+
* Executes the query, returning array of results mapped to entity instances.
|
|
245
298
|
*/
|
|
246
299
|
getResultList(limit?: number): Promise<Loaded<Entity, Hint>[]>;
|
|
300
|
+
private propagatePopulateHint;
|
|
301
|
+
private mapResult;
|
|
302
|
+
private mapResults;
|
|
247
303
|
/**
|
|
248
304
|
* Executes the query, returning the first result or null
|
|
249
305
|
*/
|
|
@@ -281,6 +337,11 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
281
337
|
processPopulateHint(): void;
|
|
282
338
|
private processPopulateWhere;
|
|
283
339
|
private mergeOnConditions;
|
|
340
|
+
/**
|
|
341
|
+
* When adding an inner join on a left joined relation, we need to nest them,
|
|
342
|
+
* otherwise the inner join could discard rows of the root table.
|
|
343
|
+
*/
|
|
344
|
+
private processNestedJoins;
|
|
284
345
|
private hasToManyJoins;
|
|
285
346
|
protected wrapPaginateSubQuery(meta: EntityMetadata): void;
|
|
286
347
|
private pruneExtraJoins;
|
package/query/QueryBuilder.js
CHANGED
|
@@ -179,10 +179,10 @@ export class QueryBuilder {
|
|
|
179
179
|
subquery = this.platform.formatQuery(rawFragment.sql, rawFragment.params);
|
|
180
180
|
field = field[0];
|
|
181
181
|
}
|
|
182
|
-
const prop = this.joinReference(field, alias, cond, type, path, schema, subquery);
|
|
182
|
+
const { prop, key } = this.joinReference(field, alias, cond, type, path, schema, subquery);
|
|
183
183
|
const [fromAlias] = this.helper.splitField(field);
|
|
184
184
|
if (subquery) {
|
|
185
|
-
this._joins[
|
|
185
|
+
this._joins[key].subquery = subquery;
|
|
186
186
|
}
|
|
187
187
|
const populate = this._joinedProps.get(fromAlias);
|
|
188
188
|
const item = { field: prop.name, strategy: LoadStrategy.JOINED, children: [] };
|
|
@@ -240,9 +240,12 @@ export class QueryBuilder {
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
prop.targetMeta.props
|
|
243
|
-
.filter(prop =>
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
.filter(prop => {
|
|
244
|
+
if (!explicitFields) {
|
|
245
|
+
return this.platform.shouldHaveColumn(prop, populate);
|
|
246
|
+
}
|
|
247
|
+
return prop.primary && !explicitFields.includes(prop.name) && !explicitFields.includes(`${alias}.${prop.name}`);
|
|
248
|
+
})
|
|
246
249
|
.forEach(prop => fields.push(...this.driver.mapPropToFieldNames(this, prop, alias)));
|
|
247
250
|
return fields;
|
|
248
251
|
}
|
|
@@ -257,6 +260,41 @@ export class QueryBuilder {
|
|
|
257
260
|
const cond = await this.em.applyFilters(this.mainAlias.entityName, {}, filterOptions, 'read');
|
|
258
261
|
this.andWhere(cond);
|
|
259
262
|
}
|
|
263
|
+
autoJoinedPaths = [];
|
|
264
|
+
/**
|
|
265
|
+
* @internal
|
|
266
|
+
*/
|
|
267
|
+
scheduleFilterCheck(path) {
|
|
268
|
+
this.autoJoinedPaths.push(path);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* @internal
|
|
272
|
+
*/
|
|
273
|
+
async applyJoinedFilters(em, filterOptions) {
|
|
274
|
+
for (const path of this.autoJoinedPaths) {
|
|
275
|
+
const join = this.getJoinForPath(path);
|
|
276
|
+
if (join.type === JoinType.pivotJoin) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
filterOptions = QueryHelper.mergePropertyFilters(join.prop.filters, filterOptions);
|
|
280
|
+
const cond = await em.applyFilters(join.prop.type, join.cond, filterOptions, 'read');
|
|
281
|
+
if (Utils.hasObjectKeys(cond)) {
|
|
282
|
+
// remove nested filters, we only care about scalars here, nesting would require another join branch
|
|
283
|
+
for (const key of Object.keys(cond)) {
|
|
284
|
+
if (Utils.isPlainObject(cond[key]) && Object.keys(cond[key]).every(k => !(Utils.isOperator(k) && !['$some', '$none', '$every'].includes(k)))) {
|
|
285
|
+
delete cond[key];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (Utils.hasObjectKeys(join.cond)) {
|
|
289
|
+
/* v8 ignore next */
|
|
290
|
+
join.cond = { $and: [join.cond, cond] };
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
join.cond = { ...cond };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
260
298
|
withSubQuery(subQuery, alias) {
|
|
261
299
|
this.ensureNotFinalized();
|
|
262
300
|
if (subQuery instanceof RawQueryFragment) {
|
|
@@ -322,8 +360,16 @@ export class QueryBuilder {
|
|
|
322
360
|
return this.where(cond, params, '$or');
|
|
323
361
|
}
|
|
324
362
|
orderBy(orderBy) {
|
|
363
|
+
return this.processOrderBy(orderBy, true);
|
|
364
|
+
}
|
|
365
|
+
andOrderBy(orderBy) {
|
|
366
|
+
return this.processOrderBy(orderBy, false);
|
|
367
|
+
}
|
|
368
|
+
processOrderBy(orderBy, reset = true) {
|
|
325
369
|
this.ensureNotFinalized();
|
|
326
|
-
|
|
370
|
+
if (reset) {
|
|
371
|
+
this._orderBy = [];
|
|
372
|
+
}
|
|
327
373
|
Utils.asArray(orderBy).forEach(o => {
|
|
328
374
|
const processed = QueryHelper.processWhere({
|
|
329
375
|
where: o,
|
|
@@ -335,7 +381,7 @@ export class QueryBuilder {
|
|
|
335
381
|
convertCustomTypes: false,
|
|
336
382
|
type: 'orderBy',
|
|
337
383
|
});
|
|
338
|
-
this._orderBy.push(CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, processed).process(this, { matchPopulateJoins: true }));
|
|
384
|
+
this._orderBy.push(CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, processed).process(this, { matchPopulateJoins: true, type: 'orderBy' }));
|
|
339
385
|
});
|
|
340
386
|
return this;
|
|
341
387
|
}
|
|
@@ -431,7 +477,7 @@ export class QueryBuilder {
|
|
|
431
477
|
}
|
|
432
478
|
setLockMode(mode, tables) {
|
|
433
479
|
this.ensureNotFinalized();
|
|
434
|
-
if (mode != null &&
|
|
480
|
+
if (mode != null && ![LockMode.OPTIMISTIC, LockMode.NONE].includes(mode) && !this.context) {
|
|
435
481
|
throw ValidationError.transactionRequired();
|
|
436
482
|
}
|
|
437
483
|
this.lockMode = mode;
|
|
@@ -525,7 +571,7 @@ export class QueryBuilder {
|
|
|
525
571
|
Utils.runIfNotEmpty(() => qb.hintComment(this._hintComments), this._hintComments);
|
|
526
572
|
Utils.runIfNotEmpty(() => this.helper.appendOnConflictClause(QueryType.UPSERT, this._onConflict, qb), this._onConflict);
|
|
527
573
|
if (this.lockMode) {
|
|
528
|
-
this.helper.getLockSQL(qb, this.lockMode, this.lockTables);
|
|
574
|
+
this.helper.getLockSQL(qb, this.lockMode, this.lockTables, this._joins);
|
|
529
575
|
}
|
|
530
576
|
this.helper.finalize(this.type, qb, this.mainAlias.metadata, this._data, this._returning);
|
|
531
577
|
this.clearRawFragmentsCache();
|
|
@@ -639,7 +685,7 @@ export class QueryBuilder {
|
|
|
639
685
|
options.mapResults ??= true;
|
|
640
686
|
const isRunType = [QueryType.INSERT, QueryType.UPDATE, QueryType.DELETE, QueryType.TRUNCATE].includes(this.type);
|
|
641
687
|
method ??= isRunType ? 'run' : 'all';
|
|
642
|
-
if (!this.connectionType && isRunType) {
|
|
688
|
+
if (!this.connectionType && (isRunType || this.context)) {
|
|
643
689
|
this.connectionType = 'write';
|
|
644
690
|
}
|
|
645
691
|
if (!this.finalized && method === 'get' && this.type === QueryType.SELECT) {
|
|
@@ -647,13 +693,11 @@ export class QueryBuilder {
|
|
|
647
693
|
}
|
|
648
694
|
const query = this.toQuery();
|
|
649
695
|
const cached = await this.em?.tryCache(this.mainAlias.entityName, this._cache, ['qb.execute', query.sql, query.params, method]);
|
|
650
|
-
if (cached?.data) {
|
|
696
|
+
if (cached?.data !== undefined) {
|
|
651
697
|
return cached.data;
|
|
652
698
|
}
|
|
653
|
-
const write = method === 'run' || !this.platform.getConfig().get('preferReadReplicas');
|
|
654
|
-
const type = this.connectionType || (write ? 'write' : 'read');
|
|
655
699
|
const loggerContext = { id: this.em?.id, ...this.loggerContext };
|
|
656
|
-
const res = await this.
|
|
700
|
+
const res = await this.getConnection().execute(query.sql, query.params, method, this.context, loggerContext);
|
|
657
701
|
const meta = this.mainAlias.metadata;
|
|
658
702
|
if (!options.mapResults || !meta) {
|
|
659
703
|
await this.em?.storeCache(this._cache, cached, res);
|
|
@@ -681,6 +725,64 @@ export class QueryBuilder {
|
|
|
681
725
|
await this.em?.storeCache(this._cache, cached, mapped);
|
|
682
726
|
return mapped;
|
|
683
727
|
}
|
|
728
|
+
getConnection() {
|
|
729
|
+
const write = !this.platform.getConfig().get('preferReadReplicas');
|
|
730
|
+
const type = this.connectionType || (write ? 'write' : 'read');
|
|
731
|
+
return this.driver.getConnection(type);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Executes the query and returns an async iterable (async generator) that yields results one by one.
|
|
735
|
+
* By default, the results are merged and mapped to entity instances, without adding them to the identity map.
|
|
736
|
+
* You can disable merging and mapping by passing the options `{ mergeResults: false, mapResults: false }`.
|
|
737
|
+
* This is useful for processing large datasets without loading everything into memory at once.
|
|
738
|
+
*
|
|
739
|
+
* ```ts
|
|
740
|
+
* const qb = em.createQueryBuilder(Book, 'b');
|
|
741
|
+
* qb.select('*').where({ title: '1984' }).leftJoinAndSelect('b.author', 'a');
|
|
742
|
+
*
|
|
743
|
+
* for await (const book of qb.stream()) {
|
|
744
|
+
* // book is an instance of Book entity
|
|
745
|
+
* console.log(book.title, book.author.name);
|
|
746
|
+
* }
|
|
747
|
+
* ```
|
|
748
|
+
*/
|
|
749
|
+
async *stream(options) {
|
|
750
|
+
options ??= {};
|
|
751
|
+
options.mergeResults ??= true;
|
|
752
|
+
options.mapResults ??= true;
|
|
753
|
+
const query = this.toQuery();
|
|
754
|
+
const loggerContext = { id: this.em?.id, ...this.loggerContext };
|
|
755
|
+
const res = this.getConnection().stream(query.sql, query.params, this.context, loggerContext);
|
|
756
|
+
const meta = this.mainAlias.metadata;
|
|
757
|
+
if (options.rawResults || !meta) {
|
|
758
|
+
yield* res;
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const joinedProps = this.driver.joinedProps(meta, this._populate);
|
|
762
|
+
const stack = [];
|
|
763
|
+
const hash = (data) => {
|
|
764
|
+
return Utils.getPrimaryKeyHash(meta.primaryKeys.map(pk => data[pk]));
|
|
765
|
+
};
|
|
766
|
+
for await (const row of res) {
|
|
767
|
+
const mapped = this.driver.mapResult(row, meta, this._populate, this);
|
|
768
|
+
if (!options.mergeResults || joinedProps.length === 0) {
|
|
769
|
+
yield this.mapResult(mapped, options.mapResults);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (stack.length > 0 && hash(stack[stack.length - 1]) !== hash(mapped)) {
|
|
773
|
+
const res = this.driver.mergeJoinedResult(stack, this.mainAlias.metadata, joinedProps);
|
|
774
|
+
for (const row of res) {
|
|
775
|
+
yield this.mapResult(row, options.mapResults);
|
|
776
|
+
}
|
|
777
|
+
stack.length = 0;
|
|
778
|
+
}
|
|
779
|
+
stack.push(mapped);
|
|
780
|
+
}
|
|
781
|
+
if (stack.length > 0) {
|
|
782
|
+
const merged = this.driver.mergeJoinedResult(stack, this.mainAlias.metadata, joinedProps);
|
|
783
|
+
yield this.mapResult(merged[0], options.mapResults);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
684
786
|
/**
|
|
685
787
|
* Alias for `qb.getResultList()`
|
|
686
788
|
*/
|
|
@@ -688,29 +790,40 @@ export class QueryBuilder {
|
|
|
688
790
|
return this.getResultList();
|
|
689
791
|
}
|
|
690
792
|
/**
|
|
691
|
-
* Executes the query, returning array of results
|
|
793
|
+
* Executes the query, returning array of results mapped to entity instances.
|
|
692
794
|
*/
|
|
693
795
|
async getResultList(limit) {
|
|
694
796
|
await this.em.tryFlush(this.mainAlias.entityName, { flushMode: this.flushMode });
|
|
695
797
|
const res = await this.execute('all', true);
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
798
|
+
return this.mapResults(res, limit);
|
|
799
|
+
}
|
|
800
|
+
propagatePopulateHint(entity, hint) {
|
|
801
|
+
helper(entity).__serializationContext.populate = hint.concat(helper(entity).__serializationContext.populate ?? []);
|
|
802
|
+
hint.forEach(hint => {
|
|
803
|
+
const [propName] = hint.field.split(':', 2);
|
|
804
|
+
const value = Reference.unwrapReference(entity[propName]);
|
|
805
|
+
if (Utils.isEntity(value)) {
|
|
806
|
+
this.propagatePopulateHint(value, hint.children ?? []);
|
|
807
|
+
}
|
|
808
|
+
else if (Utils.isCollection(value)) {
|
|
809
|
+
value.populated();
|
|
810
|
+
value.getItems(false).forEach(item => this.propagatePopulateHint(item, hint.children ?? []));
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
mapResult(row, map = true) {
|
|
815
|
+
if (!map) {
|
|
816
|
+
return row;
|
|
710
817
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
818
|
+
const entity = this.em.map(this.mainAlias.entityName, row, { schema: this._schema });
|
|
819
|
+
this.propagatePopulateHint(entity, this._populate);
|
|
820
|
+
return entity;
|
|
821
|
+
}
|
|
822
|
+
mapResults(res, limit) {
|
|
823
|
+
const entities = [];
|
|
824
|
+
for (const row of res) {
|
|
825
|
+
const entity = this.mapResult(row);
|
|
826
|
+
this.propagatePopulateHint(entity, this._populate);
|
|
714
827
|
entities.push(entity);
|
|
715
828
|
if (limit != null && --limit === 0) {
|
|
716
829
|
break;
|
|
@@ -845,7 +958,8 @@ export class QueryBuilder {
|
|
|
845
958
|
if (field instanceof RawQueryFragment) {
|
|
846
959
|
field = this.platform.formatQuery(field.sql, field.params);
|
|
847
960
|
}
|
|
848
|
-
|
|
961
|
+
const key = `${this.alias}.${prop.name}#${alias}`;
|
|
962
|
+
this._joins[key] = {
|
|
849
963
|
prop,
|
|
850
964
|
alias,
|
|
851
965
|
type,
|
|
@@ -854,7 +968,7 @@ export class QueryBuilder {
|
|
|
854
968
|
subquery: field.toString(),
|
|
855
969
|
ownerAlias: this.alias,
|
|
856
970
|
};
|
|
857
|
-
return prop;
|
|
971
|
+
return { prop, key };
|
|
858
972
|
}
|
|
859
973
|
if (!subquery && type.includes('lateral')) {
|
|
860
974
|
throw new Error(`Lateral join can be used only with a sub-query.`);
|
|
@@ -879,10 +993,13 @@ export class QueryBuilder {
|
|
|
879
993
|
aliasMap: this.getAliasMap(),
|
|
880
994
|
aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
|
|
881
995
|
});
|
|
996
|
+
const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, prop.targetMeta.className, cond);
|
|
997
|
+
cond = criteriaNode.process(this, { ignoreBranching: true, alias });
|
|
882
998
|
let aliasedName = `${fromAlias}.${prop.name}#${alias}`;
|
|
883
999
|
path ??= `${(Object.values(this._joins).find(j => j.alias === fromAlias)?.path ?? entityName)}.${prop.name}`;
|
|
884
1000
|
if (prop.kind === ReferenceKind.ONE_TO_MANY) {
|
|
885
1001
|
this._joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
|
|
1002
|
+
this._joins[aliasedName].path ??= path;
|
|
886
1003
|
}
|
|
887
1004
|
else if (prop.kind === ReferenceKind.MANY_TO_MANY) {
|
|
888
1005
|
let pivotAlias = alias;
|
|
@@ -894,17 +1011,18 @@ export class QueryBuilder {
|
|
|
894
1011
|
const joins = this.helper.joinManyToManyReference(prop, fromAlias, alias, pivotAlias, type, cond, path, schema);
|
|
895
1012
|
Object.assign(this._joins, joins);
|
|
896
1013
|
this.createAlias(prop.pivotEntity, pivotAlias);
|
|
1014
|
+
this._joins[aliasedName].path ??= path;
|
|
1015
|
+
aliasedName = Object.keys(joins)[1];
|
|
897
1016
|
}
|
|
898
1017
|
else if (prop.kind === ReferenceKind.ONE_TO_ONE) {
|
|
899
1018
|
this._joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
|
|
1019
|
+
this._joins[aliasedName].path ??= path;
|
|
900
1020
|
}
|
|
901
1021
|
else { // MANY_TO_ONE
|
|
902
1022
|
this._joins[aliasedName] = this.helper.joinManyToOneReference(prop, fromAlias, alias, type, cond, schema);
|
|
1023
|
+
this._joins[aliasedName].path ??= path;
|
|
903
1024
|
}
|
|
904
|
-
|
|
905
|
-
this._joins[aliasedName].path = path;
|
|
906
|
-
}
|
|
907
|
-
return prop;
|
|
1025
|
+
return { prop, key: aliasedName };
|
|
908
1026
|
}
|
|
909
1027
|
prepareFields(fields, type = 'where') {
|
|
910
1028
|
const ret = [];
|
|
@@ -1079,6 +1197,7 @@ export class QueryBuilder {
|
|
|
1079
1197
|
const meta = this.mainAlias.metadata;
|
|
1080
1198
|
this.applyDiscriminatorCondition();
|
|
1081
1199
|
this.processPopulateHint();
|
|
1200
|
+
this.processNestedJoins();
|
|
1082
1201
|
if (meta && (this._fields?.includes('*') || this._fields?.includes(`${this.mainAlias.aliasName}.*`))) {
|
|
1083
1202
|
meta.props
|
|
1084
1203
|
.filter(prop => prop.formula && (!prop.lazy || this.flags.has(QueryFlag.INCLUDE_LAZY_FORMULAS)))
|
|
@@ -1102,7 +1221,7 @@ export class QueryBuilder {
|
|
|
1102
1221
|
if (!this.flags.has(QueryFlag.DISABLE_PAGINATE) && this._groupBy.length === 0 && this.hasToManyJoins()) {
|
|
1103
1222
|
this.flags.add(QueryFlag.PAGINATE);
|
|
1104
1223
|
}
|
|
1105
|
-
if (meta && this.flags.has(QueryFlag.PAGINATE) && (this._limit > 0 || this._offset > 0)) {
|
|
1224
|
+
if (meta && this.flags.has(QueryFlag.PAGINATE) && !this.flags.has(QueryFlag.DISABLE_PAGINATE) && (this._limit > 0 || this._offset > 0)) {
|
|
1106
1225
|
this.wrapPaginateSubQuery(meta);
|
|
1107
1226
|
}
|
|
1108
1227
|
if (meta && (this.flags.has(QueryFlag.UPDATE_SUB_QUERY) || this.flags.has(QueryFlag.DELETE_SUB_QUERY))) {
|
|
@@ -1138,6 +1257,7 @@ export class QueryBuilder {
|
|
|
1138
1257
|
this._joins[aliasedName] = this.helper.joinOneToReference(prop, this.mainAlias.aliasName, alias, JoinType.leftJoin);
|
|
1139
1258
|
this._joins[aliasedName].path = `${(Object.values(this._joins).find(j => j.alias === fromAlias)?.path ?? meta.className)}.${prop.name}`;
|
|
1140
1259
|
this._populateMap[aliasedName] = this._joins[aliasedName].alias;
|
|
1260
|
+
this.createAlias(prop.type, alias);
|
|
1141
1261
|
}
|
|
1142
1262
|
});
|
|
1143
1263
|
this.processPopulateWhere(false);
|
|
@@ -1152,12 +1272,12 @@ export class QueryBuilder {
|
|
|
1152
1272
|
let joins = Object.values(this._joins);
|
|
1153
1273
|
for (const join of joins) {
|
|
1154
1274
|
join.cond_ ??= join.cond;
|
|
1155
|
-
join.cond =
|
|
1275
|
+
join.cond = { ...join.cond };
|
|
1156
1276
|
}
|
|
1157
1277
|
if (typeof this[key] === 'object') {
|
|
1158
1278
|
const cond = CriteriaNodeFactory
|
|
1159
1279
|
.createNode(this.metadata, this.mainAlias.entityName, this[key])
|
|
1160
|
-
.process(this, { matchPopulateJoins: true, ignoreBranching: true, preferNoBranch: true });
|
|
1280
|
+
.process(this, { matchPopulateJoins: true, ignoreBranching: true, preferNoBranch: true, filter });
|
|
1161
1281
|
// there might be new joins created by processing the `populateWhere` object
|
|
1162
1282
|
joins = Object.values(this._joins);
|
|
1163
1283
|
this.mergeOnConditions(joins, cond, filter);
|
|
@@ -1198,10 +1318,42 @@ export class QueryBuilder {
|
|
|
1198
1318
|
}
|
|
1199
1319
|
}
|
|
1200
1320
|
}
|
|
1321
|
+
/**
|
|
1322
|
+
* When adding an inner join on a left joined relation, we need to nest them,
|
|
1323
|
+
* otherwise the inner join could discard rows of the root table.
|
|
1324
|
+
*/
|
|
1325
|
+
processNestedJoins() {
|
|
1326
|
+
if (this.flags.has(QueryFlag.DISABLE_NESTED_INNER_JOIN)) {
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
const joins = Object.values(this._joins);
|
|
1330
|
+
const lookupParentGroup = (j) => {
|
|
1331
|
+
return j.nested ?? (j.parent ? lookupParentGroup(j.parent) : undefined);
|
|
1332
|
+
};
|
|
1333
|
+
for (const join of joins) {
|
|
1334
|
+
if (join.type === JoinType.innerJoin) {
|
|
1335
|
+
join.parent = joins.find(j => j.alias === join.ownerAlias);
|
|
1336
|
+
// https://stackoverflow.com/a/56815807/3665878
|
|
1337
|
+
if (join.parent?.type === JoinType.leftJoin || join.parent?.type === JoinType.nestedLeftJoin) {
|
|
1338
|
+
const nested = ((join.parent).nested ??= new Set());
|
|
1339
|
+
join.type = join.type === JoinType.innerJoin
|
|
1340
|
+
? JoinType.nestedInnerJoin
|
|
1341
|
+
: JoinType.nestedLeftJoin;
|
|
1342
|
+
nested.add(join);
|
|
1343
|
+
}
|
|
1344
|
+
else if (join.parent?.type === JoinType.nestedInnerJoin) {
|
|
1345
|
+
const group = lookupParentGroup(join.parent);
|
|
1346
|
+
const nested = group ?? ((join.parent).nested ??= new Set());
|
|
1347
|
+
join.type = join.type === JoinType.innerJoin
|
|
1348
|
+
? JoinType.nestedInnerJoin
|
|
1349
|
+
: JoinType.nestedLeftJoin;
|
|
1350
|
+
nested.add(join);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1201
1355
|
hasToManyJoins() {
|
|
1202
|
-
// console.log(this._joins);
|
|
1203
1356
|
return Object.values(this._joins).some(join => {
|
|
1204
|
-
// console.log(join.prop.name, join.prop.kind, [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(join.prop.kind));
|
|
1205
1357
|
return [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(join.prop.kind);
|
|
1206
1358
|
});
|
|
1207
1359
|
}
|
|
@@ -49,7 +49,7 @@ export declare class QueryBuilderHelper {
|
|
|
49
49
|
getQueryOrderFromObject(type: QueryType, orderBy: FlatQueryOrderMap, populate: Dictionary<string>): string[];
|
|
50
50
|
finalize(type: QueryType, qb: NativeQueryBuilder, meta?: EntityMetadata, data?: Dictionary, returning?: Field<any>[]): void;
|
|
51
51
|
splitField<T>(field: EntityKey<T>, greedyAlias?: boolean): [string, EntityKey<T>, string | undefined];
|
|
52
|
-
getLockSQL(qb: NativeQueryBuilder, lockMode: LockMode, lockTables?: string[]): void;
|
|
52
|
+
getLockSQL(qb: NativeQueryBuilder, lockMode: LockMode, lockTables?: string[], joinsMap?: Dictionary<JoinOptions>): void;
|
|
53
53
|
updateVersionProperty(qb: NativeQueryBuilder, data: Dictionary): void;
|
|
54
54
|
private prefix;
|
|
55
55
|
private appendGroupCondition;
|
|
@@ -75,10 +75,10 @@ export class QueryBuilderHelper {
|
|
|
75
75
|
if (prop?.name === a && prop.embeddedProps[f]) {
|
|
76
76
|
return aliasPrefix + prop.fieldNames[fkIdx];
|
|
77
77
|
}
|
|
78
|
-
if (
|
|
78
|
+
if (a === prop?.embedded?.[0]) {
|
|
79
79
|
return aliasPrefix + prop.fieldNames[fkIdx];
|
|
80
80
|
}
|
|
81
|
-
const noPrefix = prop
|
|
81
|
+
const noPrefix = prop?.persist === false;
|
|
82
82
|
if (prop?.fieldNameRaw) {
|
|
83
83
|
return raw(this.prefix(field, isTableNameAliasRequired));
|
|
84
84
|
}
|
|
@@ -358,7 +358,7 @@ export class QueryBuilderHelper {
|
|
|
358
358
|
return;
|
|
359
359
|
}
|
|
360
360
|
parts.push(operator === '$or' ? `(${res.sql})` : res.sql);
|
|
361
|
-
params.push(
|
|
361
|
+
res.params.forEach(p => params.push(p));
|
|
362
362
|
}
|
|
363
363
|
appendQuerySubCondition(type, cond, key) {
|
|
364
364
|
const parts = [];
|
|
@@ -502,7 +502,7 @@ export class QueryBuilderHelper {
|
|
|
502
502
|
params.push(item);
|
|
503
503
|
}
|
|
504
504
|
else {
|
|
505
|
-
params.push(
|
|
505
|
+
value.forEach(v => params.push(v));
|
|
506
506
|
}
|
|
507
507
|
return `(${value.map(() => '?').join(', ')})`;
|
|
508
508
|
}
|
|
@@ -550,7 +550,7 @@ export class QueryBuilderHelper {
|
|
|
550
550
|
let [alias, field] = this.splitField(f, true);
|
|
551
551
|
alias = populate[alias] || alias;
|
|
552
552
|
const prop = this.getProperty(field, alias);
|
|
553
|
-
const noPrefix = (prop
|
|
553
|
+
const noPrefix = (prop?.persist === false && !prop.formula && !prop.embedded) || RawQueryFragment.isKnownFragment(f);
|
|
554
554
|
const column = this.mapper(noPrefix ? field : `${alias}.${field}`, type, undefined, null);
|
|
555
555
|
/* v8 ignore next */
|
|
556
556
|
const rawColumn = Utils.isString(column) ? column.split('.').map(e => this.platform.quoteIdentifier(e)).join('.') : column;
|
|
@@ -622,11 +622,18 @@ export class QueryBuilderHelper {
|
|
|
622
622
|
const fromField = parts.join('.');
|
|
623
623
|
return [fromAlias, fromField, ref];
|
|
624
624
|
}
|
|
625
|
-
getLockSQL(qb, lockMode, lockTables = []) {
|
|
625
|
+
getLockSQL(qb, lockMode, lockTables = [], joinsMap) {
|
|
626
626
|
const meta = this.metadata.find(this.entityName);
|
|
627
627
|
if (lockMode === LockMode.OPTIMISTIC && meta && !meta.versionProperty) {
|
|
628
628
|
throw OptimisticLockError.lockFailed(this.entityName);
|
|
629
629
|
}
|
|
630
|
+
if (lockMode !== LockMode.OPTIMISTIC && lockTables.length === 0 && joinsMap) {
|
|
631
|
+
const joins = Object.values(joinsMap);
|
|
632
|
+
const innerJoins = joins.filter(join => [JoinType.innerJoin, JoinType.innerJoinLateral, JoinType.nestedInnerJoin].includes(join.type));
|
|
633
|
+
if (joins.length > innerJoins.length) {
|
|
634
|
+
lockTables.push(this.alias, ...innerJoins.map(join => join.alias));
|
|
635
|
+
}
|
|
636
|
+
}
|
|
630
637
|
qb.lockMode(lockMode, lockTables);
|
|
631
638
|
}
|
|
632
639
|
updateVersionProperty(qb, data) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { CriteriaNode } from './CriteriaNode.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { ICriteriaNodeProcessOptions, IQueryBuilder } from '../typings.js';
|
|
3
3
|
/**
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
6
6
|
export declare class ScalarCriteriaNode<T extends object> extends CriteriaNode<T> {
|
|
7
7
|
process(qb: IQueryBuilder<T>, options?: ICriteriaNodeProcessOptions): any;
|
|
8
|
-
willAutoJoin
|
|
9
|
-
shouldJoin
|
|
8
|
+
willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions): boolean;
|
|
9
|
+
private shouldJoin;
|
|
10
10
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { ReferenceKind, Utils } from '@mikro-orm/core';
|
|
2
2
|
import { CriteriaNode } from './CriteriaNode.js';
|
|
3
|
-
import { JoinType } from './enums.js';
|
|
3
|
+
import { JoinType, QueryType } from './enums.js';
|
|
4
4
|
import { QueryBuilder } from './QueryBuilder.js';
|
|
5
5
|
/**
|
|
6
6
|
* @internal
|
|
7
7
|
*/
|
|
8
8
|
export class ScalarCriteriaNode extends CriteriaNode {
|
|
9
9
|
process(qb, options) {
|
|
10
|
-
|
|
10
|
+
const matchPopulateJoins = options?.matchPopulateJoins || (this.prop && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind));
|
|
11
|
+
const nestedAlias = qb.getAliasForJoinPath(this.getPath(), { ...options, matchPopulateJoins });
|
|
12
|
+
if (this.shouldJoin(qb, nestedAlias)) {
|
|
11
13
|
const path = this.getPath();
|
|
12
14
|
const parentPath = this.parent.getPath(); // the parent is always there, otherwise `shouldJoin` would return `false`
|
|
13
15
|
const nestedAlias = qb.getAliasForJoinPath(path) || qb.getNextAlias(this.prop?.pivotTable ?? this.entityName);
|
|
@@ -31,10 +33,10 @@ export class ScalarCriteriaNode extends CriteriaNode {
|
|
|
31
33
|
return this.payload;
|
|
32
34
|
}
|
|
33
35
|
willAutoJoin(qb, alias, options) {
|
|
34
|
-
return this.shouldJoin();
|
|
36
|
+
return this.shouldJoin(qb, alias);
|
|
35
37
|
}
|
|
36
|
-
shouldJoin() {
|
|
37
|
-
if (!this.parent || !this.prop) {
|
|
38
|
+
shouldJoin(qb, nestedAlias) {
|
|
39
|
+
if (!this.parent || !this.prop || (nestedAlias && [QueryType.SELECT, QueryType.COUNT].includes(qb.type ?? QueryType.SELECT))) {
|
|
38
40
|
return false;
|
|
39
41
|
}
|
|
40
42
|
switch (this.prop.kind) {
|
package/query/index.d.ts
CHANGED
package/query/index.js
CHANGED