@mikro-orm/knex 7.0.0-dev.9 → 7.0.0-dev.90

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 (59) 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 +267 -227
  5. package/AbstractSqlPlatform.js +5 -5
  6. package/PivotCollectionPersister.d.ts +8 -4
  7. package/PivotCollectionPersister.js +55 -31
  8. package/README.md +3 -2
  9. package/SqlEntityManager.d.ts +10 -2
  10. package/SqlEntityManager.js +11 -2
  11. package/dialects/mssql/MsSqlNativeQueryBuilder.d.ts +2 -0
  12. package/dialects/mssql/MsSqlNativeQueryBuilder.js +42 -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 -14
  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 +2 -1
  25. package/index.js +2 -1
  26. package/package.json +5 -5
  27. package/plugin/index.d.ts +53 -0
  28. package/plugin/index.js +42 -0
  29. package/plugin/transformer.d.ts +115 -0
  30. package/plugin/transformer.js +883 -0
  31. package/query/ArrayCriteriaNode.d.ts +1 -0
  32. package/query/ArrayCriteriaNode.js +3 -0
  33. package/query/CriteriaNode.d.ts +4 -5
  34. package/query/CriteriaNode.js +13 -9
  35. package/query/CriteriaNodeFactory.js +12 -7
  36. package/query/NativeQueryBuilder.js +1 -1
  37. package/query/ObjectCriteriaNode.d.ts +1 -0
  38. package/query/ObjectCriteriaNode.js +35 -8
  39. package/query/QueryBuilder.d.ts +59 -10
  40. package/query/QueryBuilder.js +166 -50
  41. package/query/QueryBuilderHelper.d.ts +1 -1
  42. package/query/QueryBuilderHelper.js +20 -14
  43. package/query/ScalarCriteriaNode.d.ts +3 -3
  44. package/query/ScalarCriteriaNode.js +9 -7
  45. package/query/index.d.ts +1 -0
  46. package/query/index.js +1 -0
  47. package/query/raw.d.ts +59 -0
  48. package/query/raw.js +68 -0
  49. package/query/rawKnex.d.ts +58 -0
  50. package/query/rawKnex.js +72 -0
  51. package/schema/DatabaseSchema.js +25 -4
  52. package/schema/DatabaseTable.d.ts +5 -4
  53. package/schema/DatabaseTable.js +65 -34
  54. package/schema/SchemaComparator.js +5 -6
  55. package/schema/SchemaHelper.d.ts +2 -0
  56. package/schema/SchemaHelper.js +14 -10
  57. package/schema/SqlSchemaGenerator.d.ts +13 -6
  58. package/schema/SqlSchemaGenerator.js +41 -20
  59. package/typings.d.ts +85 -3
@@ -1,5 +1,4 @@
1
- import { inspect } from 'node:util';
2
- import { helper, isRaw, LoadStrategy, LockMode, PopulateHint, QueryFlag, QueryHelper, raw, RawQueryFragment, Reference, ReferenceKind, serialize, Utils, ValidationError, } from '@mikro-orm/core';
1
+ import { helper, isRaw, LoadStrategy, LockMode, PopulateHint, QueryFlag, QueryHelper, raw, RawQueryFragment, Reference, ReferenceKind, serialize, Utils, ValidationError, inspect, } from '@mikro-orm/core';
3
2
  import { JoinType, QueryType } from './enums.js';
4
3
  import { QueryBuilderHelper } from './QueryBuilderHelper.js';
5
4
  import { CriteriaNodeFactory } from './CriteriaNodeFactory.js';
@@ -179,10 +178,10 @@ export class QueryBuilder {
179
178
  subquery = this.platform.formatQuery(rawFragment.sql, rawFragment.params);
180
179
  field = field[0];
181
180
  }
182
- const prop = this.joinReference(field, alias, cond, type, path, schema, subquery);
181
+ const { prop, key } = this.joinReference(field, alias, cond, type, path, schema, subquery);
183
182
  const [fromAlias] = this.helper.splitField(field);
184
183
  if (subquery) {
185
- this._joins[`${fromAlias}.${prop.name}#${alias}`].subquery = subquery;
184
+ this._joins[key].subquery = subquery;
186
185
  }
