@mikro-orm/knex 7.0.0-dev.5 → 7.0.0-dev.50

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