@mikro-orm/knex 7.0.0-dev.21 → 7.0.0-dev.23

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.
@@ -1,5 +1,5 @@
1
1
  import { type ControlledTransaction, type Dialect, Kysely } from 'kysely';
2
- import { type AnyEntity, Connection, type Dictionary, type EntityData, type IsolationLevel, type LoggingOptions, type MaybePromise, type QueryResult, RawQueryFragment, type Transaction, type TransactionEventBroadcaster } from '@mikro-orm/core';
2
+ import { type AnyEntity, Connection, type Dictionary, type EntityData, type IsolationLevel, type LogContext, type LoggingOptions, type MaybePromise, type QueryResult, RawQueryFragment, type Transaction, type TransactionEventBroadcaster } from '@mikro-orm/core';
3
3
  import type { AbstractSqlPlatform } from './AbstractSqlPlatform.js';
4
4
  import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
5
5
  export declare abstract class AbstractSqlConnection extends Connection {
@@ -31,15 +31,17 @@ export declare abstract class AbstractSqlConnection extends Connection {
31
31
  readOnly?: boolean;
32
32
  ctx?: ControlledTransaction<any>;
33
33
  eventBroadcaster?: TransactionEventBroadcaster;
34
+ loggerContext?: LogContext;
34
35
  }): Promise<T>;
35
36
  begin(options?: {
36
37
  isolationLevel?: IsolationLevel;
37
38
  readOnly?: boolean;
38
39
  ctx?: ControlledTransaction<any, any>;
39
40
  eventBroadcaster?: TransactionEventBroadcaster;
41
+ loggerContext?: LogContext;
40
42
  }): Promise<ControlledTransaction<any, any>>;