187
186
  const populate = this._joinedProps.get(fromAlias);
188
187
  const item = { field: prop.name, strategy: LoadStrategy.JOINED, children: [] };
@@ -253,7 +252,7 @@ export class QueryBuilder {
253
252
  * Apply filters to the QB where condition.
254
253
  */
255
254
  async applyFilters(filterOptions = {}) {
256
- /* v8 ignore next 3 */
255
+ /* v8 ignore next */
257
256
  if (!this.em) {
258
257
  throw new Error('Cannot apply filters, this QueryBuilder is not attached to an EntityManager');
259
258
  }
@@ -270,12 +269,13 @@ export class QueryBuilder {
270
269
  /**
271
270
  * @internal
272
271
  */
273
- async applyJoinedFilters(em, filterOptions = {}) {
272
+ async applyJoinedFilters(em, filterOptions) {
274
273
  for (const path of this.autoJoinedPaths) {
275
274
  const join = this.getJoinForPath(path);
276
275
  if (join.type === JoinType.pivotJoin) {
277
276
  continue;
278
277
  }
278
+ filterOptions = QueryHelper.mergePropertyFilters(join.prop.filters, filterOptions);
279
279
  const cond = await em.applyFilters(join.prop.type, join.cond, filterOptions, 'read');
280
280
  if (Utils.hasObjectKeys(cond)) {
281
281
  // remove nested filters, we only care about scalars here, nesting would require another join branch
@@ -285,7 +285,7 @@ export class QueryBuilder {
285
285
  }
286
286
  }
287
287
  if (Utils.hasObjectKeys(join.cond)) {
288
- /* istanbul ignore next */
288
+ /* v8 ignore next */
289
289
  join.cond = { $and: [join.cond, cond] };
290
290
  }
291
291
  else {
@@ -312,7 +312,7 @@ export class QueryBuilder {
312
312
  cond = { [raw(`(${sql})`)]: Utils.asArray(params) };
313
313
  operator ??= '$and';
314
314
  }
315
- else if (Utils.isString(cond)) {
315
+ else if (typeof cond === 'string') {
316
316
  cond = { [raw(`(${cond})`, Utils.asArray(params))]: [] };
317
317
  operator ??= '$and';
318
318
  }
@@ -359,8 +359,16 @@ export class QueryBuilder {
359
359
  return this.where(cond, params, '$or');
360
360
  }
361
361
  orderBy(orderBy) {
362
+ return this.processOrderBy(orderBy, true);
363
+ }
364
+ andOrderBy(orderBy) {
365
+ return this.processOrderBy(orderBy, false);
366
+ }
367
+ processOrderBy(orderBy, reset = true) {
362
368
  this.ensureNotFinalized();
363
- this._orderBy = [];
369
+ if (reset) {
370
+ this._orderBy = [];
371
+ }
364
372
  Utils.asArray(orderBy).forEach(o => {
365
373
  const processed = QueryHelper.processWhere({
366
374
  where: o,
@@ -383,7 +391,7 @@ export class QueryBuilder {
383
391
  }
384
392
  having(cond = {}, params, operator) {
385
393
  this.ensureNotFinalized();
386
- if (Utils.isString(cond)) {
394
+ if (typeof cond === 'string') {
387
395
  cond = { [raw(`(${cond})`, params)]: [] };
388
396
  }
389
397
  cond = CriteriaNodeFactory.createNode(this.metadata, this.mainAlias.entityName, cond).process(this);
@@ -468,7 +476,7 @@ export class QueryBuilder {
468
476
  }
469
477
  setLockMode(mode, tables) {
470
478
  this.ensureNotFinalized();
471
- if (mode != null && mode !== LockMode.OPTIMISTIC && !this.context) {
479
+ if (mode != null && ![LockMode.OPTIMISTIC, LockMode.NONE].includes(mode) && !this.context) {
472
480
  throw ValidationError.transactionRequired();
473
481
  }
474
482
  this.lockMode = mode;
@@ -562,7 +570,7 @@ export class QueryBuilder {
562
570
  Utils.runIfNotEmpty(() => qb.hintComment(this._hintComments), this._hintComments);
563
571
  Utils.runIfNotEmpty(() => this.helper.appendOnConflictClause(QueryType.UPSERT, this._onConflict, qb), this._onConflict);
564
572
  if (this.lockMode) {
565
- this.helper.getLockSQL(qb, this.lockMode, this.lockTables);
573
+ this.helper.getLockSQL(qb, this.lockMode, this.lockTables, this._joins);
566
574
  }
567
575
  this.helper.finalize(this.type, qb, this.mainAlias.metadata, this._data, this._returning);
568
576
  this.clearRawFragmentsCache();
@@ -676,7 +684,7 @@ export class QueryBuilder {
676
684
  options.mapResults ??= true;
677
685
  const isRunType = [QueryType.INSERT, QueryType.UPDATE, QueryType.DELETE, QueryType.TRUNCATE].includes(this.type);
678
686
  method ??= isRunType ? 'run' : 'all';
679
- if (!this.connectionType && isRunType) {
687
+ if (!this.connectionType && (isRunType || this.context)) {
680
688
  this.connectionType = 'write';
681
689
  }
682
690
  if (!this.finalized && method === 'get' && this.type === QueryType.SELECT) {
@@ -684,13 +692,11 @@ export class QueryBuilder {
684
692
  }
685
693
  const query = this.toQuery();
686
694
  const cached = await this.em?.tryCache(this.mainAlias.entityName, this._cache, ['qb.execute', query.sql, query.params, method]);
687
- if (cached?.data) {
695
+ if (cached?.data !== undefined) {
688
696
  return cached.data;
689
697
  }
690
- const write = method === 'run' || !this.platform.getConfig().get('preferReadReplicas');
691
- const type = this.connectionType || (write ? 'write' : 'read');
692
698
  const loggerContext = { id: this.em?.id, ...this.loggerContext };
693
- const res = await this.driver.getConnection(type).execute(query.sql, query.params, method, this.context, loggerContext);
699
+ const res = await this.getConnection().execute(query.sql, query.params, method, this.context, loggerContext);
694
700
  const meta = this.mainAlias.metadata;
695
701
  if (!options.mapResults || !meta) {
696
702
  await this.em?.storeCache(this._cache, cached, res);
@@ -718,6 +724,64 @@ export class QueryBuilder {
718
724
  await this.em?.storeCache(this._cache, cached, mapped);
719
725
  return mapped;
720
726
  }
727
+ getConnection() {
728
+ const write = !this.platform.getConfig().get('preferReadReplicas');
729
+ const type = this.connectionType || (write ? 'write' : 'read');
730
+ return this.driver.getConnection(type);
731
+ }
732
+ /**
733
+ * Executes the query and returns an async iterable (async generator) that yields results one by one.
734
+ * By default, the results are merged and mapped to entity instances, without adding them to the identity map.
735
+ * You can disable merging and mapping by passing the options `{ mergeResults: false, mapResults: false }`.
736
+ * This is useful for processing large datasets without loading everything into memory at once.
737
+ *
738
+ * ```ts
739
+ * const qb = em.createQueryBuilder(Book, 'b');
740
+ * qb.select('*').where({ title: '1984' }).leftJoinAndSelect('b.author', 'a');
741
+ *
742
+ * for await (const book of qb.stream()) {
743
+ * // book is an instance of Book entity
744
+ * console.log(book.title, book.author.name);
745
+ * }
746
+ * ```
747
+ */
748
+ async *stream(options) {
749
+ options ??= {};
750
+ options.mergeResults ??= true;
751
+ options.mapResults ??= true;
752
+ const query = this.toQuery();
753
+ const loggerContext = { id: this.em?.id, ...this.loggerContext };
754
+ const res = this.getConnection().stream(query.sql, query.params, this.context, loggerContext);
755
+ const meta = this.mainAlias.metadata;
756
+ if (options.rawResults || !meta) {
757
+ yield* res;
758
+ return;
759
+ }
760
+ const joinedProps = this.driver.joinedProps(meta, this._populate);
761
+ const stack = [];
762
+ const hash = (data) => {
763
+ return Utils.getPrimaryKeyHash(meta.primaryKeys.map(pk => data[pk]));
764
+ };
765
+ for await (const row of res) {
766
+ const mapped = this.driver.mapResult(row, meta, this._populate, this);
767
+ if (!options.mergeResults || joinedProps.length === 0) {
768
+ yield this.mapResult(mapped, options.mapResults);
769
+ continue;
770
+ }
771
+ if (stack.length > 0 && hash(stack[stack.length - 1]) !== hash(mapped)) {
772
+ const res = this.driver.mergeJoinedResult(stack, this.mainAlias.metadata, joinedProps);
773
+ for (const row of res) {
774
+ yield this.mapResult(row, options.mapResults);
775
+ }
776
+ stack.length = 0;
777
+ }
778
+ stack.push(mapped);
779
+ }
780
+ if (stack.length > 0) {
781
+ const merged = this.driver.mergeJoinedResult(stack, this.mainAlias.metadata, joinedProps);
782
+ yield this.mapResult(merged[0], options.mapResults);
783
+ }
784
+ }
721
785
  /**
722
786
  * Alias for `qb.getResultList()`
723
787
  */
@@ -725,29 +789,40 @@ export class QueryBuilder {
725
789
  return this.getResultList();
726
790
  }
727
791
  /**
728
- * Executes the query, returning array of results
792
+ * Executes the query, returning array of results mapped to entity instances.
729
793
  */
730
794
  async getResultList(limit) {
731
795
  await this.em.tryFlush(this.mainAlias.entityName, { flushMode: this.flushMode });
732
796
  const res = await this.execute('all', true);
733
- const entities = [];
734
- function propagatePopulateHint(entity, hint) {
735
- helper(entity).__serializationContext.populate = hint.concat(helper(entity).__serializationContext.populate ?? []);
736
- hint.forEach(hint => {
737
- const [propName] = hint.field.split(':', 2);
738
- const value = Reference.unwrapReference(entity[propName]);
739
- if (Utils.isEntity(value)) {
740
- propagatePopulateHint(value, hint.children ?? []);
741
- }
742
- else if (Utils.isCollection(value)) {
743
- value.populated();
744
- value.getItems(false).forEach(item => propagatePopulateHint(item, hint.children ?? []));
745
- }
746
- });
797
+ return this.mapResults(res, limit);
798
+ }
799
+ propagatePopulateHint(entity, hint) {
800
+ helper(entity).__serializationContext.populate = hint.concat(helper(entity).__serializationContext.populate ?? []);
801
+ hint.forEach(hint => {
802
+ const [propName] = hint.field.split(':', 2);
803
+ const value = Reference.unwrapReference(entity[propName]);
804
+ if (Utils.isEntity(value)) {
805
+ this.propagatePopulateHint(value, hint.children ?? []);
806
+ }
807
+ else if (Utils.isCollection(value)) {
808
+ value.populated();
809
+ value.getItems(false).forEach(item => this.propagatePopulateHint(item, hint.children ?? []));
810
+ }
811
+ });
812
+ }
813
+ mapResult(row, map = true) {
814
+ if (!map) {
815
+ return row;
747
816
  }
748
- for (const r of res) {
749
- const entity = this.em.map(this.mainAlias.entityName, r, { schema: this._schema });
750
- propagatePopulateHint(entity, this._populate);
817
+ const entity = this.em.map(this.mainAlias.entityName, row, { schema: this._schema });
818
+ this.propagatePopulateHint(entity, this._populate);
819
+ return entity;
820
+ }
821
+ mapResults(res, limit) {
822
+ const entities = [];
823
+ for (const row of res) {
824
+ const entity = this.mapResult(row);
825
+ this.propagatePopulateHint(entity, this._populate);
751
826
  entities.push(entity);
752
827
  if (limit != null && --limit === 0) {
753
828
  break;
@@ -827,7 +902,7 @@ export class QueryBuilder {
827
902
  qb[prop] = properties.includes(prop) ? Utils.copy(this[prop]) : this[prop];
828
903
  }
829
904
  delete RawQueryFragment.cloneRegistry;
830
- /* v8 ignore next 3 */
905
+ /* v8 ignore next */
831
906
  if (this._fields && !reset.includes('_fields')) {
832
907
  qb._fields = [...this._fields];
833
908
  }
@@ -882,7 +957,8 @@ export class QueryBuilder {
882
957
  if (field instanceof RawQueryFragment) {
883
958
  field = this.platform.formatQuery(field.sql, field.params);
884
959
  }
885
- this._joins[`${this.alias}.${prop.name}#${alias}`] = {
960
+ const key = `${this.alias}.${prop.name}#${alias}`;
961
+ this._joins[key] = {
886
962
  prop,
887
963
  alias,
888
964
  type,
@@ -891,7 +967,7 @@ export class QueryBuilder {
891
967
  subquery: field.toString(),
892
968
  ownerAlias: this.alias,
893
969
  };
894
- return prop;
970
+ return { prop, key };
895
971
  }
896
972
  if (!subquery && type.includes('lateral')) {
897
973
  throw new Error(`Lateral join can be used only with a sub-query.`);
@@ -916,10 +992,13 @@ export class QueryBuilder {
916
992
  aliasMap: this.getAliasMap(),
917
993
  aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
918
994
  });
995
+ const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, prop.targetMeta.className, cond);
996
+ cond = criteriaNode.process(this, { ignoreBranching: true, alias });
919
997
  let aliasedName = `${fromAlias}.${prop.name}#${alias}`;
920
998
  path ??= `${(Object.values(this._joins).find(j => j.alias === fromAlias)?.path ?? entityName)}.${prop.name}`;
921
999
  if (prop.kind === ReferenceKind.ONE_TO_MANY) {
922
1000
  this._joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
1001
+ this._joins[aliasedName].path ??= path;
923
1002
  }
924
1003
  else if (prop.kind === ReferenceKind.MANY_TO_MANY) {
925
1004
  let pivotAlias = alias;
@@ -931,17 +1010,18 @@ export class QueryBuilder {
931
1010
  const joins = this.helper.joinManyToManyReference(prop, fromAlias, alias, pivotAlias, type, cond, path, schema);
932
1011
  Object.assign(this._joins, joins);
933
1012
  this.createAlias(prop.pivotEntity, pivotAlias);
1013
+ this._joins[aliasedName].path ??= path;
1014
+ aliasedName = Object.keys(joins)[1];
934
1015
  }
935
1016
  else if (prop.kind === ReferenceKind.ONE_TO_ONE) {
936
1017
  this._joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
1018
+ this._joins[aliasedName].path ??= path;
937
1019
  }
938
1020
  else { // MANY_TO_ONE
939
1021
  this._joins[aliasedName] = this.helper.joinManyToOneReference(prop, fromAlias, alias, type, cond, schema);
1022
+ this._joins[aliasedName].path ??= path;
940
1023
  }
941
- if (!this._joins[aliasedName].path && path) {
942
- this._joins[aliasedName].path = path;
943
- }
944
- return prop;
1024
+ return { prop, key: aliasedName };
945
1025
  }
946
1026
  prepareFields(fields, type = 'where') {
947
1027
  const ret = [];
@@ -954,7 +1034,7 @@ export class QueryBuilder {
954
1034
  ret.push(rawField);
955
1035
  return;
956
1036
  }
957
- if (!Utils.isString(field)) {
1037
+ if (typeof field !== 'string') {
958
1038
  ret.push(field);
959
1039
  return;
960
1040
  }
@@ -965,7 +1045,7 @@ export class QueryBuilder {
965
1045
  }
966
1046
  const [a, f] = this.helper.splitField(field);
967
1047
  const prop = this.helper.getProperty(f, a);
968
- /* v8 ignore next 3 */
1048
+ /* v8 ignore next */
969
1049
  if (prop && [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
970
1050
  return;
971
1051
  }
@@ -1116,6 +1196,7 @@ export class QueryBuilder {
1116
1196
  const meta = this.mainAlias.metadata;
1117
1197
  this.applyDiscriminatorCondition();
1118
1198
  this.processPopulateHint();
1199
+ this.processNestedJoins();
1119
1200
  if (meta && (this._fields?.includes('*') || this._fields?.includes(`${this.mainAlias.aliasName}.*`))) {
1120
1201
  meta.props
1121
1202
  .filter(prop => prop.formula && (!prop.lazy || this.flags.has(QueryFlag.INCLUDE_LAZY_FORMULAS)))
@@ -1139,7 +1220,7 @@ export class QueryBuilder {
1139
1220
  if (!this.flags.has(QueryFlag.DISABLE_PAGINATE) && this._groupBy.length === 0 && this.hasToManyJoins()) {
1140
1221
  this.flags.add(QueryFlag.PAGINATE);
1141
1222
  }
1142
- if (meta && this.flags.has(QueryFlag.PAGINATE) && (this._limit > 0 || this._offset > 0)) {
1223
+ if (meta && this.flags.has(QueryFlag.PAGINATE) && !this.flags.has(QueryFlag.DISABLE_PAGINATE) && (this._limit > 0 || this._offset > 0)) {
1143
1224
  this.wrapPaginateSubQuery(meta);
1144
1225
  }
1145
1226
  if (meta && (this.flags.has(QueryFlag.UPDATE_SUB_QUERY) || this.flags.has(QueryFlag.DELETE_SUB_QUERY))) {
@@ -1175,6 +1256,7 @@ export class QueryBuilder {
1175
1256
  this._joins[aliasedName] = this.helper.joinOneToReference(prop, this.mainAlias.aliasName, alias, JoinType.leftJoin);
1176
1257
  this._joins[aliasedName].path = `${(Object.values(this._joins).find(j => j.alias === fromAlias)?.path ?? meta.className)}.${prop.name}`;
1177
1258
  this._populateMap[aliasedName] = this._joins[aliasedName].alias;
1259
+ this.createAlias(prop.type, alias);
1178
1260
  }
1179
1261
  });
1180
1262
  this.processPopulateWhere(false);
@@ -1194,7 +1276,7 @@ export class QueryBuilder {
1194
1276
  if (typeof this[key] === 'object') {
1195
1277
  const cond = CriteriaNodeFactory
1196
1278
  .createNode(this.metadata, this.mainAlias.entityName, this[key])
1197
- .process(this, { matchPopulateJoins: true, ignoreBranching: true, preferNoBranch: true });
1279
+ .process(this, { matchPopulateJoins: true, ignoreBranching: true, preferNoBranch: true, filter });
1198
1280
  // there might be new joins created by processing the `populateWhere` object
1199
1281
  joins = Object.values(this._joins);
1200
1282
  this.mergeOnConditions(joins, cond, filter);
@@ -1235,6 +1317,40 @@ export class QueryBuilder {
1235
1317
  }
1236
1318
  }
1237
1319
  }
1320
+ /**
1321
+ * When adding an inner join on a left joined relation, we need to nest them,
1322
+ * otherwise the inner join could discard rows of the root table.
1323
+ */
1324
+ processNestedJoins() {
1325
+ if (this.flags.has(QueryFlag.DISABLE_NESTED_INNER_JOIN)) {
1326
+ return;
1327
+ }
1328
+ const joins = Object.values(this._joins);
1329
+ const lookupParentGroup = (j) => {
1330
+ return j.nested ?? (j.parent ? lookupParentGroup(j.parent) : undefined);
1331
+ };
1332
+ for (const join of joins) {
1333
+ if (join.type === JoinType.innerJoin) {
1334
+ join.parent = joins.find(j => j.alias === join.ownerAlias);
1335
+ // https://stackoverflow.com/a/56815807/3665878
1336
+ if (join.parent?.type === JoinType.leftJoin || join.parent?.type === JoinType.nestedLeftJoin) {
1337
+ const nested = ((join.parent).nested ??= new Set());
1338
+ join.type = join.type === JoinType.innerJoin
1339
+ ? JoinType.nestedInnerJoin
1340
+ : JoinType.nestedLeftJoin;
1341
+ nested.add(join);
1342
+ }
1343
+ else if (join.parent?.type === JoinType.nestedInnerJoin) {
1344
+ const group = lookupParentGroup(join.parent);
1345
+ const nested = group ?? ((join.parent).nested ??= new Set());
1346
+ join.type = join.type === JoinType.innerJoin
1347
+ ? JoinType.nestedInnerJoin
1348
+ : JoinType.nestedLeftJoin;
1349
+ nested.add(join);
1350
+ }
1351
+ }
1352
+ }
1353
+ }
1238
1354
  hasToManyJoins() {
1239
1355
  return Object.values(this._joins).some(join => {
1240
1356
  return [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(join.prop.kind);
@@ -1291,7 +1407,7 @@ export class QueryBuilder {
1291
1407
  }
1292
1408
  return false;
1293
1409
  });
1294
- /* v8 ignore next 3 */
1410
+ /* v8 ignore next */
1295
1411
  if (field instanceof RawQueryFragment) {
1296
1412
  innerQuery.select(field);
1297
1413
  }
@@ -1400,7 +1516,7 @@ export class QueryBuilder {
1400
1516
  return new QueryBuilderHelper(this.mainAlias.entityName, this.mainAlias.aliasName, this._aliases, this.subQueries, this.driver);
1401
1517
  }
1402
1518
  ensureFromClause() {
1403
- /* v8 ignore next 3 */
1519
+ /* v8 ignore next */
1404
1520
  if (!this._mainAlias) {
1405
1521
  throw new Error(`Cannot proceed to build a query because the main alias is not set.`);
1406
1522
  }
@@ -1410,9 +1526,9 @@ export class QueryBuilder {
1410
1526
  throw new Error('This QueryBuilder instance is already finalized, clone it first if you want to modify it.');
1411
1527
  }
1412
1528
  }
1413
- /* v8 ignore start */
1414
1529
  /** @ignore */
1415
- [inspect.custom](depth = 2) {
1530
+ /* v8 ignore next */
1531
+ [Symbol.for('nodejs.util.inspect.custom')](depth = 2) {
1416
1532
  const object = { ...this };
1417
1533
  const hidden = ['metadata', 'driver', 'context', 'platform', 'type'];
1418
1534
  Object.keys(object).filter(k => k.startsWith('_')).forEach(k => delete object[k]);
@@ -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;
@@ -1,5 +1,4 @@
1
- import { inspect } from 'node:util';
2
- import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, isRaw, LockMode, OptimisticLockError, QueryOperator, QueryOrderNumeric, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core';
1
+ import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, isRaw, LockMode, OptimisticLockError, QueryOperator, QueryOrderNumeric, raw, RawQueryFragment, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';
3
2
  import { JoinType, QueryType } from './enums.js';
4
3
  import { NativeQueryBuilder } from './NativeQueryBuilder.js';
5
4
  /**
@@ -26,7 +25,7 @@ export class QueryBuilderHelper {
26
25
  if (isRaw(field)) {
27
26
  return raw(field.sql, field.params);
28
27
  }
29
- /* v8 ignore next 3 */
28
+ /* v8 ignore next */
30
29
  if (typeof field !== 'string') {
31
30
  return field;
32
31
  }
@@ -75,10 +74,10 @@ export class QueryBuilderHelper {
75
74
  if (prop?.name === a && prop.embeddedProps[f]) {
76
75
  return aliasPrefix + prop.fieldNames[fkIdx];
77
76
  }
78
- if (prop?.embedded && a === prop.embedded[0]) {
77
+ if (a === prop?.embedded?.[0]) {
79
78
  return aliasPrefix + prop.fieldNames[fkIdx];
80
79
  }
81
- const noPrefix = prop && prop.persist === false;
80
+ const noPrefix = prop?.persist === false;
82
81
  if (prop?.fieldNameRaw) {
83
82
  return raw(this.prefix(field, isTableNameAliasRequired));
84
83
  }
@@ -358,7 +357,7 @@ export class QueryBuilderHelper {
358
357
  return;
359
358
  }
360
359
  parts.push(operator === '$or' ? `(${res.sql})` : res.sql);
361
- params.push(...res.params);
360
+ res.params.forEach(p => params.push(p));
362
361
  }
363
362
  appendQuerySubCondition(type, cond, key) {
364
363
  const parts = [];
@@ -420,9 +419,9 @@ export class QueryBuilderHelper {
420
419
  }
421
420
  // operators
422
421
  const op = Object.keys(QueryOperator).find(op => op in value);
423
- /* v8 ignore next 3 */
422
+ /* v8 ignore next */
424
423
  if (!op) {
425
- throw new Error(`Invalid query condition: ${inspect(cond, { depth: 5 })}`);
424
+ throw ValidationError.invalidQueryCondition(cond);
426
425
  }
427
426
  const replacement = this.getOperatorReplacement(op, value);
428
427
  const fields = Utils.splitPrimaryKeys(key);
@@ -456,7 +455,7 @@ export class QueryBuilderHelper {
456
455
  const [a, f] = this.splitField(key);
457
456
  const prop = this.getProperty(f, a);
458
457
  if (op === '$fulltext') {
459
- /* v8 ignore next 3 */
458
+ /* v8 ignore next */
460
459
  if (!prop) {
461
460
  throw new Error(`Cannot use $fulltext operator on ${key}, property not found`);
462
461
  }
@@ -502,7 +501,7 @@ export class QueryBuilderHelper {
502
501
  params.push(item);
503
502
  }
504
503
  else {
505
- params.push(...value);
504
+ value.forEach(v => params.push(v));
506
505
  }
507
506
  return `(${value.map(() => '?').join(', ')})`;
508
507
  }
@@ -539,7 +538,7 @@ export class QueryBuilderHelper {
539
538
  const ret = [];
540
539
  for (const key of Object.keys(orderBy)) {
541
540
  const direction = orderBy[key];
542
- const order = Utils.isNumber(direction) ? QueryOrderNumeric[direction] : direction;
541
+ const order = typeof direction === 'number' ? QueryOrderNumeric[direction] : direction;
543
542
  const raw = RawQueryFragment.getKnownFragment(key);
544
543
  if (raw) {
545
544
  ret.push(...this.platform.getOrderByExpression(this.platform.formatQuery(raw.sql, raw.params), order));
@@ -550,10 +549,10 @@ export class QueryBuilderHelper {
550
549
  let [alias, field] = this.splitField(f, true);
551
550
  alias = populate[alias] || alias;
552
551
  const prop = this.getProperty(field, alias);
553
- const noPrefix = (prop && prop.persist === false && !prop.formula && !prop.embedded) || RawQueryFragment.isKnownFragment(f);
552
+ const noPrefix = (prop?.persist === false && !prop.formula && !prop.embedded) || RawQueryFragment.isKnownFragment(f);
554
553
  const column = this.mapper(noPrefix ? field : `${alias}.${field}`, type, undefined, null);
555
554
  /* v8 ignore next */
556
- const rawColumn = Utils.isString(column) ? column.split('.').map(e => this.platform.quoteIdentifier(e)).join('.') : column;
555
+ const rawColumn = typeof column === 'string' ? column.split('.').map(e => this.platform.quoteIdentifier(e)).join('.') : column;
557
556
  const customOrder = prop?.customOrder;
558
557
  let colPart = customOrder
559
558
  ? this.platform.generateCustomOrder(rawColumn, customOrder)
@@ -622,11 +621,18 @@ export class QueryBuilderHelper {
622
621
  const fromField = parts.join('.');
623
622
  return [fromAlias, fromField, ref];
624
623
  }
625
- getLockSQL(qb, lockMode, lockTables = []) {
624
+ getLockSQL(qb, lockMode, lockTables = [], joinsMap) {
626
625
  const meta = this.metadata.find(this.entityName);
627
626
  if (lockMode === LockMode.OPTIMISTIC && meta && !meta.versionProperty) {
628
627
  throw OptimisticLockError.lockFailed(this.entityName);
629
628
  }
629
+ if (lockMode !== LockMode.OPTIMISTIC && lockTables.length === 0 && joinsMap) {
630
+ const joins = Object.values(joinsMap);
631
+ const innerJoins = joins.filter(join => [JoinType.innerJoin, JoinType.innerJoinLateral, JoinType.nestedInnerJoin].includes(join.type));
632
+ if (joins.length > innerJoins.length) {
633
+ lockTables.push(this.alias, ...innerJoins.map(join => join.alias));
634
+ }
635
+ }
630
636
  qb.lockMode(lockMode, lockTables);
631
637
  }
632
638
  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';