@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.
Files changed (55) hide show
  1. package/AbstractSqlConnection.d.ts +11 -5
  2. package/AbstractSqlConnection.js +78 -32
  3. package/AbstractSqlDriver.d.ts +9 -5
  4. package/AbstractSqlDriver.js +274 -226
  5. package/AbstractSqlPlatform.js +5 -5
  6. package/PivotCollectionPersister.d.ts +3 -2
  7. package/PivotCollectionPersister.js +12 -21
  8. package/README.md +3 -2
  9. package/SqlEntityManager.d.ts +9 -2
  10. package/SqlEntityManager.js +2 -2
  11. package/dialects/mssql/MsSqlNativeQueryBuilder.d.ts +2 -0
  12. package/dialects/mssql/MsSqlNativeQueryBuilder.js +44 -3
  13. package/dialects/mysql/MySqlExceptionConverter.d.ts +3 -3
  14. package/dialects/mysql/MySqlExceptionConverter.js +4 -5
  15. package/dialects/mysql/MySqlSchemaHelper.js +2 -2
  16. package/dialects/postgresql/PostgreSqlTableCompiler.d.ts +1 -0
  17. package/dialects/postgresql/PostgreSqlTableCompiler.js +1 -0
  18. package/dialects/sqlite/BaseSqliteConnection.d.ts +3 -2
  19. package/dialects/sqlite/BaseSqliteConnection.js +2 -8
  20. package/dialects/sqlite/BaseSqlitePlatform.js +1 -2
  21. package/dialects/sqlite/SqliteExceptionConverter.d.ts +2 -2
  22. package/dialects/sqlite/SqliteExceptionConverter.js +6 -4
  23. package/dialects/sqlite/SqliteSchemaHelper.js +5 -6
  24. package/index.d.ts +1 -1
  25. package/index.js +1 -1
  26. package/package.json +5 -5
  27. package/query/ArrayCriteriaNode.d.ts +1 -0
  28. package/query/ArrayCriteriaNode.js +3 -0
  29. package/query/CriteriaNode.d.ts +4 -2
  30. package/query/CriteriaNode.js +11 -6
  31. package/query/CriteriaNodeFactory.js +12 -7
  32. package/query/NativeQueryBuilder.js +1 -1
  33. package/query/ObjectCriteriaNode.d.ts +1 -0
  34. package/query/ObjectCriteriaNode.js +39 -10
  35. package/query/QueryBuilder.d.ts +59 -7
  36. package/query/QueryBuilder.js +177 -53
  37. package/query/QueryBuilderHelper.d.ts +1 -1
  38. package/query/QueryBuilderHelper.js +18 -11
  39. package/query/ScalarCriteriaNode.d.ts +3 -3
  40. package/query/ScalarCriteriaNode.js +9 -7
  41. package/query/index.d.ts +1 -0
  42. package/query/index.js +1 -0
  43. package/query/raw.d.ts +59 -0
  44. package/query/raw.js +68 -0
  45. package/query/rawKnex.d.ts +58 -0
  46. package/query/rawKnex.js +72 -0
  47. package/schema/DatabaseSchema.js +25 -4
  48. package/schema/DatabaseTable.d.ts +5 -4
  49. package/schema/DatabaseTable.js +68 -34
  50. package/schema/SchemaComparator.js +4 -4
  51. package/schema/SchemaHelper.d.ts +2 -0
  52. package/schema/SchemaHelper.js +14 -10
  53. package/schema/SqlSchemaGenerator.d.ts +13 -6
  54. package/schema/SqlSchemaGenerator.js +40 -19
  55. package/typings.d.ts +85 -3
@@ -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[`${fromAlias}.${prop.name}#${alias}`].subquery = subquery;
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 => explicitFields
244
- ? explicitFields.includes(prop.name) || explicitFields.includes(`${alias}.${prop.name}`) || prop.primary
245
- : this.platform.shouldHaveColumn(prop, populate))
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 3 */
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
- /* istanbul ignore next */
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 (Utils.isString(cond)) {
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
- this._orderBy = [];
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 (Utils.isString(cond)) {
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 && mode !== LockMode.OPTIMISTIC && !this.context) {
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.driver.getConnection(type).execute(query.sql, query.params, method, this.context, loggerContext);
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
- const entities = [];
725
- function propagatePopulateHint(entity, hint) {
726
- helper(entity).__serializationContext.populate = hint.concat(helper(entity).__serializationContext.populate ?? []);
727
- hint.forEach(hint => {
728
- const [propName] = hint.field.split(':', 2);
729
- const value = Reference.unwrapReference(entity[propName]);
730
- if (Utils.isEntity(value)) {
731
- propagatePopulateHint(value, hint.children ?? []);
732
- }
733
- else if (Utils.isCollection(value)) {
734
- value.populated();
735
- value.getItems(false).forEach(item => propagatePopulateHint(item, hint.children ?? []));
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
- for (const r of res) {
740
- const entity = this.em.map(this.mainAlias.entityName, r, { schema: this._schema });
741
- propagatePopulateHint(entity, this._populate);
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 3 */
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
- this._joins[`${this.alias}.${prop.name}#${alias}`] = {
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
- if (!this._joins[aliasedName].path && path) {
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 (!Utils.isString(field)) {
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 3 */
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 3 */
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 3 */
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 3 */
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 (prop?.embedded && a === prop.embedded[0]) {
78
+ if (a === prop?.embedded?.[0]) {
79
79
  return aliasPrefix + prop.fieldNames[fkIdx];
80
80
  }
81
- const noPrefix = prop && prop.persist === false;
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(...res.params);
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 3 */
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 3 */
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(...value);
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 = Utils.isNumber(direction) ? QueryOrderNumeric[direction] : direction;
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 && prop.persist === false && !prop.formula && !prop.embedded) || RawQueryFragment.isKnownFragment(f);
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 = Utils.isString(column) ? column.split('.').map(e => this.platform.quoteIdentifier(e)).join('.') : column;
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 { IQueryBuilder, ICriteriaNodeProcessOptions } from '../typings.js';
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<T>(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions): boolean;
9
- shouldJoin(): boolean;
8
+ willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions): boolean;
9
+ private shouldJoin;
10
10
  }
@@ -1,13 +1,15 @@
1
- import { ReferenceKind, Utils } from '@mikro-orm/core';
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
- if (this.shouldJoin()) {
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 => Utils.isArrayOperator(key) && Array.isArray(this.payload[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
@@ -7,3 +7,4 @@ export * from './ObjectCriteriaNode.js';
7
7
  export * from './ScalarCriteriaNode.js';
8
8
  export * from './CriteriaNodeFactory.js';
9
9
  export * from './NativeQueryBuilder.js';
10
+ export * from './raw.js';
package/query/index.js CHANGED
@@ -7,3 +7,4 @@ export * from './ObjectCriteriaNode.js';
7
7
  export * from './ScalarCriteriaNode.js';
8
8
  export * from './CriteriaNodeFactory.js';
9
9
  export * from './NativeQueryBuilder.js';
10
+ export * from './raw.js';