41
- commit(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster): Promise<void>;
42
- rollback(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster): Promise<void>;
43
+ commit(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
44
+ rollback(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
43
45
  execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[], method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
44
46
  /**
45
47
  * Execute raw SQL queries from file
@@ -61,11 +61,11 @@ export class AbstractSqlConnection extends Connection {
61
61
  const trx = await this.begin(options);
62
62
  try {
63
63
  const ret = await cb(trx);
64
- await this.commit(trx, options.eventBroadcaster);
64
+ await this.commit(trx, options.eventBroadcaster, options.loggerContext);
65
65
  return ret;
66
66
  }
67
67
  catch (error) {
68
- await this.rollback(trx, options.eventBroadcaster);
68
+ await this.rollback(trx, options.eventBroadcaster, options.loggerContext);
69
69
  throw error;
70
70
  }
71
71
  }
@@ -78,7 +78,7 @@ export class AbstractSqlConnection extends Connection {
78
78
  const trx = await options.ctx.savepoint(savepointName).execute();
79
79
  Reflect.defineProperty(trx, 'index', { value: ctx.index + 1 });
80
80
  Reflect.defineProperty(trx, 'savepointName', { value: savepointName });
81
- this.logQuery(this.platform.getSavepointSQL(savepointName));
81
+ this.logQuery(this.platform.getSavepointSQL(savepointName), options.loggerContext);
82
82
  await options.eventBroadcaster?.dispatchEvent(EventType.afterTransactionStart, trx);
83
83
  return trx;
84
84
  }
@@ -92,36 +92,46 @@ export class AbstractSqlConnection extends Connection {
92
92
  trxBuilder = trxBuilder.setAccessMode('read only');
93
93
  }
94
94
  const trx = await trxBuilder.execute();
95
- for (const query of this.platform.getBeginTransactionSQL(options)) {
96
- this.logQuery(query);
95
+ if (options.ctx) {
96
+ const ctx = options.ctx;
97
+ ctx.index ??= 0;
98
+ const savepointName = `trx${ctx.index + 1}`;
99
+ Reflect.defineProperty(trx, 'index', { value: ctx.index + 1 });
100
+ Reflect.defineProperty(trx, 'savepointName', { value: savepointName });
101
+ this.logQuery(this.platform.getSavepointSQL(savepointName), options.loggerContext);
102
+ }
103
+ else {
104
+ for (const query of this.platform.getBeginTransactionSQL(options)) {
105
+ this.logQuery(query, options.loggerContext);
106
+ }
97
107
  }
98
108
  await options.eventBroadcaster?.dispatchEvent(EventType.afterTransactionStart, trx);
99
109
  return trx;
100
110
  }
101
- async commit(ctx, eventBroadcaster) {
111
+ async commit(ctx, eventBroadcaster, loggerContext) {
102
112
  if (ctx.isRolledBack) {
103
113
  return;
104
114
  }
105
115
  await eventBroadcaster?.dispatchEvent(EventType.beforeTransactionCommit, ctx);
106
116
  if ('savepointName' in ctx) {
107
117
  await ctx.releaseSavepoint(ctx.savepointName).execute();
108
- this.logQuery(this.platform.getReleaseSavepointSQL(ctx.savepointName));
118
+ this.logQuery(this.platform.getReleaseSavepointSQL(ctx.savepointName), loggerContext);
109
119
  }
110
120
  else {
111
121
  await ctx.commit().execute();
112
- this.logQuery(this.platform.getCommitTransactionSQL());
122
+ this.logQuery(this.platform.getCommitTransactionSQL(), loggerContext);
113
123
  }
114
124
  await eventBroadcaster?.dispatchEvent(EventType.afterTransactionCommit, ctx);
115
125
  }
116
- async rollback(ctx, eventBroadcaster) {
126
+ async rollback(ctx, eventBroadcaster, loggerContext) {
117
127
  await eventBroadcaster?.dispatchEvent(EventType.beforeTransactionRollback, ctx);
118
128
  if ('savepointName' in ctx) {
119
129
  await ctx.rollbackToSavepoint(ctx.savepointName).execute();
120
- this.logQuery(this.platform.getRollbackToSavepointSQL(ctx.savepointName));
130
+ this.logQuery(this.platform.getRollbackToSavepointSQL(ctx.savepointName), loggerContext);
121
131
  }
122
132
  else {
123
133
  await ctx.rollback().execute();
124
- this.logQuery(this.platform.getRollbackTransactionSQL());
134
+ this.logQuery(this.platform.getRollbackTransactionSQL(), loggerContext);
125
135
  }
126
136
  await eventBroadcaster?.dispatchEvent(EventType.afterTransactionRollback, ctx);
127
137
  }
@@ -1,4 +1,4 @@
1
- import { ALIAS_REPLACEMENT_RE, DatabaseDriver, EntityManagerType, getOnConflictFields, getOnConflictReturningFields, helper, isRaw, LoadStrategy, parseJsonSafe, QueryFlag, QueryHelper, QueryOrder, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core';
1
+ import { ALIAS_REPLACEMENT_RE, DatabaseDriver, EntityManagerType, getOnConflictFields, getOnConflictReturningFields, helper, isRaw, LoadStrategy, parseJsonSafe, QueryFlag, QueryHelper, QueryOrder, raw, RawQueryFragment, ReferenceKind, Utils, getLoadingStrategy, } from '@mikro-orm/core';
2
2
  import { QueryBuilder } from './query/QueryBuilder.js';
3
3
  import { JoinType, QueryType } from './query/enums.js';
4
4
  import { SqlEntityManager } from './SqlEntityManager.js';
@@ -331,6 +331,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
331
331
  if (meta && !Utils.isEmpty(populate)) {
332
332
  this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, true);
333
333
  }
334
+ if (options.em) {
335
+ await qb.applyJoinedFilters(options.em, options.filters);
336
+ }
334
337
  return this.rethrow(qb.getCount());
335
338
  }
336
339
  async nativeInsert(entityName, data, options = {}) {
@@ -338,7 +341,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
338
341
  const meta = this.metadata.find(entityName);
339
342
  const collections = this.extractManyToMany(entityName, data);
340
343
  const pks = meta?.primaryKeys ?? [this.config.getNamingStrategy().referenceColumnName()];
341
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes).withSchema(this.getSchemaName(meta, options));
344
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
342
345
  const res = await this.rethrow(qb.insert(data).execute('run', false));
343
346
  res.row = res.row || {};
344
347
  let pk;
@@ -399,14 +402,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
399
402
  value = this.mapDataToFieldNames(value, false, prop.embeddedProps, options.convertCustomTypes);
400
403
  }
401
404
  }
402
- if (options.convertCustomTypes && prop.customType) {
403
- params.push(prop.customType.convertToDatabaseValue(value, this.platform, { key: prop.name, mode: 'query-data' }));
404
- return;
405
- }
406
405
  if (typeof value === 'undefined' && this.platform.usesDefaultKeyword()) {
407
406
  params.push(raw('default'));
408
407
  return;
409
408
  }
409
+ if (options.convertCustomTypes && prop.customType) {
410
+ params.push(prop.customType.convertToDatabaseValue(value, this.platform, { key: prop.name, mode: 'query-data' }));
411
+ return;
412
+ }
410
413
  params.push(value);
411
414
  };
412
415
  if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
@@ -465,7 +468,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
465
468
  if (transform) {
466
469
  sql = transform(sql);
467
470
  }
468
- const res = await this.execute(sql, params, 'run', options.ctx);
471
+ const res = await this.execute(sql, params, 'run', options.ctx, options.loggerContext);
469
472
  let pk;
470
473
  /* v8 ignore next 3 */
471
474
  if (pks.length > 1) { // owner has composite pk
@@ -493,7 +496,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
493
496
  where = { [meta?.primaryKeys[0] ?? pks[0]]: where };
494
497
  }
495
498
  if (Utils.hasObjectKeys(data)) {
496
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes)
499
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext)
497
500
  .withSchema(this.getSchemaName(meta, options));
498
501
  if (options.upsert) {
499
502
  /* v8 ignore next */
@@ -532,7 +535,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
532
535
  const meta = this.metadata.get(entityName);
533
536
  if (options.upsert) {
534
537
  const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(where[0]) ? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
535
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes).withSchema(this.getSchemaName(meta, options));
538
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
536
539
  const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
537
540
  qb.insert(data)
538
541
  .onConflict(uniqueFields)
@@ -645,7 +648,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
645
648
  /* v8 ignore next */
646
649
  sql += returningFields.length > 0 ? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}` : '';
647
650
  }
648
- const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx));
651
+ const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, options.loggerContext));
649
652
  for (let i = 0; i < collections.length; i++) {
650
653
  await this.processManyToMany(meta, where[i], collections[i], false, options);
651
654
  }
@@ -657,7 +660,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
657
660
  if (Utils.isPrimaryKey(where) && pks.length === 1) {
658
661
  where = { [pks[0]]: where };
659
662
  }
660
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false).delete(where).withSchema(this.getSchemaName(meta, options));
663
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext).delete(where).withSchema(this.getSchemaName(meta, options));
661
664
  return this.rethrow(qb.execute('run', false));
662
665
  }
663
666
  /**
@@ -744,7 +747,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
744
747
  schema = this.config.get('schema');
745
748
  }
746
749
  const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
747
- const persister = groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema);
750
+ const persister = groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext);
748
751
  persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks);
749
752
  }
750
753
  for (const persister of Utils.values(groups)) {
@@ -752,112 +755,64 @@ export class AbstractSqlDriver extends DatabaseDriver {
752
755
  }
753
756
  }
754
757
  async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
758
+ if (owners.length === 0) {
759
+ return {};
760
+ }
755
761
  const pivotMeta = this.metadata.find(prop.pivotEntity);
756
762
  const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
757
763
  const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
758
764
  const ownerMeta = this.metadata.find(pivotProp2.type);
759
- options = { ...options };
760
- const qb = this.createQueryBuilder(prop.pivotEntity, ctx, options.connectionType, undefined, options?.logging)
761
- .withSchema(this.getSchemaName(pivotMeta, options))
762
- .indexHint(options.indexHint)
763
- .comment(options.comments)
764
- .hintComment(options.hintComments);
765
- const pivotAlias = qb.alias;
766
- const pivotKey = pivotProp2.joinColumns.map(column => `${pivotAlias}.${column}`).join(Utils.PK_SEPARATOR);
767
765
  const cond = {
768
- [pivotKey]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) },
766
+ [pivotProp2.name]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) },
769
767
  };
770
- /* v8 ignore next 3 */
771
- if (!Utils.isEmpty(where) && Object.keys(where).every(k => Utils.isOperator(k, false))) {
772
- where = cond;
773
- }
774
- else {
775
- where = { ...where, ...cond };
776
- }
777
- orderBy = this.getPivotOrderBy(prop, pivotProp1, pivotAlias, orderBy);
778
- const populate = this.autoJoinOneToOneOwner(prop.targetMeta, []);
779
- const fields = [];
780
- const k1 = !prop.owner ? 'joinColumns' : 'inverseJoinColumns';
781
- const k2 = prop.owner ? 'joinColumns' : 'inverseJoinColumns';
782
- const cols = [
783
- ...prop[k1].map(col => `${pivotAlias}.${col} as fk__${col}`),
784
- ...prop[k2].map(col => `${pivotAlias}.${col} as fk__${col}`),
785
- ];
786
- fields.push(...cols);
787
- if (!pivotJoin) {
788
- const targetAlias = qb.getNextAlias(prop.targetMeta.tableName);
789
- const targetSchema = this.getSchemaName(prop.targetMeta, options) ?? this.platform.getDefaultSchemaName();
790
- qb.innerJoin(pivotProp1.name, targetAlias, {}, targetSchema);
791
- const targetFields = this.buildFields(prop.targetMeta, (options.populate ?? []), [], qb, targetAlias, options);
792
- const additionalFields = [];
793
- for (const field of targetFields) {
794
- const f = field.toString();
795
- additionalFields.push(f.includes('.') ? field : `${targetAlias}.${f}`);
796
- if (RawQueryFragment.isKnownFragment(field)) {
797
- qb.rawFragments.add(f);
798
- }
799
- }
800
- fields.unshift(...additionalFields);
801
- // we need to handle 1:1 owner auto-joins explicitly, as the QB type is the pivot table, not the target
802
- populate.forEach(hint => {
803
- const alias = qb.getNextAlias(prop.targetMeta.tableName);
804
- qb.leftJoin(`${targetAlias}.${hint.field}`, alias);
805
- // eslint-disable-next-line dot-notation
806
- for (const join of Object.values(qb['_joins'])) {
807
- const [propName] = hint.field.split(':', 2);
808
- if (join.alias === alias && join.prop.name === propName) {
809
- fields.push(...qb.helper.mapJoinColumns(qb.type, join));
810
- }
811
- }
812
- });
813
- }
814
- qb.select(fields)
815
- .where({ [pivotProp1.name]: where })
816
- .orderBy(orderBy)
817
- .setLockMode(options.lockMode, options.lockTableAliases);
818
- if (owners.length === 1 && (options.offset != null || options.limit != null)) {
819
- qb.limit(options.limit, options.offset);
820
- }
821
- const res = owners.length ? await this.rethrow(qb.execute('all', { mergeResults: false, mapResults: false })) : [];
822
- const tmp = {};
823
- const items = res.map((row) => {
824
- const root = super.mapResult(row, prop.targetMeta);
825
- this.mapJoinedProps(root, prop.targetMeta, populate, qb, root, tmp, pivotMeta.className + '.' + pivotProp1.name);
826
- return root;
768
+ if (!Utils.isEmpty(where)) {
769
+ cond[pivotProp1.name] = { ...where };
770
+ }
771
+ where = cond;
772
+ const populateField = pivotJoin ? `${pivotProp1.name}:ref` : pivotProp1.name;
773
+ const populate = this.autoJoinOneToOneOwner(prop.targetMeta, options?.populate ?? [], options?.fields);
774
+ const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${pivotProp1.name}.${f}`) : [];
775
+ const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${pivotProp1.name}.${f}`) : [];
776
+ const fields = pivotJoin
777
+ ? [pivotProp1.name, pivotProp2.name]
778
+ : [pivotProp1.name, pivotProp2.name, ...childFields];
779
+ const res = await this.find(pivotMeta.className, where, {
780
+ ctx,
781
+ ...options,
782
+ fields,
783
+ exclude: childExclude,
784
+ orderBy: this.getPivotOrderBy(prop, pivotProp1, orderBy, options?.orderBy),
785
+ populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate }],
786
+ populateWhere: undefined,
787
+ // @ts-ignore
788
+ _populateWhere: 'infer',
789
+ populateFilter: !Utils.isEmpty(options?.populateFilter) ? { [pivotProp2.name]: options?.populateFilter } : undefined,
827
790
  });
828
- qb.clearRawFragmentsCache();
829
791
  const map = {};
830
- const pkProps = ownerMeta.getPrimaryProps();
831
792
  for (const owner of owners) {
832
- const key = Utils.getPrimaryKeyHash(prop.joinColumns.map((_col, idx) => {
833
- const pkProp = pkProps[idx];
834
- return pkProp.customType ? pkProp.customType.convertToJSValue(owner[idx], this.platform) : owner[idx];
835
- }));
793
+ const key = Utils.getPrimaryKeyHash(owner);
836
794
  map[key] = [];
837
795
  }
838
- for (const item of items) {
839
- const key = Utils.getPrimaryKeyHash(prop.joinColumns.map((col, idx) => {
840
- const pkProp = pkProps[idx];
841
- return pkProp.customType ? pkProp.customType.convertToJSValue(item[`fk__${col}`], this.platform) : item[`fk__${col}`];
842
- }));
843
- map[key].push(item);
844
- prop.joinColumns.forEach(col => delete item[`fk__${col}`]);
845
- prop.inverseJoinColumns.forEach((col, idx) => {
846
- Utils.renameKey(item, `fk__${col}`, prop.targetMeta.primaryKeys[idx]);
847
- });
796
+ for (const item of res) {
797
+ const key = Utils.getPrimaryKeyHash(Utils.asArray(item[pivotProp2.name]));
798
+ map[key].push(item[pivotProp1.name]);
848
799
  }
849
800
  return map;
850
801
  }
851
- getPivotOrderBy(prop, pivotProp, pivotAlias, orderBy) {
852
- // FIXME this is ignoring the rest of the array items
802
+ getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
853
803
  if (!Utils.isEmpty(orderBy)) {
854
- return [{ [pivotProp.name]: Utils.asArray(orderBy)[0] }];
804
+ return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
805
+ }
806
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && Utils.asArray(parentOrderBy).some(o => o[prop.name])) {
807
+ return Utils.asArray(parentOrderBy)
808
+ .filter(o => o[prop.name])
809
+ .map(o => ({ [pivotProp.name]: o[prop.name] }));
855
810
  }
856
811
  if (!Utils.isEmpty(prop.orderBy)) {
857
- return [{ [pivotProp.name]: Utils.asArray(prop.orderBy)[0] }];
812
+ return Utils.asArray(prop.orderBy).map(o => ({ [pivotProp.name]: o }));
858
813
  }
859
814
  if (prop.fixedOrder) {
860
- return [{ [`${pivotAlias}.${prop.fixedOrderColumn}`]: QueryOrder.ASC }];
815
+ return [{ [prop.fixedOrderColumn]: QueryOrder.ASC }];
861
816
  }
862
817
  return [];
863
818
  }
@@ -875,7 +830,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
875
830
  const toPopulate = meta.relations
876
831
  .filter(prop => prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && !prop.lazy && !relationsToPopulate.includes(prop.name))
877
832
  .filter(prop => fields.length === 0 || fields.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)))
878
- .map(prop => ({ field: `${prop.name}:ref`, strategy: prop.strategy }));
833
+ .map(prop => ({ field: `${prop.name}:ref`, strategy: LoadStrategy.JOINED }));
879
834
  return [...populate, ...toPopulate];
880
835
  }
881
836
  /**
@@ -883,16 +838,17 @@ export class AbstractSqlDriver extends DatabaseDriver {
883
838
  */
884
839
  joinedProps(meta, populate, options) {
885
840
  return populate.filter(hint => {
886
- const [propName, ref] = hint.field.split(':', 2);
841
+ const [propName] = hint.field.split(':', 2);
887
842
  const prop = meta.properties[propName] || {};
888
- if (hint.filter && hint.strategy === LoadStrategy.JOINED) {
843
+ const strategy = getLoadingStrategy(hint.strategy || prop.strategy || options?.strategy || this.config.get('loadStrategy'), prop.kind);
844
+ if (hint.filter && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) && !prop.nullable) {
889
845
  return true;
890
846
  }
891
847
  // skip redundant joins for 1:1 owner population hints when using `mapToPk`
892
848
  if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
893
849
  return false;
894
850
  }
895
- if ((options?.strategy || hint.strategy || prop.strategy || this.config.get('loadStrategy')) !== LoadStrategy.JOINED) {
851
+ if (strategy !== LoadStrategy.JOINED) {
896
852
  // force joined strategy for explicit 1:1 owner populate hint as it would require a join anyway
897
853
  return prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner;
898
854
  }
@@ -903,31 +859,26 @@ export class AbstractSqlDriver extends DatabaseDriver {
903
859
  * @internal
904
860
  */
905
861
  mergeJoinedResult(rawResults, meta, joinedProps) {
862
+ if (rawResults.length <= 1) {
863
+ return rawResults;
864
+ }
906
865
  const res = [];
907
866
  const map = {};
867
+ const collectionsToMerge = {};
868
+ const hints = joinedProps.map(hint => {
869
+ const [propName, ref] = hint.field.split(':', 2);
870
+ return { propName, ref, children: hint.children };
871
+ });
908
872
  for (const item of rawResults) {
909
873
  const pk = Utils.getCompositeKeyHash(item, meta);
910
874
  if (map[pk]) {
911
- for (const hint of joinedProps) {
912
- const [propName, ref] = hint.field.split(':', 2);
913
- const prop = meta.properties[propName];
875
+ for (const { propName } of hints) {
914
876
  if (!item[propName]) {
915
877
  continue;
916
878
  }
917
- if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind) && ref) {
918
- map[pk][propName] = [...map[pk][propName], ...item[propName]];
919
- continue;
920
- }
921
- switch (prop.kind) {
922
- case ReferenceKind.ONE_TO_MANY:
923
- case ReferenceKind.MANY_TO_MANY:
924
- map[pk][propName] = this.mergeJoinedResult([...map[pk][propName], ...item[propName]], prop.targetMeta, hint.children ?? []);
925
- break;
926
- case ReferenceKind.MANY_TO_ONE:
927
- case ReferenceKind.ONE_TO_ONE:
928
- map[pk][propName] = this.mergeJoinedResult([map[pk][propName], item[propName]], prop.targetMeta, hint.children ?? [])[0];
929
- break;
930
- }
879
+ collectionsToMerge[pk] ??= {};
880
+ collectionsToMerge[pk][propName] ??= [map[pk][propName]];
881
+ collectionsToMerge[pk][propName].push(item[propName]);
931
882
  }
932
883
  }
933
884
  else {
@@ -935,6 +886,31 @@ export class AbstractSqlDriver extends DatabaseDriver {
935
886
  res.push(item);
936
887
  }
937
888
  }
889
+ for (const pk in collectionsToMerge) {
890
+ const entity = map[pk];
891
+ const collections = collectionsToMerge[pk];
892
+ for (const { propName, ref, children } of hints) {
893
+ if (!collections[propName]) {
894
+ continue;
895
+ }
896
+ const prop = meta.properties[propName];
897
+ const items = collections[propName].flat();
898
+ if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind) && ref) {
899
+ entity[propName] = items;
900
+ continue;
901
+ }
902
+ switch (prop.kind) {
903
+ case ReferenceKind.ONE_TO_MANY:
904
+ case ReferenceKind.MANY_TO_MANY:
905
+ entity[propName] = this.mergeJoinedResult(items, prop.targetMeta, children ?? []);
906
+ break;
907
+ case ReferenceKind.MANY_TO_ONE:
908
+ case ReferenceKind.ONE_TO_ONE:
909
+ entity[propName] = this.mergeJoinedResult(items, prop.targetMeta, children ?? [])[0];
910
+ break;
911
+ }
912
+ }
913
+ }
938
914
  return res;
939
915
  }
940
916
  getFieldsForJoinedLoad(qb, meta, options) {
@@ -962,7 +938,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
962
938
  const [propName, ref] = hint.field.split(':', 2);
963
939
  const prop = meta.properties[propName];
964
940
  // ignore ref joins of known FKs unless it's a filter hint
965
- if (ref && !hint.filter && (prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner))) {
941
+ if (ref && !hint.filter && (prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))) {
966
942
  continue;
967
943
  }
968
944
  if (options.count && !options?.populateFilter?.[prop.name]) {
@@ -976,11 +952,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
976
952
  if (!options.parentJoinPath && populateWhereAll && !hint.filter && !path.startsWith('[populate]')) {
977
953
  path = '[populate]' + path;
978
954
  }
955
+ const mandatoryToOneProperty = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.nullable;
979
956
  const joinType = pivotRefJoin
980
957
  ? JoinType.pivotJoin
981
- : hint.filter && !prop.nullable
982
- ? JoinType.innerJoin
983
- : JoinType.leftJoin;
958
+ : hint.joinType
959
+ ? hint.joinType
960
+ : (hint.filter && !prop.nullable) || mandatoryToOneProperty
961
+ ? JoinType.innerJoin
962
+ : JoinType.leftJoin;
984
963
  qb.join(field, tableAlias, {}, joinType, path);
985
964
  if (pivotRefJoin) {
986
965
  fields.push(...prop.joinColumns.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)), ...prop.inverseJoinColumns.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)));
@@ -1012,7 +991,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1012
991
  parentJoinPath: path,
1013
992
  }));
1014
993
  }
1015
- else if (hint.filter || prop.mapToPk) {
994
+ else if (hint.filter || prop.mapToPk || (ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind))) {
1016
995
  fields.push(...prop.referencedColumnNames.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)));
1017
996
  }
1018
997
  }
@@ -1087,7 +1066,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1087
1066
  for (const prop of meta.relations) {
1088
1067
  if (collections[prop.name]) {
1089
1068
  const pivotMeta = this.metadata.find(prop.pivotEntity);
1090
- const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema);
1069
+ const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext);
1091
1070
  persister.enqueueUpdate(prop, collections[prop.name], clear, pks);
1092
1071
  await this.rethrow(persister.execute());
1093
1072
  }
@@ -1154,7 +1133,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1154
1133
  let path = parentPath;
1155
1134
  const meta2 = this.metadata.find(prop.type);
1156
1135
  const childOrder = orderHint[prop.name];
1157
- if (![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || !prop.owner || Utils.isPlainObject(childOrder)) {
1136
+ if (prop.kind !== ReferenceKind.SCALAR && (![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || !prop.owner || Utils.isPlainObject(childOrder))) {
1158
1137
  path += `.${propName}`;
1159
1138
  }
1160
1139
  if (prop.kind === ReferenceKind.MANY_TO_MANY && typeof childOrder !== 'object') {
@@ -1162,10 +1141,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
1162
1141
  }
1163
1142
  const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
1164
1143
  const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true }) ?? parentAlias;
1165
- if (!join && parentAlias === qb.alias) {
1144
+ if (!join) {
1166
1145
  continue;
1167
1146
  }
1168
- if (![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED].includes(prop.kind) && typeof childOrder === 'object') {
1147
+ if (join && ![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED].includes(prop.kind) && typeof childOrder === 'object') {
1169
1148
  const children = this.buildPopulateOrderBy(qb, meta2, Utils.asArray(childOrder), path, explicit, propAlias);
1170
1149
  orderBy.push(...children);
1171
1150
  continue;
@@ -1320,16 +1299,19 @@ export class AbstractSqlDriver extends DatabaseDriver {
1320
1299
  ret.push('*');
1321
1300
  }
1322
1301
  if (ret.length > 0 && !hasExplicitFields && addFormulas) {
1323
- meta.props
1324
- .filter(prop => prop.formula && !lazyProps.includes(prop))
1325
- .forEach(prop => {
1326
- const a = this.platform.quoteIdentifier(alias);
1327
- const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
1328
- ret.push(raw(`${prop.formula(a)} as ${aliased}`));
1329
- });
1330
- meta.props
1331
- .filter(prop => !prop.object && (prop.hasConvertToDatabaseValueSQL || prop.hasConvertToJSValueSQL))
1332
- .forEach(prop => ret.push(prop.name));
1302
+ for (const prop of meta.props) {
1303
+ if (lazyProps.includes(prop)) {
1304
+ continue;
1305
+ }
1306
+ if (prop.formula) {
1307
+ const a = this.platform.quoteIdentifier(alias);
1308
+ const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
1309
+ ret.push(raw(`${prop.formula(a)} as ${aliased}`));
1310
+ }
1311
+ if (!prop.object && (prop.hasConvertToDatabaseValueSQL || prop.hasConvertToJSValueSQL)) {
1312
+ ret.push(prop.name);
1313
+ }
1314
+ }
1333
1315
  }
1334
1316
  // add joined relations after the root entity fields
1335
1317
  if (joinedProps.length > 0) {
@@ -43,13 +43,13 @@ export class AbstractSqlPlatform extends Platform {
43
43
  return 'rollback';
44
44
  }
45
45
  getSavepointSQL(savepointName) {
46
- return `savepoint ${savepointName}`;
46
+ return `savepoint ${this.quoteIdentifier(savepointName)}`;
47
47
  }
48
48
  getRollbackToSavepointSQL(savepointName) {
49
- return `rollback to savepoint ${savepointName}`;
49
+ return `rollback to savepoint ${this.quoteIdentifier(savepointName)}`;
50
50
  }
51
51
  getReleaseSavepointSQL(savepointName) {
52
- return `release savepoint ${savepointName}`;
52
+ return `release savepoint ${this.quoteIdentifier(savepointName)}`;
53
53
  }
54
54
  quoteValue(value) {
55
55
  if (isRaw(value)) {
@@ -1,16 +1,17 @@
1
- import { type EntityMetadata, type EntityProperty, type Primary, type Transaction } from '@mikro-orm/core';
1
+ import { type Dictionary, type EntityMetadata, type EntityProperty, type Primary, type Transaction } from '@mikro-orm/core';
2
2
  import { type AbstractSqlDriver } from './AbstractSqlDriver.js';
3
3
  export declare class PivotCollectionPersister<Entity extends object> {
4
4
  private readonly meta;
5
5
  private readonly driver;
6
6
  private readonly ctx?;
7
7
  private readonly schema?;
8
+ private readonly loggerContext?;
8
9
  private readonly platform;
9
10
  private readonly inserts;
10
11
  private readonly deletes;
11
12
  private readonly batchSize;
12
13
  private order;
13
- constructor(meta: EntityMetadata<Entity>, driver: AbstractSqlDriver, ctx?: Transaction | undefined, schema?: string | undefined);
14
+ constructor(meta: EntityMetadata<Entity>, driver: AbstractSqlDriver, ctx?: Transaction | undefined, schema?: string | undefined, loggerContext?: Dictionary | undefined);
14
15
  enqueueUpdate(prop: EntityProperty<Entity>, insertDiff: Primary<Entity>[][], deleteDiff: Primary<Entity>[][] | boolean, pks: Primary<Entity>[]): void;
15
16
  private enqueueInsert;
16
17
  private enqueueDelete;
@@ -38,16 +38,18 @@ export class PivotCollectionPersister {
38
38
  driver;
39
39
  ctx;
40
40
  schema;
41
+ loggerContext;
41
42
  platform;
42
43
  inserts = new Map();
43
44
  deletes = new Map();
44
45
  batchSize;
45
46
  order = 0;
46
- constructor(meta, driver, ctx, schema) {
47
+ constructor(meta, driver, ctx, schema, loggerContext) {
47
48
  this.meta = meta;
48
49
  this.driver = driver;
49
50
  this.ctx = ctx;
50
51
  this.schema = schema;
52
+ this.loggerContext = loggerContext;
51
53
  this.platform = this.driver.getPlatform();
52
54
  this.batchSize = this.driver.config.get('batchSize');
53
55
  }
@@ -99,6 +101,7 @@ export class PivotCollectionPersister {
99
101
  await this.driver.nativeDelete(this.meta.className, cond, {
100
102
  ctx: this.ctx,
101
103
  schema: this.schema,
104
+ loggerContext: this.loggerContext,
102
105
  });
103
106
  }
104
107
  }
@@ -118,13 +121,14 @@ export class PivotCollectionPersister {
118
121
  schema: this.schema,
119
122
  convertCustomTypes: false,
120
123
  processCollections: false,
124
+ loggerContext: this.loggerContext,
121
125
  });
122
126
  }
123
127
  /* v8 ignore start */
124
128
  }
125
129
  else {
126
130
  await Utils.runSerial(items, item => {
127
- return this.driver.createQueryBuilder(this.meta.className, this.ctx, 'write')
131
+ return this.driver.createQueryBuilder(this.meta.className, this.ctx, 'write', false, this.loggerContext)
128
132
  .withSchema(this.schema)
129
133
  .insert(item)
130
134
  .execute('run', false);
package/README.md CHANGED
@@ -11,7 +11,6 @@ TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-or
11
11
  [![Chat on discord](https://img.shields.io/discord/1214904142443839538?label=discord&color=blue)](https://discord.gg/w8bjxFHS7X)
12
12
  [![Downloads](https://img.shields.io/npm/dm/@mikro-orm/core.svg)](https://www.npmjs.com/package/@mikro-orm/core)
13
13
  [![Coverage Status](https://img.shields.io/coveralls/mikro-orm/mikro-orm.svg)](https://coveralls.io/r/mikro-orm/mikro-orm?branch=master)
14
- [![Maintainability](https://api.codeclimate.com/v1/badges/27999651d3adc47cfa40/maintainability)](https://codeclimate.com/github/mikro-orm/mikro-orm/maintainability)
15
14
  [![Build Status](https://github.com/mikro-orm/mikro-orm/workflows/tests/badge.svg?branch=master)](https://github.com/mikro-orm/mikro-orm/actions?workflow=tests)
16
15
 
17
16
  ## 🤔 Unit of What?
@@ -141,7 +140,7 @@ There is also auto-generated [CHANGELOG.md](CHANGELOG.md) file based on commit m
141
140
  - [Composite and Foreign Keys as Primary Key](https://mikro-orm.io/docs/composite-keys)
142
141
  - [Filters](https://mikro-orm.io/docs/filters)
143
142
  - [Using `QueryBuilder`](https://mikro-orm.io/docs/query-builder)
144
- - [Preloading Deeply Nested Structures via populate](https://mikro-orm.io/docs/nested-populate)
143
+ - [Populating relations](https://mikro-orm.io/docs/populating-relations)
145
144
  - [Property Validation](https://mikro-orm.io/docs/property-validation)
146
145
  - [Lifecycle Hooks](https://mikro-orm.io/docs/events#hooks)
147
146
  - [Vanilla JS Support](https://mikro-orm.io/docs/usage-with-js)
@@ -5,6 +5,8 @@ export declare class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
5
5
  sql: string;
6
6
  params: unknown[];
7
7
  };
8
+ protected compileInsert(): void;
9
+ private appendOutputTable;
8
10
  private compileUpsert;
9
11
  protected compileSelect(): void;
10
12
  protected addLockClause(): void;
@@ -12,6 +12,10 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
12
12
  if (this.options.flags?.has(QueryFlag.IDENTITY_INSERT)) {
13
13
  this.parts.push(`set identity_insert ${this.getTableName()} on;`);
14
14
  }
15
+ const { prefix, suffix } = this.appendOutputTable();
16
+ if (prefix) {
17
+ this.parts.push(prefix);
18
+ }
15
19
  if (this.options.comment) {
16
20
  this.parts.push(...this.options.comment.map(comment => `/* ${comment} */`));
17
21
  }
@@ -37,7 +41,11 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
37
41
  this.compileTruncate();
38
42
  break;
39
43
  }
40
- if ([QueryType.INSERT, QueryType.UPDATE, QueryType.DELETE].includes(this.type)) {
44
+ if (suffix) {
45
+ this.parts[this.parts.length - 1] += ';';
46
+ this.parts.push(suffix);
47
+ }
48
+ else if ([QueryType.INSERT, QueryType.UPDATE, QueryType.DELETE].includes(this.type)) {
41
49
  this.parts[this.parts.length - 1] += '; select @@rowcount;';
42
50
  }
43
51
  }
@@ -46,6 +54,37 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
46
54
  }
47
55
  return this.combineParts();
48
56
  }
57
+ compileInsert() {
58
+ if (!this.options.data) {
59
+ throw new Error('No data provided');
60
+ }
61
+ this.parts.push('insert');
62
+ this.addHintComment();
63
+ this.parts.push(`into ${this.getTableName()}`);
64
+ if (Object.keys(this.options.data).length === 0) {
65
+ this.addOutputClause('inserted');
66
+ this.parts.push('default values');
67
+ return;
68
+ }
69
+ const parts = this.processInsertData();
70
+ if (this.options.flags?.has(QueryFlag.OUTPUT_TABLE)) {
71
+ this.parts[this.parts.length - 2] += ' into #out ';
72
+ }
73
+ this.parts.push(parts.join(', '));
74
+ }
75
+ appendOutputTable() {
76
+ if (!this.options.flags?.has(QueryFlag.OUTPUT_TABLE)) {
77
+ return { prefix: '', suffix: '' };
78
+ }
79
+ const returningFields = this.options.returning;
80
+ const selections = returningFields
81
+ .map(field => `[t].${this.platform.quoteIdentifier(field)}`)
82
+ .join(',');
83
+ return {
84
+ prefix: `select top(0) ${selections} into #out from ${this.getTableName()} as t left join ${this.getTableName()} on 0 = 1;`,
85
+ suffix: `select ${selections} from #out as t; drop table #out`,
86
+ };
87
+ }
49
88
  compileUpsert() {
50
89
  const clause = this.options.onConflict;
51
90
  const dataAsArray = Utils.asArray(this.options.data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/knex",
3
- "version": "7.0.0-dev.21",
3
+ "version": "7.0.0-dev.23",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -50,13 +50,13 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "kysely": "0.28.2",
53
+ "kysely": "0.28.5",
54
54
  "sqlstring": "2.3.3"
55
55
  },
56
56
  "devDependencies": {
57
- "@mikro-orm/core": "^6.4.15"
57
+ "@mikro-orm/core": "^6.5.1"
58
58
  },
59
59
  "peerDependencies": {
60
- "@mikro-orm/core": "7.0.0-dev.21"
60
+ "@mikro-orm/core": "7.0.0-dev.23"
61
61
  }
62
62
  }
