@mikro-orm/knex 7.0.0-dev.8 → 7.0.0-dev.80
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 +11 -5
- package/AbstractSqlConnection.js +78 -32
- package/AbstractSqlDriver.d.ts +9 -5
- package/AbstractSqlDriver.js +274 -226
- package/AbstractSqlPlatform.js +5 -5
- package/PivotCollectionPersister.d.ts +3 -2
- package/PivotCollectionPersister.js +12 -21
- 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 +44 -3
- package/dialects/mysql/MySqlExceptionConverter.d.ts +3 -3
- package/dialects/mysql/MySqlExceptionConverter.js +4 -5
- package/dialects/mysql/MySqlSchemaHelper.js +2 -2
- package/dialects/postgresql/PostgreSqlTableCompiler.d.ts +1 -0
- package/dialects/postgresql/PostgreSqlTableCompiler.js +1 -0
- package/dialects/sqlite/BaseSqliteConnection.d.ts +3 -2
- package/dialects/sqlite/BaseSqliteConnection.js +2 -8
- package/dialects/sqlite/BaseSqlitePlatform.js +1 -2
- package/dialects/sqlite/SqliteExceptionConverter.d.ts +2 -2
- package/dialects/sqlite/SqliteExceptionConverter.js +6 -4
- package/dialects/sqlite/SqliteSchemaHelper.js +5 -6
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +5 -5
- 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 +12 -7
- package/query/NativeQueryBuilder.js +1 -1
- package/query/ObjectCriteriaNode.d.ts +1 -0
- package/query/ObjectCriteriaNode.js +39 -10
- package/query/QueryBuilder.d.ts +59 -7
- package/query/QueryBuilder.js +177 -53
- package/query/QueryBuilderHelper.d.ts +1 -1
- package/query/QueryBuilderHelper.js +18 -11
- package/query/ScalarCriteriaNode.d.ts +3 -3
- package/query/ScalarCriteriaNode.js +9 -7
- package/query/index.d.ts +1 -0
- package/query/index.js +1 -0
- package/query/raw.d.ts +59 -0
- package/query/raw.js +68 -0
- package/query/rawKnex.d.ts +58 -0
- package/query/rawKnex.js +72 -0
- package/schema/DatabaseSchema.js +25 -4
- package/schema/DatabaseTable.d.ts +5 -4
- package/schema/DatabaseTable.js +68 -34
- package/schema/SchemaComparator.js +4 -4
- package/schema/SchemaHelper.d.ts +2 -0
- package/schema/SchemaHelper.js +14 -10
- package/schema/SqlSchemaGenerator.d.ts +13 -6
- package/schema/SqlSchemaGenerator.js +40 -19
- package/typings.d.ts +85 -3
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
|
}
|
|
@@ -250,7 +253,7 @@ export class QueryBuilder {
|
|
|
250
253
|
* Apply filters to the QB where condition.
|
|
251
254
|
*/
|
|
252
255
|
async applyFilters(filterOptions = {}) {
|
|
253
|
-
/* v8 ignore next
|
|
256
|
+
/* v8 ignore next */
|
|
254
257
|
if (!this.em) {
|
|
255
258
|
throw new Error('Cannot apply filters, this QueryBuilder is not attached to an EntityManager');
|
|
256
259
|
}
|
|
@@ -267,16 +270,23 @@ export class QueryBuilder {
|
|
|
267
270
|
/**
|
|
268
271
|
* @internal
|
|
269
272
|
*/
|
|
270
|
-
async applyJoinedFilters(em, filterOptions
|
|
273
|
+
async applyJoinedFilters(em, filterOptions) {
|
|
271
274
|
for (const path of this.autoJoinedPaths) {
|
|
272
275
|
const join = this.getJoinForPath(path);
|
|
273
276
|
if (join.type === JoinType.pivotJoin) {
|
|
274
277
|
continue;
|
|
275
278
|
}
|
|
279
|
+
filterOptions = QueryHelper.mergePropertyFilters(join.prop.filters, filterOptions);
|
|
276
280
|
const cond = await em.applyFilters(join.prop.type, join.cond, filterOptions, 'read');
|
|
277
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
|
+
}
|
|
278
288
|
if (Utils.hasObjectKeys(join.cond)) {
|
|
279
|
-
/*
|
|
289
|
+
/* v8 ignore next */
|
|
280
290
|
join.cond = { $and: [join.cond, cond] };
|
|
281
291
|
}
|
|
282
292
|
else {
|
|
@@ -303,7 +313,7 @@ export class QueryBuilder {
|
|
|
303
313
|
cond = { [raw(`(${sql})`)]: Utils.asArray(params) };
|
|
304
314
|
operator ??= '$and';
|
|
305
315
|
}
|
|
306
|
-
else if (
|
|
316
|
+
else if (typeof cond === 'string') {
|
|
307
317
|
cond = { [raw(`(${cond})`, Utils.asArray(params))]: [] };
|
|
308
318
|
operator ??= '$and';
|
|
309
319
|
}
|
|
@@ -350,8 +360,16 @@ export class QueryBuilder {
|
|
|
350
360
|
return this.where(cond, params, '$or');
|
|
351
361
|
}
|
|
352
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) {
|
|
353
369
|
this.ensureNotFinalized();
|
|
354
|
-
|
|
370
|
+
if (reset) {
|
|
371
|
+
this._orderBy = [];
|
|
372
|
+
}
|
|
355
373
|
Utils.asArray(orderBy).forEach(o => {
|
|
356
374
|
const processed = QueryHelper.processWhere({
|
|
357
375
|
where: o,
|
|
@@ -363,7 +381,7 @@ export class QueryBuilder {
|
|
|
363
381
|
convertCustomTypes: false,
|
|
364
382
|
type: 'orderBy',
|
|
365
383
|
});
|
|
366
|
-
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' }));
|
|
367
385
|
});
|
|
368
386
|
return this;
|
|
369
387
|
}
|
|
@@ -374,7 +392,7 @@ export class QueryBuilder {
|
|
|
374
392
|
}
|
|
375
393
|
having(cond = {}, params, operator) {
|
|
376
394
|
this.ensureNotFinalized();
|
|
377
|
-
if (
|
|
395
|
+
if (typeof cond === 'string') {
|
|
378
396
|
cond = { [raw(`(${cond})`, params)]: [] };
|
|
379
397
|
}
|
|
380
398
|
cond = CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, cond).process(this);
|
|
@@ -459,7 +477,7 @@ export class QueryBuilder {
|
|
|
459
477
|
}
|
|
460
478
|
setLockMode(mode, tables) {
|
|
461
479
|
this.ensureNotFinalized();
|
|
462
|
-
if (mode != null &&
|
|
480
|
+
if (mode != null && ![LockMode.OPTIMISTIC, LockMode.NONE].includes(mode) && !this.context) {
|
|
463
481
|
throw ValidationError.transactionRequired();
|
|
464
482
|
}
|
|
465
483
|
this.lockMode = mode;
|
|
@@ -553,7 +571,7 @@ export class QueryBuilder {
|
|
|
553
571
|
Utils.runIfNotEmpty(() => qb.hintComment(this._hintComments), this._hintComments);
|
|
554
572
|
Utils.runIfNotEmpty(() => this.helper.appendOnConflictClause(QueryType.UPSERT, this._onConflict, qb), this._onConflict);
|
|
555
573
|
if (this.lockMode) {
|
|
556
|
-
this.helper.getLockSQL(qb, this.lockMode, this.lockTables);
|
|
574
|
+
this.helper.getLockSQL(qb, this.lockMode, this.lockTables, this._joins);
|
|
557
575
|
}
|
|
558
576
|
this.helper.finalize(this.type, qb, this.mainAlias.metadata, this._data, this._returning);
|
|
559
577
|
this.clearRawFragmentsCache();
|
|
@@ -667,7 +685,7 @@ export class QueryBuilder {
|
|
|
667
685
|
options.mapResults ??= true;
|
|
668
686
|
const isRunType = [QueryType.INSERT, QueryType.UPDATE, QueryType.DELETE, QueryType.TRUNCATE].includes(this.type);
|
|
669
687
|
method ??= isRunType ? 'run' : 'all';
|
|
670
|
-
if (!this.connectionType && isRunType) {
|
|
688
|
+
if (!this.connectionType && (isRunType || this.context)) {
|
|
671
689
|
this.connectionType = 'write';
|
|
672
690
|
}
|
|
673
691
|
if (!this.finalized && method === 'get' && this.type === QueryType.SELECT) {
|
|
@@ -675,13 +693,11 @@ export class QueryBuilder {
|
|
|
675
693
|
}
|
|
676
694
|
const query = this.toQuery();
|
|
677
695
|
const cached = await this.em?.tryCache(this.mainAlias.entityName, this._cache, ['qb.execute', query.sql, query.params, method]);
|
|
678
|
-
if (cached?.data) {
|
|
696
|
+
if (cached?.data !== undefined) {
|
|
679
697
|
return cached.data;
|
|
680
698
|
}
|
|
681
|
-
const write = method === 'run' || !this.platform.getConfig().get('preferReadReplicas');
|
|
682
|
-
const type = this.connectionType || (write ? 'write' : 'read');
|
|
683
699
|
const loggerContext = { id: this.em?.id, ...this.loggerContext };
|
|
684
|
-
const res = await this.
|
|
700
|
+
const res = await this.getConnection().execute(query.sql, query.params, method, this.context, loggerContext);
|
|
685
701
|
const meta = this.mainAlias.metadata;
|
|
686
702
|
if (!options.mapResults || !meta) {
|
|
687
703
|
await this.em?.storeCache(this._cache, cached, res);
|
|
@@ -709,6 +725,64 @@ export class QueryBuilder {
|
|
|
709
725
|
await this.em?.storeCache(this._cache, cached, mapped);
|
|
710
726
|
return mapped;
|
|
711
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
|
+
}
|
|
712
786
|
/**
|
|
713
787
|
* Alias for `qb.getResultList()`
|
|
714
788
|
*/
|
|
@@ -716,29 +790,40 @@ export class QueryBuilder {
|
|
|
716
790
|
return this.getResultList();
|
|
717
791
|
}
|
|
718
792
|
/**
|
|
719
|
-
* Executes the query, returning array of results
|
|
793
|
+
* Executes the query, returning array of results mapped to entity instances.
|
|
720
794
|
*/
|
|
721
795
|
async getResultList(limit) {
|
|
722
796
|
await this.em.tryFlush(this.mainAlias.entityName, { flushMode: this.flushMode });
|
|
723
797
|
const res = await this.execute('all', true);
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
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;
|
|
738
817
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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);
|
|
742
827
|
entities.push(entity);
|
|
743
828
|
if (limit != null && --limit === 0) {
|
|
744
829
|
break;
|
|
@@ -818,7 +903,7 @@ export class QueryBuilder {
|
|
|
818
903
|
qb[prop] = properties.includes(prop) ? Utils.copy(this[prop]) : this[prop];
|
|
819
904
|
}
|
|
820
905
|
delete RawQueryFragment.cloneRegistry;
|
|
821
|
-
/* v8 ignore next
|
|
906
|
+
/* v8 ignore next */
|
|
822
907
|
if (this._fields && !reset.includes('_fields')) {
|
|
823
908
|
qb._fields = [...this._fields];
|
|
824
909
|
}
|
|
@@ -873,7 +958,8 @@ export class QueryBuilder {
|
|
|
873
958
|
if (field instanceof RawQueryFragment) {
|
|
874
959
|
field = this.platform.formatQuery(field.sql, field.params);
|
|
875
960
|
}
|
|
876
|
-
|
|
961
|
+
const key = `${this.alias}.${prop.name}#${alias}`;
|
|
962
|
+
this._joins[key] = {
|
|
877
963
|
prop,
|
|
878
964
|
alias,
|
|
879
965
|
type,
|
|
@@ -882,7 +968,7 @@ export class QueryBuilder {
|
|
|
882
968
|
subquery: field.toString(),
|
|
883
969
|
ownerAlias: this.alias,
|
|
884
970
|
};
|
|
885
|
-
return prop;
|
|
971
|
+
return { prop, key };
|
|
886
972
|
}
|
|
887
973
|
if (!subquery && type.includes('lateral')) {
|
|
888
974
|
throw new Error(`Lateral join can be used only with a sub-query.`);
|
|
@@ -907,10 +993,13 @@ export class QueryBuilder {
|
|
|
907
993
|
aliasMap: this.getAliasMap(),
|
|
908
994
|
aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
|
|
909
995
|
});
|
|
996
|
+
const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, prop.targetMeta.className, cond);
|
|
997
|
+
cond = criteriaNode.process(this, { ignoreBranching: true, alias });
|
|
910
998
|
let aliasedName = `${fromAlias}.${prop.name}#${alias}`;
|
|
911
999
|
path ??= `${(Object.values(this._joins).find(j => j.alias === fromAlias)?.path ?? entityName)}.${prop.name}`;
|
|
912
1000
|
if (prop.kind === ReferenceKind.ONE_TO_MANY) {
|
|
913
1001
|
this._joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
|
|
1002
|
+
this._joins[aliasedName].path ??= path;
|
|
914
1003
|
}
|
|
915
1004
|
else if (prop.kind === ReferenceKind.MANY_TO_MANY) {
|
|
916
1005
|
let pivotAlias = alias;
|
|
@@ -922,17 +1011,18 @@ export class QueryBuilder {
|
|
|
922
1011
|
const joins = this.helper.joinManyToManyReference(prop, fromAlias, alias, pivotAlias, type, cond, path, schema);
|
|
923
1012
|
Object.assign(this._joins, joins);
|
|
924
1013
|
this.createAlias(prop.pivotEntity, pivotAlias);
|
|
1014
|
+
this._joins[aliasedName].path ??= path;
|
|
1015
|
+
aliasedName = Object.keys(joins)[1];
|
|
925
1016
|
}
|
|
926
1017
|
else if (prop.kind === ReferenceKind.ONE_TO_ONE) {
|
|
927
1018
|
this._joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
|
|
1019
|
+
this._joins[aliasedName].path ??= path;
|
|
928
1020
|
}
|
|
929
1021
|
else { // MANY_TO_ONE
|
|
930
1022
|
this._joins[aliasedName] = this.helper.joinManyToOneReference(prop, fromAlias, alias, type, cond, schema);
|
|
1023
|
+
this._joins[aliasedName].path ??= path;
|
|
931
1024
|
}
|
|
932
|
-
|
|
933
|
-
this._joins[aliasedName].path = path;
|
|
934
|
-
}
|
|
935
|
-
return prop;
|
|
1025
|
+
return { prop, key: aliasedName };
|
|
936
1026
|
}
|
|
937
1027
|
prepareFields(fields, type = 'where') {
|
|
938
1028
|
const ret = [];
|
|
@@ -945,7 +1035,7 @@ export class QueryBuilder {
|
|
|
945
1035
|
ret.push(rawField);
|
|
946
1036
|
return;
|
|
947
1037
|
}
|
|
948
|
-
if (
|
|
1038
|
+
if (typeof field !== 'string') {
|
|
949
1039
|
ret.push(field);
|
|
950
1040
|
return;
|
|
951
1041
|
}
|
|
@@ -956,7 +1046,7 @@ export class QueryBuilder {
|
|
|
956
1046
|
}
|
|
957
1047
|
const [a, f] = this.helper.splitField(field);
|
|
958
1048
|
const prop = this.helper.getProperty(f, a);
|
|
959
|
-
/* v8 ignore next
|
|
1049
|
+
/* v8 ignore next */
|
|
960
1050
|
if (prop && [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
|
|
961
1051
|
return;
|
|
962
1052
|
}
|
|
@@ -1107,6 +1197,7 @@ export class QueryBuilder {
|
|
|
1107
1197
|
const meta = this.mainAlias.metadata;
|
|
1108
1198
|
this.applyDiscriminatorCondition();
|
|
1109
1199
|
this.processPopulateHint();
|
|
1200
|
+
this.processNestedJoins();
|
|
1110
1201
|
if (meta && (this._fields?.includes('*') || this._fields?.includes(`${this.mainAlias.aliasName}.*`))) {
|
|
1111
1202
|
meta.props
|
|
1112
1203
|
.filter(prop => prop.formula && (!prop.lazy || this.flags.has(QueryFlag.INCLUDE_LAZY_FORMULAS)))
|
|
@@ -1130,7 +1221,7 @@ export class QueryBuilder {
|
|
|
1130
1221
|
if (!this.flags.has(QueryFlag.DISABLE_PAGINATE) && this._groupBy.length === 0 && this.hasToManyJoins()) {
|
|
1131
1222
|
this.flags.add(QueryFlag.PAGINATE);
|
|
1132
1223
|
}
|
|
1133
|
-
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)) {
|
|
1134
1225
|
this.wrapPaginateSubQuery(meta);
|
|
1135
1226
|
}
|
|
1136
1227
|
if (meta && (this.flags.has(QueryFlag.UPDATE_SUB_QUERY) || this.flags.has(QueryFlag.DELETE_SUB_QUERY))) {
|
|
@@ -1166,6 +1257,7 @@ export class QueryBuilder {
|
|
|
1166
1257
|
this._joins[aliasedName] = this.helper.joinOneToReference(prop, this.mainAlias.aliasName, alias, JoinType.leftJoin);
|
|
1167
1258
|
this._joins[aliasedName].path = `${(Object.values(this._joins).find(j => j.alias === fromAlias)?.path ?? meta.className)}.${prop.name}`;
|
|
1168
1259
|
this._populateMap[aliasedName] = this._joins[aliasedName].alias;
|
|
1260
|
+
this.createAlias(prop.type, alias);
|
|
1169
1261
|
}
|
|
1170
1262
|
});
|
|
1171
1263
|
this.processPopulateWhere(false);
|
|
@@ -1185,7 +1277,7 @@ export class QueryBuilder {
|
|
|
1185
1277
|
if (typeof this[key] === 'object') {
|
|
1186
1278
|
const cond = CriteriaNodeFactory
|
|
1187
1279
|
.createNode(this.metadata, this.mainAlias.entityName, this[key])
|
|
1188
|
-
.process(this, { matchPopulateJoins: true, ignoreBranching: true, preferNoBranch: true });
|
|
1280
|
+
.process(this, { matchPopulateJoins: true, ignoreBranching: true, preferNoBranch: true, filter });
|
|
1189
1281
|
// there might be new joins created by processing the `populateWhere` object
|
|
1190
1282
|
joins = Object.values(this._joins);
|
|
1191
1283
|
this.mergeOnConditions(joins, cond, filter);
|
|
@@ -1226,10 +1318,42 @@ export class QueryBuilder {
|
|
|
1226
1318
|
}
|
|
1227
1319
|
}
|
|
1228
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
|
+
}
|
|
1229
1355
|
hasToManyJoins() {
|
|
1230
|
-
// console.log(this._joins);
|
|
1231
1356
|
return Object.values(this._joins).some(join => {
|
|
1232
|
-
// console.log(join.prop.name, join.prop.kind, [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(join.prop.kind));
|
|
1233
1357
|
return [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(join.prop.kind);
|
|
1234
1358
|
});
|
|
1235
1359
|
}
|
|
@@ -1284,7 +1408,7 @@ export class QueryBuilder {
|
|
|
1284
1408
|
}
|
|
1285
1409
|
return false;
|
|
1286
1410
|
});
|
|
1287
|
-
/* v8 ignore next
|
|
1411
|
+
/* v8 ignore next */
|
|
1288
1412
|
if (field instanceof RawQueryFragment) {
|
|
1289
1413
|
innerQuery.select(field);
|
|
1290
1414
|
}
|
|
@@ -1393,7 +1517,7 @@ export class QueryBuilder {
|
|
|
1393
1517
|
return new QueryBuilderHelper(this.mainAlias.entityName, this.mainAlias.aliasName, this._aliases, this.subQueries, this.driver);
|
|
1394
1518
|
}
|
|
1395
1519
|
ensureFromClause() {
|
|
1396
|
-
/* v8 ignore next
|
|
1520
|
+
/* v8 ignore next */
|
|
1397
1521
|
if (!this._mainAlias) {
|
|
1398
1522
|
throw new Error(`Cannot proceed to build a query because the main alias is not set.`);
|
|
1399
1523
|
}
|
|
@@ -1403,8 +1527,8 @@ export class QueryBuilder {
|
|
|
1403
1527
|
throw new Error('This QueryBuilder instance is already finalized, clone it first if you want to modify it.');
|
|
1404
1528
|
}
|
|
1405
1529
|
}
|
|
1406
|
-
/* v8 ignore start */
|
|
1407
1530
|
/** @ignore */
|
|
1531
|
+
/* v8 ignore next */
|
|
1408
1532
|
[inspect.custom](depth = 2) {
|
|
1409
1533
|
const object = { ...this };
|
|
1410
1534
|
const hidden = ['metadata', 'driver', 'context', 'platform', 'type'];
|
|
@@ -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;
|
|
@@ -26,7 +26,7 @@ export class QueryBuilderHelper {
|
|
|
26
26
|
if (isRaw(field)) {
|
|
27
27
|
return raw(field.sql, field.params);
|
|
28
28
|
}
|
|
29
|
-
/* v8 ignore next
|
|
29
|
+
/* v8 ignore next */
|
|
30
30
|
if (typeof field !== 'string') {
|
|
31
31
|
return field;
|
|
32
32
|
}
|
|
@@ -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 = [];
|
|
@@ -420,7 +420,7 @@ export class QueryBuilderHelper {
|
|
|
420
420
|
}
|
|
421
421
|
// operators
|
|
422
422
|
const op = Object.keys(QueryOperator).find(op => op in value);
|
|
423
|
-
/* v8 ignore next
|
|
423
|
+
/* v8 ignore next */
|
|
424
424
|
if (!op) {
|
|
425
425
|
throw new Error(`Invalid query condition: ${inspect(cond, { depth: 5 })}`);
|
|
426
426
|
}
|
|
@@ -456,7 +456,7 @@ export class QueryBuilderHelper {
|
|
|
456
456
|
const [a, f] = this.splitField(key);
|
|
457
457
|
const prop = this.getProperty(f, a);
|
|
458
458
|
if (op === '$fulltext') {
|
|
459
|
-
/* v8 ignore next
|
|
459
|
+
/* v8 ignore next */
|
|
460
460
|
if (!prop) {
|
|
461
461
|
throw new Error(`Cannot use $fulltext operator on ${key}, property not found`);
|
|
462
462
|
}
|
|
@@ -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
|
}
|
|
@@ -539,7 +539,7 @@ export class QueryBuilderHelper {
|
|
|
539
539
|
const ret = [];
|
|
540
540
|
for (const key of Object.keys(orderBy)) {
|
|
541
541
|
const direction = orderBy[key];
|
|
542
|
-
const order =
|
|
542
|
+
const order = typeof direction === 'number' ? QueryOrderNumeric[direction] : direction;
|
|
543
543
|
const raw = RawQueryFragment.getKnownFragment(key);
|
|
544
544
|
if (raw) {
|
|
545
545
|
ret.push(...this.platform.getOrderByExpression(this.platform.formatQuery(raw.sql, raw.params), order));
|
|
@@ -550,10 +550,10 @@ 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
|
-
const rawColumn =
|
|
556
|
+
const rawColumn = typeof column === 'string' ? column.split('.').map(e => this.platform.quoteIdentifier(e)).join('.') : column;
|
|
557
557
|
const customOrder = prop?.customOrder;
|
|
558
558
|
let colPart = customOrder
|
|
559
559
|
? this.platform.generateCustomOrder(rawColumn, customOrder)
|
|
@@ -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
|
-
import {
|
|
1
|
+
import { ARRAY_OPERATORS, ReferenceKind } 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);
|
|
@@ -23,7 +25,7 @@ export class ScalarCriteriaNode extends CriteriaNode {
|
|
|
23
25
|
return this.payload.getNativeQuery().toRaw();
|
|
24
26
|
}
|
|
25
27
|
if (this.payload && typeof this.payload === 'object') {
|
|
26
|
-
const keys = Object.keys(this.payload).filter(key =>
|
|
28
|
+
const keys = Object.keys(this.payload).filter(key => ARRAY_OPERATORS.includes(key) && Array.isArray(this.payload[key]));
|
|
27
29
|
for (const key of keys) {
|
|
28
30
|
this.payload[key] = JSON.stringify(this.payload[key]);
|
|
29
31
|
}
|
|
@@ -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