@@ -66,7 +66,7 @@ export class CriteriaNode {
66
66
  }
67
67
  }
68
68
  renameFieldToPK(qb) {
69
- let joinAlias = qb.getAliasForJoinPath(this.getPath());
69
+ let joinAlias = qb.getAliasForJoinPath(this.getPath(), { matchPopulateJoins: true });
70
70
  if (!joinAlias && this.parent && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind) && this.prop.owner) {
71
71
  joinAlias = qb.getAliasForJoinPath(this.parent.getPath());
72
72
  return Utils.getPrimaryKeyHash(this.prop.joinColumns.map(col => `${joinAlias ?? qb.alias}.${col}`));
@@ -46,10 +46,14 @@ export class CriteriaNodeFactory {
46
46
  static createObjectItemNode(metadata, entityName, node, payload, key, meta) {
47
47
  const prop = meta?.properties[key];
48
48
  const childEntity = prop && prop.kind !== ReferenceKind.SCALAR ? prop.type : entityName;
49
- if (prop?.customType instanceof JsonType) {
49
+ const isNotEmbedded = prop?.kind !== ReferenceKind.EMBEDDED;
50
+ if (isNotEmbedded && prop?.customType instanceof JsonType) {
50
51
  return this.createScalarNode(metadata, childEntity, payload[key], node, key);
51
52
  }
52
- if (prop?.kind !== ReferenceKind.EMBEDDED) {
53
+ if (prop?.kind === ReferenceKind.SCALAR && payload[key] != null && Object.keys(payload[key]).some(f => Utils.isGroupOperator(f))) {
54
+ throw ValidationError.cannotUseGroupOperatorsInsideScalars(entityName, prop.name, payload);
55
+ }
56
+ if (isNotEmbedded) {
53
57
  return this.createNode(metadata, childEntity, payload[key], node, key);
54
58
  }
55
59
  if (payload[key] == null) {
@@ -66,11 +70,12 @@ export class CriteriaNodeFactory {
66
70
  throw ValidationError.cannotUseOperatorsInsideEmbeddables(entityName, prop.name, payload);
67
71
  }
68
72
  const map = Object.keys(payload[key]).reduce((oo, k) => {
69
- if (!prop.embeddedProps[k] && !allowedOperators.includes(k)) {
73
+ const embeddedProp = prop.embeddedProps[k] ?? Object.values(prop.embeddedProps).find(p => p.name === k);
74
+ if (!embeddedProp && !allowedOperators.includes(k)) {
70
75
  throw ValidationError.invalidEmbeddableQuery(entityName, k, prop.type);
71
76
  }
72
- if (prop.embeddedProps[k]) {
73
- oo[prop.embeddedProps[k].name] = payload[key][k];
77
+ if (embeddedProp) {
78
+ oo[embeddedProp.name] = payload[key][k];
74
79
  }
75
80
  else if (typeof payload[key][k] === 'object') {
76
81
  oo[k] = JSON.stringify(payload[key][k]);
@@ -6,7 +6,8 @@ import { JoinType, QueryType } from './enums.js';
6
6
  */
7
7
  export class ObjectCriteriaNode extends CriteriaNode {
8
8
  process(qb, options) {
9
- const nestedAlias = qb.getAliasForJoinPath(this.getPath(), options);
9
+ const matchPopulateJoins = options?.matchPopulateJoins || (this.prop && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind));
10
+ const nestedAlias = qb.getAliasForJoinPath(this.getPath(), { ...options, matchPopulateJoins });
10
11
  const ownerAlias = options?.alias || qb.alias;
11
12
  const keys = Object.keys(this.payload);
12
13
  let alias = options?.alias;
@@ -206,7 +207,23 @@ export class ObjectCriteriaNode extends CriteriaNode {
206
207
  }
207
208
  else {
208
209
  const prev = qb._fields?.slice();
209
- qb[method](field, nestedAlias, undefined, JoinType.leftJoin, path);
210
+ const toOneProperty = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind);
211
+ const joinType = toOneProperty && !this.prop.nullable
212
+ ? JoinType.innerJoin
213
+ : JoinType.leftJoin;
214
+ qb[method](field, nestedAlias, undefined, joinType, path);
215
+ // if the property is nullable, we need to use left join, so we mimic the inner join behaviour
216
+ // with an exclusive condition on the join columns:
217
+ // - if the owning column is null, the row is missing, we don't apply the filter
218
+ // - if the target column is not null, the row is matched, we apply the filter
219
+ if (toOneProperty && this.prop.nullable && options?.filter) {
220
+ qb.andWhere({
221
+ $or: [
222
+ { [field]: null },
223
+ { [nestedAlias + '.' + Utils.getPrimaryKeyHash(this.prop.referencedPKs)]: { $ne: null } },
224
+ ],
225
+ });
226
+ }
210
227
  if (!qb.hasFlag(QueryFlag.INFER_POPULATE)) {
211
228
  qb._fields = prev;
212
229
  }
@@ -290,6 +290,11 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
290
290
  processPopulateHint(): void;
291
291
  private processPopulateWhere;
292
292
  private mergeOnConditions;
293
+ /**
294
+ * When adding an inner join on a left joined relation, we need to nest them,
295
+ * otherwise the inner join could discard rows of the root table.
296
+ */
297
+ private processNestedJoins;
293
298
  private hasToManyJoins;
294
299
  protected wrapPaginateSubQuery(meta: EntityMetadata): void;
295
300
  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: [] };
@@ -882,7 +882,8 @@ export class QueryBuilder {
882
882
  if (field instanceof RawQueryFragment) {
883
883
  field = this.platform.formatQuery(field.sql, field.params);
884
884
  }
885
- this._joins[`${this.alias}.${prop.name}#${alias}`] = {
885
+ const key = `${this.alias}.${prop.name}#${alias}`;
886
+ this._joins[key] = {
886
887
  prop,
887
888
  alias,
888
889
  type,
@@ -891,7 +892,7 @@ export class QueryBuilder {
891
892
  subquery: field.toString(),
892
893
  ownerAlias: this.alias,
893
894
  };
894
- return prop;
895
+ return { prop, key };
895
896
  }
896
897
  if (!subquery && type.includes('lateral')) {
897
898
  throw new Error(`Lateral join can be used only with a sub-query.`);
@@ -920,6 +921,7 @@ export class QueryBuilder {
920
921
  path ??= `${(Object.values(this._joins).find(j => j.alias === fromAlias)?.path ?? entityName)}.${prop.name}`;
921
922
  if (prop.kind === ReferenceKind.ONE_TO_MANY) {
922
923
  this._joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
924
+ this._joins[aliasedName].path ??= path;
923
925
  }
924
926
  else if (prop.kind === ReferenceKind.MANY_TO_MANY) {
925
927
  let pivotAlias = alias;
@@ -931,17 +933,18 @@ export class QueryBuilder {
931
933
  const joins = this.helper.joinManyToManyReference(prop, fromAlias, alias, pivotAlias, type, cond, path, schema);
932
934
  Object.assign(this._joins, joins);
933
935
  this.createAlias(prop.pivotEntity, pivotAlias);
936
+ this._joins[aliasedName].path ??= path;
937
+ aliasedName = Object.keys(joins)[1];
934
938
  }
935
939
  else if (prop.kind === ReferenceKind.ONE_TO_ONE) {
936
940
  this._joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
941
+ this._joins[aliasedName].path ??= path;
937
942
  }
938
943
  else { // MANY_TO_ONE
939
944
  this._joins[aliasedName] = this.helper.joinManyToOneReference(prop, fromAlias, alias, type, cond, schema);
945
+ this._joins[aliasedName].path ??= path;
940
946
  }
941
- if (!this._joins[aliasedName].path && path) {
942
- this._joins[aliasedName].path = path;
943
- }
944
- return prop;
947
+ return { prop, key: aliasedName };
945
948
  }
946
949
  prepareFields(fields, type = 'where') {
947
950
  const ret = [];
@@ -1116,6 +1119,7 @@ export class QueryBuilder {
1116
1119
  const meta = this.mainAlias.metadata;
1117
1120
  this.applyDiscriminatorCondition();
1118
1121
  this.processPopulateHint();
1122
+ this.processNestedJoins();
1119
1123
  if (meta && (this._fields?.includes('*') || this._fields?.includes(`${this.mainAlias.aliasName}.*`))) {
1120
1124
  meta.props
1121
1125
  .filter(prop => prop.formula && (!prop.lazy || this.flags.has(QueryFlag.INCLUDE_LAZY_FORMULAS)))
@@ -1195,7 +1199,7 @@ export class QueryBuilder {
1195
1199
  if (typeof this[key] === 'object') {
1196
1200
  const cond = CriteriaNodeFactory
1197
1201
  .createNode(this.metadata, this.mainAlias.entityName, this[key])
1198
- .process(this, { matchPopulateJoins: true, ignoreBranching: true, preferNoBranch: true });
1202
+ .process(this, { matchPopulateJoins: true, ignoreBranching: true, preferNoBranch: true, filter });
1199
1203
  // there might be new joins created by processing the `populateWhere` object
1200
1204
  joins = Object.values(this._joins);
1201
1205
  this.mergeOnConditions(joins, cond, filter);
@@ -1236,6 +1240,29 @@ export class QueryBuilder {
1236
1240
  }
1237
1241
  }
1238
1242
  }
1243
+ /**
1244
+ * When adding an inner join on a left joined relation, we need to nest them,
1245
+ * otherwise the inner join could discard rows of the root table.
1246
+ */
1247
+ processNestedJoins() {
1248
+ if (this.flags.has(QueryFlag.DISABLE_NESTED_INNER_JOIN)) {
1249
+ return;
1250
+ }
1251
+ const joins = Object.values(this._joins);
1252
+ for (const join of joins) {
1253
+ if (join.type === JoinType.innerJoin) {
1254
+ const parentJoin = joins.find(j => j.alias === join.ownerAlias);
1255
+ // https://stackoverflow.com/a/56815807/3665878
1256
+ if (parentJoin?.type === JoinType.leftJoin || parentJoin?.type === JoinType.nestedLeftJoin) {
1257
+ const nested = (parentJoin.nested ??= new Set());
1258
+ join.type = join.type === JoinType.innerJoin
1259
+ ? JoinType.nestedInnerJoin
1260
+ : JoinType.nestedLeftJoin;
1261
+ nested.add(join);
1262
+ }
1263
+ }
1264
+ }
1265
+ }
1239
1266
  hasToManyJoins() {
1240
1267
  return Object.values(this._joins).some(join => {
1241
1268
  return [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(join.prop.kind);
@@ -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) {
@@ -1,4 +1,4 @@
1
- import { type Configuration, type DeferMode, type Dictionary, type EntityMetadata, type EntityProperty, type NamingStrategy } from '@mikro-orm/core';
1
+ import { type Configuration, type DeferMode, type Dictionary, type EntityMetadata, type EntityProperty, type NamingStrategy, type IndexCallback } from '@mikro-orm/core';
2
2
  import type { SchemaHelper } from './SchemaHelper.js';
3
3
  import type { CheckDef, Column, ForeignKey, IndexDef } from '../typings.js';
4
4
  import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
@@ -54,12 +54,13 @@ export declare class DatabaseTable {
54
54
  private getPropertyTypeForForeignKey;
55
55
  private getPropertyTypeForColumn;
56
56
  private getPropertyDefaultValue;
57
+ private processIndexExpression;
57
58
  addIndex(meta: EntityMetadata, index: {
58
- properties: string | string[];
59
+ properties?: string | string[];
59
60
  name?: string;
60
61
  type?: string;
61
- expression?: string;
62
- deferMode?: DeferMode;
62
+ expression?: string | IndexCallback<any>;
63
+ deferMode?: DeferMode | `${DeferMode}`;
63
64
  options?: Dictionary;
64
65
  }, type: 'index' | 'unique' | 'primary'): void;
65
66
  addCheck(check: CheckDef): void;
@@ -1,4 +1,4 @@
1
- import { Cascade, DecimalType, EntitySchema, ReferenceKind, t, Type, UnknownType, Utils, } from '@mikro-orm/core';
1
+ import { Cascade, DecimalType, EntitySchema, ReferenceKind, t, Type, UnknownType, Utils, RawQueryFragment, } from '@mikro-orm/core';
2
2
  /**
3
3
  * @internal
4
4
  */
@@ -115,6 +115,7 @@ export class DatabaseTable {
115
115
  localTableName: this.getShortestName(false),
116
116
  referencedColumnNames: prop.referencedColumnNames,
117
117
  referencedTableName: schema ? `${schema}.${prop.referencedTableName}` : prop.referencedTableName,
118
+ createForeignKeyConstraint: prop.createForeignKeyConstraint,
118
119
  };
119
120
  const cascade = prop.cascade.includes(Cascade.REMOVE) || prop.cascade.includes(Cascade.ALL);
120
121
  if (prop.deleteRule || cascade || prop.nullable) {
@@ -700,6 +701,23 @@ export class DatabaseTable {
700
701
  }
701
702
  return '' + val;
702
703
  }
704
+ processIndexExpression(indexName, expression, meta) {
705
+ if (expression instanceof Function) {
706
+ const table = {
707
+ name: this.name,
708
+ schema: this.schema,
709
+ toString() {
710
+ if (this.schema) {
711
+ return `${this.schema}.${this.name}`;
712
+ }
713
+ return this.name;
714
+ },
715
+ };
716
+ const exp = expression(table, meta.createColumnMappingObject(), indexName);
717
+ return exp instanceof RawQueryFragment ? this.platform.formatQuery(exp.sql, exp.params) : exp;
718
+ }
719
+ return expression;
720
+ }
703
721
  addIndex(meta, index, type) {
704
722
  const properties = Utils.unique(Utils.flatten(Utils.asArray(index.properties).map(prop => {
705
723
  const parts = prop.split('.');
@@ -741,7 +759,7 @@ export class DatabaseTable {
741
759
  primary: type === 'primary',
742
760
  unique: type !== 'index',
743
761
  type: index.type,
744
- expression: index.expression,
762
+ expression: this.processIndexExpression(name, index.expression, meta),
745
763
  options: index.options,
746
764
  deferMode: index.deferMode,
747
765
  });
@@ -263,7 +263,12 @@ export class SchemaHelper {
263
263
  if (column.autoincrement && !column.generated && !compositePK && (!changedProperties || changedProperties.has('autoincrement') || changedProperties.has('type'))) {
264
264
  Utils.runIfNotEmpty(() => col.push('primary key'), primaryKey && column.primary);
265
265
  }
266
- Utils.runIfNotEmpty(() => col.push(`default ${column.default}`), useDefault);
266
+ if (useDefault) {
267
+ // https://dev.mysql.com/doc/refman/9.0/en/data-type-defaults.html
268
+ const needsExpression = ['blob', 'text', 'json', 'point', 'linestring', 'polygon', 'multipoint', 'multilinestring', 'multipolygon', 'geometrycollection'].some(type => column.type.toLowerCase().startsWith(type));
269
+ const defaultSql = needsExpression && !column.default.startsWith('(') ? `(${column.default})` : column.default;
270
+ col.push(`default ${defaultSql}`);
271
+ }
267
272
  Utils.runIfNotEmpty(() => col.push(column.extra), column.extra);
268
273
  Utils.runIfNotEmpty(() => col.push(`comment ${this.platform.quoteValue(column.comment)}`), column.comment);
269
274
  return col.join(' ');
@@ -421,7 +426,7 @@ export class SchemaHelper {
421
426
  return `alter table ${table.getQuotedName()} comment = ${this.platform.quoteValue(comment ?? '')}`;
422
427
  }
423
428
  createForeignKey(table, foreignKey, alterTable = true, inline = false) {
424
- if (!this.options.createForeignKeyConstraints) {
429
+ if (!this.options.createForeignKeyConstraints || !foreignKey.createForeignKeyConstraint) {
425
430
  return '';
426
431
  }
427
432
  const constraintName = this.quote(foreignKey.constraintName);
@@ -284,7 +284,6 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
284
284
  name ??= this.config.get('dbName');
285
285
  const sql = this.helper.getCreateDatabaseSQL('' + this.platform.quoteIdentifier(name));
286
286
  if (sql) {
287
- // console.log(sql);
288
287
  await this.execute(sql);
289
288
  }
290
289
  this.config.set('dbName', name);
@@ -318,12 +317,10 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
318
317
  if (this.platform.supportsMultipleStatements()) {
319
318
  for (const group of groups) {
320
319
  const query = group.join('\n');
321
- // console.log(query);
322
320
  await this.driver.execute(query);
323
321
  }
324
322
  return;
325
323
  }
326
- // console.log(groups);
327
324
  await Utils.runSerial(groups.flat(), line => this.driver.execute(line));
328
325
  }
329
326
  async dropTableIfExists(name, schema) {
package/typings.d.ts CHANGED
@@ -59,6 +59,7 @@ export interface ForeignKey {
59
59
  updateRule?: string;
60
60
  deleteRule?: string;
61
61
  deferMode?: DeferMode;
62
+ createForeignKeyConstraint: boolean;
62
63
  }
63
64
  export interface IndexDef {
64
65
  columnNames: string[];
@@ -74,7 +75,7 @@ export interface IndexDef {
74
75
  storageEngineIndexType?: 'hash' | 'btree';
75
76
  predicate?: string;
76
77
  }>;
77
- deferMode?: DeferMode;
78
+ deferMode?: DeferMode | `${DeferMode}`;
78
79
  }
79
80
  export interface CheckDef<T = unknown> {
80
81
  name: string;
@@ -171,6 +172,7 @@ export interface ICriteriaNodeProcessOptions {
171
172
  ignoreBranching?: boolean;
172
173
  preferNoBranch?: boolean;
173
174
  type?: 'orderBy';
175
+ filter?: boolean;
174
176
  }
175
177
  export interface ICriteriaNode<T extends object> {
176
178
  readonly entityName: string;