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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { type Knex } from 'knex';
2
- import { Connection, type AnyEntity, type Configuration, type ConnectionOptions, type EntityData, type IsolationLevel, type QueryResult, type Transaction, type TransactionEventBroadcaster, type LoggingOptions } from '@mikro-orm/core';
2
+ import { Connection, type AnyEntity, type Configuration, type ConnectionOptions, type EntityData, type IsolationLevel, type QueryResult, type Transaction, type TransactionEventBroadcaster, type LogContext, type LoggingOptions } from '@mikro-orm/core';
3
3
  import type { AbstractSqlPlatform } from './AbstractSqlPlatform';
4
4
  export declare abstract class AbstractSqlConnection extends Connection {
5
5
  private static __patched;
@@ -33,15 +33,17 @@ export declare abstract class AbstractSqlConnection extends Connection {
33
33
  readOnly?: boolean;
34
34
  ctx?: Knex.Transaction;
35
35
  eventBroadcaster?: TransactionEventBroadcaster;
36
+ loggerContext?: LogContext;
36
37
  }): Promise<T>;
37
38
  begin(options?: {
38
39
  isolationLevel?: IsolationLevel;
39
40
  readOnly?: boolean;
40
41
  ctx?: Knex.Transaction;
41
42
  eventBroadcaster?: TransactionEventBroadcaster;
43
+ loggerContext?: LogContext;
42
44
  }): Promise<Knex.Transaction>;
43
- commit(ctx: Knex.Transaction, eventBroadcaster?: TransactionEventBroadcaster): Promise<void>;
44
- rollback(ctx: Knex.Transaction, eventBroadcaster?: TransactionEventBroadcaster): Promise<void>;
45
+ commit(ctx: Knex.Transaction, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
46
+ rollback(ctx: Knex.Transaction, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
45
47
  execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(queryOrKnex: string | Knex.QueryBuilder | Knex.Raw, params?: unknown[], method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
46
48
  /**
47
49
  * Execute raw SQL queries from file
@@ -56,11 +56,11 @@ class AbstractSqlConnection extends core_1.Connection {
56
56
  const trx = await this.begin(options);
57
57
  try {
58
58
  const ret = await cb(trx);
59
- await this.commit(trx, options.eventBroadcaster);
59
+ await this.commit(trx, options.eventBroadcaster, options.loggerContext);
60
60
  return ret;
61
61
  }
62
62
  catch (error) {
63
- await this.rollback(trx, options.eventBroadcaster);
63
+ await this.rollback(trx, options.eventBroadcaster, options.loggerContext);
64
64
  throw error;
65
65
  }
66
66
  }
@@ -72,6 +72,19 @@ class AbstractSqlConnection extends core_1.Connection {
72
72
  isolationLevel: options.isolationLevel,
73
73
  readOnly: options.readOnly,
74
74
  });
75
+ if (options.ctx) {
76
+ const ctx = options.ctx;
77
+ ctx.index ??= 0;
78
+ const savepointName = `trx${ctx.index + 1}`;
79
+ Reflect.defineProperty(trx, 'index', { value: ctx.index + 1 });
80
+ Reflect.defineProperty(trx, 'savepointName', { value: savepointName });
81
+ this.logQuery(this.platform.getSavepointSQL(savepointName), options.loggerContext);
82
+ }
83
+ else {
84
+ for (const query of this.platform.getBeginTransactionSQL(options)) {
85
+ this.logQuery(query, options.loggerContext);
86
+ }
87
+ }
75
88
  if (!options.ctx) {
76
89
  await options.eventBroadcaster?.dispatchEvent(core_1.EventType.afterTransactionStart, trx);
77
90
  }
@@ -80,23 +93,35 @@ class AbstractSqlConnection extends core_1.Connection {
80
93
  }
81
94
  return trx;
82
95
  }
83
- async commit(ctx, eventBroadcaster) {
96
+ async commit(ctx, eventBroadcaster, loggerContext) {
84
97
  const runTrxHooks = isRootTransaction(ctx);
85
98
  if (runTrxHooks) {
86
99
  await eventBroadcaster?.dispatchEvent(core_1.EventType.beforeTransactionCommit, ctx);
87
100
  }
88
101
  ctx.commit();
89
102
  await ctx.executionPromise; // https://github.com/knex/knex/issues/3847#issuecomment-626330453
103
+ if ('savepointName' in ctx) {
104
+ this.logQuery(this.platform.getReleaseSavepointSQL(ctx.savepointName), loggerContext);
105
+ }
106
+ else {
107
+ this.logQuery(this.platform.getCommitTransactionSQL(), loggerContext);
108
+ }
90
109
  if (runTrxHooks) {
91
110
  await eventBroadcaster?.dispatchEvent(core_1.EventType.afterTransactionCommit, ctx);
92
111
  }
93
112
  }
94
- async rollback(ctx, eventBroadcaster) {
113
+ async rollback(ctx, eventBroadcaster, loggerContext) {
95
114
  const runTrxHooks = isRootTransaction(ctx);
96
115
  if (runTrxHooks) {
97
116
  await eventBroadcaster?.dispatchEvent(core_1.EventType.beforeTransactionRollback, ctx);
98
117
  }
99
118
  await ctx.rollback();
119
+ if ('savepointName' in ctx) {
120
+ this.logQuery(this.platform.getRollbackToSavepointSQL(ctx.savepointName), loggerContext);
121
+ }
122
+ else {
123
+ this.logQuery(this.platform.getRollbackTransactionSQL(), loggerContext);
124
+ }
100
125
  if (runTrxHooks) {
101
126
  await eventBroadcaster?.dispatchEvent(core_1.EventType.afterTransactionRollback, ctx);
102
127
  }
@@ -107,7 +132,7 @@ class AbstractSqlConnection extends core_1.Connection {
107
132
  ctx ??= (queryOrKnex.client.transacting ? queryOrKnex : null);
108
133
  const q = queryOrKnex.toSQL();
109
134
  queryOrKnex = q.sql;
110
- params = q.bindings;
135
+ params = q.bindings ?? [];
111
136
  }
112
137
  queryOrKnex = this.config.get('onQuery')(queryOrKnex, params);
113
138
  const formatted = this.platform.formatQuery(queryOrKnex, params);
@@ -141,11 +166,7 @@ class AbstractSqlConnection extends core_1.Connection {
141
166
  return driverOptions;
142
167
  }
143
168
  return (0, knex_1.knex)(this.getKnexOptions(type))
144
- .on('query', data => {
145
- if (!data.__knexQueryUid) {
146
- this.logQuery(data.sql.toLowerCase().replace(/;$/, ''));
147
- }
148
- });
169
+ .on('query', data => data);
149
170
  }
150
171
  getKnexOptions(type) {
151
172
  const config = core_1.Utils.mergeConfig({
@@ -333,6 +333,9 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
333
333
  if (meta && !core_1.Utils.isEmpty(populate)) {
334
334
  this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, true);
335
335
  }
336
+ if (options.em) {
337
+ await qb.applyJoinedFilters(options.em, options.filters);
338
+ }
336
339
  return this.rethrow(qb.getCount());
337
340
  }
338
341
  async nativeInsert(entityName, data, options = {}) {
@@ -340,7 +343,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
340
343
  const meta = this.metadata.find(entityName);
341
344
  const collections = this.extractManyToMany(entityName, data);
342
345
  const pks = meta?.primaryKeys ?? [this.config.getNamingStrategy().referenceColumnName()];
343
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes).withSchema(this.getSchemaName(meta, options));
346
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
344
347
  const res = await this.rethrow(qb.insert(data).execute('run', false));
345
348
  res.row = res.row || {};
346
349
  let pk;
@@ -401,14 +404,14 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
401
404
  value = this.mapDataToFieldNames(value, false, prop.embeddedProps, options.convertCustomTypes);
402
405
  }
403
406
  }
404
- if (options.convertCustomTypes && prop.customType) {
405
- params.push(prop.customType.convertToDatabaseValue(value, this.platform, { key: prop.name, mode: 'query-data' }));
406
- return;
407
- }
408
407
  if (typeof value === 'undefined' && this.platform.usesDefaultKeyword()) {
409
408
  params.push((0, core_1.raw)('default'));
410
409
  return;
411
410
  }
411
+ if (options.convertCustomTypes && prop.customType) {
412
+ params.push(prop.customType.convertToDatabaseValue(value, this.platform, { key: prop.name, mode: 'query-data' }));
413
+ return;
414
+ }
412
415
  params.push(value);
413
416
  };
414
417
  if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
@@ -467,7 +470,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
467
470
  if (transform) {
468
471
  sql = transform(sql);
469
472
  }
470
- const res = await this.execute(sql, params, 'run', options.ctx);
473
+ const res = await this.execute(sql, params, 'run', options.ctx, options.loggerContext);
471
474
  let pk;
472
475
  /* istanbul ignore next */
473
476
  if (pks.length > 1) { // owner has composite pk
@@ -495,7 +498,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
495
498
  where = { [meta?.primaryKeys[0] ?? pks[0]]: where };
496
499
  }
497
500
  if (core_1.Utils.hasObjectKeys(data)) {
498
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes)
501
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext)
499
502
  .withSchema(this.getSchemaName(meta, options));
500
503
  if (options.upsert) {
501
504
  /* istanbul ignore next */
@@ -534,7 +537,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
534
537
  const meta = this.metadata.get(entityName);
535
538
  if (options.upsert) {
536
539
  const uniqueFields = options.onConflictFields ?? (core_1.Utils.isPlainObject(where[0]) ? Object.keys(where[0]).flatMap(key => core_1.Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
537
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes).withSchema(this.getSchemaName(meta, options));
540
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
538
541
  const returning = (0, core_1.getOnConflictReturningFields)(meta, data[0], uniqueFields, options);
539
542
  qb.insert(data)
540
543
  .onConflict(uniqueFields)
@@ -647,7 +650,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
647
650
  /* istanbul ignore next */
648
651
  sql += returningFields.length > 0 ? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}` : '';
649
652
  }
650
- const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx));
653
+ const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, options.loggerContext));
651
654
  for (let i = 0; i < collections.length; i++) {
652
655
  await this.processManyToMany(meta, where[i], collections[i], false, options);
653
656
  }
@@ -659,7 +662,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
659
662
  if (core_1.Utils.isPrimaryKey(where) && pks.length === 1) {
660
663
  where = { [pks[0]]: where };
661
664
  }
662
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false).delete(where).withSchema(this.getSchemaName(meta, options));
665
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext).delete(where).withSchema(this.getSchemaName(meta, options));
663
666
  return this.rethrow(qb.execute('run', false));
664
667
  }
665
668
  /**
@@ -747,7 +750,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
747
750
  schema = this.config.get('schema');
748
751
  }
749
752
  const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
750
- const persister = groups[tableName] ??= new PivotCollectionPersister_1.PivotCollectionPersister(pivotMeta, this, options?.ctx, schema);
753
+ const persister = groups[tableName] ??= new PivotCollectionPersister_1.PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext);
751
754
  persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks);
752
755
  }
753
756
  for (const persister of core_1.Utils.values(groups)) {
@@ -755,112 +758,64 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
755
758
  }
756
759
  }
757
760
  async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
761
+ if (owners.length === 0) {
762
+ return {};
763
+ }
758
764
  const pivotMeta = this.metadata.find(prop.pivotEntity);
759
765
  const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
760
766
  const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
761
767
  const ownerMeta = this.metadata.find(pivotProp2.type);
762
- options = { ...options };
763
- const qb = this.createQueryBuilder(prop.pivotEntity, ctx, options.connectionType, undefined, options?.logging)
764
- .withSchema(this.getSchemaName(pivotMeta, options))
765
- .indexHint(options.indexHint)
766
- .comment(options.comments)
767
- .hintComment(options.hintComments);
768
- const pivotAlias = qb.alias;
769
- const pivotKey = pivotProp2.joinColumns.map(column => `${pivotAlias}.${column}`).join(core_1.Utils.PK_SEPARATOR);
770
768
  const cond = {
771
- [pivotKey]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) },
769
+ [pivotProp2.name]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) },
772
770
  };
773
- /* istanbul ignore if */
774
- if (!core_1.Utils.isEmpty(where) && Object.keys(where).every(k => core_1.Utils.isOperator(k, false))) {
775
- where = cond;
776
- }
777
- else {
778
- where = { ...where, ...cond };
779
- }
780
- orderBy = this.getPivotOrderBy(prop, pivotProp1, pivotAlias, orderBy);
781
- const populate = this.autoJoinOneToOneOwner(prop.targetMeta, []);
782
- const fields = [];
783
- const k1 = !prop.owner ? 'joinColumns' : 'inverseJoinColumns';
784
- const k2 = prop.owner ? 'joinColumns' : 'inverseJoinColumns';
785
- const cols = [
786
- ...prop[k1].map(col => `${pivotAlias}.${col} as fk__${col}`),
787
- ...prop[k2].map(col => `${pivotAlias}.${col} as fk__${col}`),
788
- ];
789
- fields.push(...cols);
790
- if (!pivotJoin) {
791
- const targetAlias = qb.getNextAlias(prop.targetMeta.tableName);
792
- const targetSchema = this.getSchemaName(prop.targetMeta, options) ?? this.platform.getDefaultSchemaName();
793
- qb.innerJoin(pivotProp1.name, targetAlias, {}, targetSchema);
794
- const targetFields = this.buildFields(prop.targetMeta, (options.populate ?? []), [], qb, targetAlias, options);
795
- const additionalFields = [];
796
- for (const field of targetFields) {
797
- const f = field.toString();
798
- additionalFields.push(f.includes('.') ? field : `${targetAlias}.${f}`);
799
- if (core_1.RawQueryFragment.isKnownFragment(field)) {
800
- qb.rawFragments.add(f);
801
- }
802
- }
803
- fields.unshift(...additionalFields);
804
- // we need to handle 1:1 owner auto-joins explicitly, as the QB type is the pivot table, not the target
805
- populate.forEach(hint => {
806
- const alias = qb.getNextAlias(prop.targetMeta.tableName);
807
- qb.leftJoin(`${targetAlias}.${hint.field}`, alias);
808
- // eslint-disable-next-line dot-notation
809
- for (const join of Object.values(qb['_joins'])) {
810
- const [propName] = hint.field.split(':', 2);
811
- if (join.alias === alias && join.prop.name === propName) {
812
- fields.push(...qb.helper.mapJoinColumns(qb.type, join));
813
- }
814
- }
815
- });
816
- }
817
- qb.select(fields)
818
- .where({ [pivotProp1.name]: where })
819
- .orderBy(orderBy)
820
- .setLockMode(options.lockMode, options.lockTableAliases);
821
- if (owners.length === 1 && (options.offset != null || options.limit != null)) {
822
- qb.limit(options.limit, options.offset);
823
- }
824
- const res = owners.length ? await this.rethrow(qb.execute('all', { mergeResults: false, mapResults: false })) : [];
825
- const tmp = {};
826
- const items = res.map((row) => {
827
- const root = super.mapResult(row, prop.targetMeta);
828
- this.mapJoinedProps(root, prop.targetMeta, populate, qb, root, tmp, pivotMeta.className + '.' + pivotProp1.name);
829
- return root;
771
+ if (!core_1.Utils.isEmpty(where)) {
772
+ cond[pivotProp1.name] = { ...where };
773
+ }
774
+ where = cond;
775
+ const populateField = pivotJoin ? `${pivotProp1.name}:ref` : pivotProp1.name;
776
+ const populate = this.autoJoinOneToOneOwner(prop.targetMeta, options?.populate ?? [], options?.fields);
777
+ const childFields = !core_1.Utils.isEmpty(options?.fields) ? options.fields.map(f => `${pivotProp1.name}.${f}`) : [];
778
+ const childExclude = !core_1.Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${pivotProp1.name}.${f}`) : [];
779
+ const fields = pivotJoin
780
+ ? [pivotProp1.name, pivotProp2.name]
781
+ : [pivotProp1.name, pivotProp2.name, ...childFields];
782
+ const res = await this.find(pivotMeta.className, where, {
783
+ ctx,
784
+ ...options,
785
+ fields,
786
+ exclude: childExclude,
787
+ orderBy: this.getPivotOrderBy(prop, pivotProp1, orderBy, options?.orderBy),
788
+ populate: [{ field: populateField, strategy: core_1.LoadStrategy.JOINED, joinType: query_1.JoinType.innerJoin, children: populate }],
789
+ populateWhere: undefined,
790
+ // @ts-ignore
791
+ _populateWhere: 'infer',
792
+ populateFilter: !core_1.Utils.isEmpty(options?.populateFilter) ? { [pivotProp2.name]: options?.populateFilter } : undefined,
830
793
  });
831
- qb.clearRawFragmentsCache();
832
794
  const map = {};
833
- const pkProps = ownerMeta.getPrimaryProps();
834
795
  for (const owner of owners) {
835
- const key = core_1.Utils.getPrimaryKeyHash(prop.joinColumns.map((_col, idx) => {
836
- const pkProp = pkProps[idx];
837
- return pkProp.customType ? pkProp.customType.convertToJSValue(owner[idx], this.platform) : owner[idx];
838
- }));
796
+ const key = core_1.Utils.getPrimaryKeyHash(owner);
839
797
  map[key] = [];
840
798
  }
841
- for (const item of items) {
842
- const key = core_1.Utils.getPrimaryKeyHash(prop.joinColumns.map((col, idx) => {
843
- const pkProp = pkProps[idx];
844
- return pkProp.customType ? pkProp.customType.convertToJSValue(item[`fk__${col}`], this.platform) : item[`fk__${col}`];
845
- }));
846
- map[key].push(item);
847
- prop.joinColumns.forEach(col => delete item[`fk__${col}`]);
848
- prop.inverseJoinColumns.forEach((col, idx) => {
849
- core_1.Utils.renameKey(item, `fk__${col}`, prop.targetMeta.primaryKeys[idx]);
850
- });
799
+ for (const item of res) {
800
+ const key = core_1.Utils.getPrimaryKeyHash(core_1.Utils.asArray(item[pivotProp2.name]));
801
+ map[key].push(item[pivotProp1.name]);
851
802
  }
852
803
  return map;
853
804
  }
854
- getPivotOrderBy(prop, pivotProp, pivotAlias, orderBy) {
855
- // FIXME this is ignoring the rest of the array items
805
+ getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
856
806
  if (!core_1.Utils.isEmpty(orderBy)) {
857
- return [{ [pivotProp.name]: core_1.Utils.asArray(orderBy)[0] }];
807
+ return core_1.Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
808
+ }
809
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && core_1.Utils.asArray(parentOrderBy).some(o => o[prop.name])) {
810
+ return core_1.Utils.asArray(parentOrderBy)
811
+ .filter(o => o[prop.name])
812
+ .map(o => ({ [pivotProp.name]: o[prop.name] }));
858
813
  }
859
814
  if (!core_1.Utils.isEmpty(prop.orderBy)) {
860
- return [{ [pivotProp.name]: core_1.Utils.asArray(prop.orderBy)[0] }];
815
+ return core_1.Utils.asArray(prop.orderBy).map(o => ({ [pivotProp.name]: o }));
861
816
  }
862
817
  if (prop.fixedOrder) {
863
- return [{ [`${pivotAlias}.${prop.fixedOrderColumn}`]: core_1.QueryOrder.ASC }];
818
+ return [{ [prop.fixedOrderColumn]: core_1.QueryOrder.ASC }];
864
819
  }
865
820
  return [];
866
821
  }
@@ -878,7 +833,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
878
833
  const toPopulate = meta.relations
879
834
  .filter(prop => prop.kind === core_1.ReferenceKind.ONE_TO_ONE && !prop.owner && !prop.lazy && !relationsToPopulate.includes(prop.name))
880
835
  .filter(prop => fields.length === 0 || fields.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)))
881
- .map(prop => ({ field: `${prop.name}:ref`, strategy: prop.strategy }));
836
+ .map(prop => ({ field: `${prop.name}:ref`, strategy: core_1.LoadStrategy.JOINED }));
882
837
  return [...populate, ...toPopulate];
883
838
  }
884
839
  /**
@@ -886,16 +841,17 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
886
841
  */
887
842
  joinedProps(meta, populate, options) {
888
843
  return populate.filter(hint => {
889
- const [propName, ref] = hint.field.split(':', 2);
844
+ const [propName] = hint.field.split(':', 2);
890
845
  const prop = meta.properties[propName] || {};
891
- if (hint.filter && hint.strategy === core_1.LoadStrategy.JOINED) {
846
+ const strategy = (0, core_1.getLoadingStrategy)(hint.strategy || prop.strategy || options?.strategy || this.config.get('loadStrategy'), prop.kind);
847
+ if (hint.filter && [core_1.ReferenceKind.ONE_TO_ONE, core_1.ReferenceKind.MANY_TO_ONE].includes(prop.kind) && !prop.nullable) {
892
848
  return true;
893
849
  }
894
850
  // skip redundant joins for 1:1 owner population hints when using `mapToPk`
895
851
  if (prop.kind === core_1.ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
896
852
  return false;
897
853
  }
898
- if ((options?.strategy || hint.strategy || prop.strategy || this.config.get('loadStrategy')) !== core_1.LoadStrategy.JOINED) {
854
+ if (strategy !== core_1.LoadStrategy.JOINED) {
899
855
  // force joined strategy for explicit 1:1 owner populate hint as it would require a join anyway
900
856
  return prop.kind === core_1.ReferenceKind.ONE_TO_ONE && !prop.owner;
901
857
  }
@@ -906,31 +862,26 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
906
862
  * @internal
907
863
  */
908
864
  mergeJoinedResult(rawResults, meta, joinedProps) {
865
+ if (rawResults.length <= 1) {
866
+ return rawResults;
867
+ }
909
868
  const res = [];
910
869
  const map = {};
870
+ const collectionsToMerge = {};
871
+ const hints = joinedProps.map(hint => {
872
+ const [propName, ref] = hint.field.split(':', 2);
873
+ return { propName, ref, children: hint.children };
874
+ });
911
875
  for (const item of rawResults) {
912
876
  const pk = core_1.Utils.getCompositeKeyHash(item, meta);
913
877
  if (map[pk]) {
914
- for (const hint of joinedProps) {
915
- const [propName, ref] = hint.field.split(':', 2);
916
- const prop = meta.properties[propName];
878
+ for (const { propName } of hints) {
917
879
  if (!item[propName]) {
918
880
  continue;
919
881
  }
920
- if ([core_1.ReferenceKind.ONE_TO_MANY, core_1.ReferenceKind.MANY_TO_MANY].includes(prop.kind) && ref) {
921
- map[pk][propName] = [...map[pk][propName], ...item[propName]];
922
- continue;
923
- }
924
- switch (prop.kind) {
925
- case core_1.ReferenceKind.ONE_TO_MANY:
926
- case core_1.ReferenceKind.MANY_TO_MANY:
927
- map[pk][propName] = this.mergeJoinedResult([...map[pk][propName], ...item[propName]], prop.targetMeta, hint.children ?? []);
928
- break;
929
- case core_1.ReferenceKind.MANY_TO_ONE:
930
- case core_1.ReferenceKind.ONE_TO_ONE:
931
- map[pk][propName] = this.mergeJoinedResult([map[pk][propName], item[propName]], prop.targetMeta, hint.children ?? [])[0];
932
- break;
933
- }
882
+ collectionsToMerge[pk] ??= {};
883
+ collectionsToMerge[pk][propName] ??= [map[pk][propName]];
884
+ collectionsToMerge[pk][propName].push(item[propName]);
934
885
  }
935
886
  }
936
887
  else {
@@ -938,6 +889,31 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
938
889
  res.push(item);
939
890
  }
940
891
  }
892
+ for (const pk in collectionsToMerge) {
893
+ const entity = map[pk];
894
+ const collections = collectionsToMerge[pk];
895
+ for (const { propName, ref, children } of hints) {
896
+ if (!collections[propName]) {
897
+ continue;
898
+ }
899
+ const prop = meta.properties[propName];
900
+ const items = collections[propName].flat();
901
+ if ([core_1.ReferenceKind.ONE_TO_MANY, core_1.ReferenceKind.MANY_TO_MANY].includes(prop.kind) && ref) {
902
+ entity[propName] = items;
903
+ continue;
904
+ }
905
+ switch (prop.kind) {
906
+ case core_1.ReferenceKind.ONE_TO_MANY:
907
+ case core_1.ReferenceKind.MANY_TO_MANY:
908
+ entity[propName] = this.mergeJoinedResult(items, prop.targetMeta, children ?? []);
909
+ break;
910
+ case core_1.ReferenceKind.MANY_TO_ONE:
911
+ case core_1.ReferenceKind.ONE_TO_ONE:
912
+ entity[propName] = this.mergeJoinedResult(items, prop.targetMeta, children ?? [])[0];
913
+ break;
914
+ }
915
+ }
916
+ }
941
917
  return res;
942
918
  }
943
919
  getFieldsForJoinedLoad(qb, meta, explicitFields, exclude, populate = [], options, parentTableAlias, parentJoinPath, count) {
@@ -964,7 +940,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
964
940
  const [propName, ref] = hint.field.split(':', 2);
965
941
  const prop = meta.properties[propName];
966
942
  // ignore ref joins of known FKs unless it's a filter hint
967
- if (ref && !hint.filter && (prop.kind === core_1.ReferenceKind.MANY_TO_ONE || (prop.kind === core_1.ReferenceKind.ONE_TO_ONE && !prop.owner))) {
943
+ if (ref && !hint.filter && (prop.kind === core_1.ReferenceKind.MANY_TO_ONE || (prop.kind === core_1.ReferenceKind.ONE_TO_ONE && prop.owner))) {
968
944
  continue;
969
945
  }
970
946
  if (count && (!options?.populateFilter || !options.populateFilter[prop.name])) {
@@ -978,11 +954,14 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
978
954
  if (!parentJoinPath && populateWhereAll && !hint.filter && !path.startsWith('[populate]')) {
979
955
  path = '[populate]' + path;
980
956
  }
957
+ const mandatoryToOneProperty = [core_1.ReferenceKind.MANY_TO_ONE, core_1.ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.nullable;
981
958
  const joinType = pivotRefJoin
982
959
  ? query_1.JoinType.pivotJoin
983
- : hint.filter && !prop.nullable
984
- ? query_1.JoinType.innerJoin
985
- : query_1.JoinType.leftJoin;
960
+ : hint.joinType
961
+ ? hint.joinType
962
+ : (hint.filter && !prop.nullable) || mandatoryToOneProperty
963
+ ? query_1.JoinType.innerJoin
964
+ : query_1.JoinType.leftJoin;
986
965
  qb.join(field, tableAlias, {}, joinType, path);
987
966
  if (pivotRefJoin) {
988
967
  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}`)));
@@ -1000,7 +979,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
1000
979
  if (!ref && !prop.mapToPk) {
1001
980
  fields.push(...this.getFieldsForJoinedLoad(qb, meta2, childExplicitFields.length === 0 ? undefined : childExplicitFields, childExclude, hint.children, options, tableAlias, path, count));
1002
981
  }
1003
- else if (hint.filter || prop.mapToPk) {
982
+ else if (hint.filter || prop.mapToPk || (ref && [core_1.ReferenceKind.MANY_TO_ONE, core_1.ReferenceKind.ONE_TO_ONE].includes(prop.kind))) {
1004
983
  fields.push(...prop.referencedColumnNames.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)));
1005
984
  }
1006
985
  }
@@ -1078,7 +1057,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
1078
1057
  for (const prop of meta.relations) {
1079
1058
  if (collections[prop.name]) {
1080
1059
  const pivotMeta = this.metadata.find(prop.pivotEntity);
1081
- const persister = new PivotCollectionPersister_1.PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema);
1060
+ const persister = new PivotCollectionPersister_1.PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext);
1082
1061
  persister.enqueueUpdate(prop, collections[prop.name], clear, pks);
1083
1062
  await this.rethrow(persister.execute());
1084
1063
  }
@@ -1145,7 +1124,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
1145
1124
  let path = parentPath;
1146
1125
  const meta2 = this.metadata.find(prop.type);
1147
1126
  const childOrder = orderHint[prop.name];
1148
- if (![core_1.ReferenceKind.MANY_TO_ONE, core_1.ReferenceKind.ONE_TO_ONE].includes(prop.kind) || !prop.owner || core_1.Utils.isPlainObject(childOrder)) {
1127
+ if (prop.kind !== core_1.ReferenceKind.SCALAR && (![core_1.ReferenceKind.MANY_TO_ONE, core_1.ReferenceKind.ONE_TO_ONE].includes(prop.kind) || !prop.owner || core_1.Utils.isPlainObject(childOrder))) {
1149
1128
  path += `.${propName}`;
1150
1129
  }
1151
1130
  if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && typeof childOrder !== 'object') {
@@ -1153,10 +1132,10 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
1153
1132
  }
1154
1133
  const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
1155
1134
  const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true }) ?? parentAlias;
1156
- if (!join && parentAlias === qb.alias) {
1135
+ if (!join) {
1157
1136
  continue;
1158
1137
  }
1159
- if (![core_1.ReferenceKind.SCALAR, core_1.ReferenceKind.EMBEDDED].includes(prop.kind) && typeof childOrder === 'object') {
1138
+ if (join && ![core_1.ReferenceKind.SCALAR, core_1.ReferenceKind.EMBEDDED].includes(prop.kind) && typeof childOrder === 'object') {
1160
1139
  const children = this.buildPopulateOrderBy(qb, meta2, core_1.Utils.asArray(childOrder), path, explicit, propAlias);
1161
1140
  orderBy.push(...children);
1162
1141
  continue;
@@ -1311,16 +1290,19 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
1311
1290
  ret.push('*');
1312
1291
  }
1313
1292
  if (ret.length > 0 && !hasExplicitFields && addFormulas) {
1314
- meta.props
1315
- .filter(prop => prop.formula && !lazyProps.includes(prop))
1316
- .forEach(prop => {
1317
- const a = this.platform.quoteIdentifier(alias);
1318
- const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
1319
- ret.push((0, core_1.raw)(`${prop.formula(a)} as ${aliased}`));
1320
- });
1321
- meta.props
1322
- .filter(prop => !prop.object && (prop.hasConvertToDatabaseValueSQL || prop.hasConvertToJSValueSQL))
1323
- .forEach(prop => ret.push(prop.name));
1293
+ for (const prop of meta.props) {
1294
+ if (lazyProps.includes(prop)) {
1295
+ continue;
1296
+ }
1297
+ if (prop.formula) {
1298
+ const a = this.platform.quoteIdentifier(alias);
1299
+ const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
1300
+ ret.push((0, core_1.raw)(`${prop.formula(a)} as ${aliased}`));
1301
+ }
1302
+ if (!prop.object && (prop.hasConvertToDatabaseValueSQL || prop.hasConvertToJSValueSQL)) {
1303
+ ret.push(prop.name);
1304
+ }
1305
+ }
1324
1306
  }
1325
1307
  // add joined relations after the root entity fields
1326
1308
  if (joinedProps.length > 0) {
@@ -1,4 +1,4 @@
1
- import { Platform, type Constructor, type EntityManager, type EntityRepository, type IDatabaseDriver, type MikroORM } from '@mikro-orm/core';
1
+ import { Platform, type Constructor, type EntityManager, type EntityRepository, type IDatabaseDriver, type MikroORM, type IsolationLevel } from '@mikro-orm/core';
2
2
  import { SqlSchemaGenerator, type SchemaHelper } from './schema';
3
3
  import type { IndexDef } from './typings';
4
4
  export declare abstract class AbstractSqlPlatform extends Platform {
@@ -10,6 +10,15 @@ export declare abstract class AbstractSqlPlatform extends Platform {
10
10
  /** @inheritDoc */
11
11
  lookupExtensions(orm: MikroORM): void;
12
12
  getSchemaGenerator(driver: IDatabaseDriver, em?: EntityManager): SqlSchemaGenerator;
13
+ getBeginTransactionSQL(options?: {
14
+ isolationLevel?: IsolationLevel;
15
+ readOnly?: boolean;
16
+ }): string[];
17
+ getCommitTransactionSQL(): string;
18
+ getRollbackTransactionSQL(): string;
19
+ getSavepointSQL(savepointName: string): string;
20
+ getRollbackToSavepointSQL(savepointName: string): string;
21
+ getReleaseSavepointSQL(savepointName: string): string;
13
22
  quoteValue(value: any): string;
14
23
  escape(value: any): string;
15
24
  getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string;
@@ -27,6 +27,27 @@ class AbstractSqlPlatform extends core_1.Platform {
27
27
  getSchemaGenerator(driver, em) {
28
28
  return new schema_1.SqlSchemaGenerator(em ?? driver);
29
29
  }
30
+ getBeginTransactionSQL(options) {
31
+ if (options?.isolationLevel) {
32
+ return [`set transaction isolation level ${options.isolationLevel}`, 'begin'];
33
+ }
34
+ return ['begin'];
35
+ }
36
+ getCommitTransactionSQL() {
37
+ return 'commit';
38
+ }
39
+ getRollbackTransactionSQL() {
40
+ return 'rollback';
41
+ }
42
+ getSavepointSQL(savepointName) {
43
+ return `savepoint ${this.quoteIdentifier(savepointName)}`;
44
+ }
45
+ getRollbackToSavepointSQL(savepointName) {
46
+ return `rollback to savepoint ${this.quoteIdentifier(savepointName)}`;
47
+ }
48
+ getReleaseSavepointSQL(savepointName) {
49
+ return `release savepoint ${this.quoteIdentifier(savepointName)}`;
50
+ }
30
51
  quoteValue(value) {
31
52
  if (core_1.Utils.isRawSql(value)) {
32
53
  return this.formatQuery(value.sql, value.params ?? []);
@@ -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';
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;
@@ -41,16 +41,18 @@ class PivotCollectionPersister {
41
41
  driver;
42
42
  ctx;
43
43
  schema;
44
+ loggerContext;
44
45
  platform;
45
46
  inserts = new Map();
46
47
  deletes = new Map();
47
48
  batchSize;
48
49
  order = 0;
49
- constructor(meta, driver, ctx, schema) {
50
+ constructor(meta, driver, ctx, schema, loggerContext) {
50
51
  this.meta = meta;
51
52
  this.driver = driver;
52
53
  this.ctx = ctx;
53
54
  this.schema = schema;
55
+ this.loggerContext = loggerContext;
54
56
  this.platform = this.driver.getPlatform();
55
57
  this.batchSize = this.driver.config.get('batchSize');
56
58
  }
@@ -102,6 +104,7 @@ class PivotCollectionPersister {
102
104
  await this.driver.nativeDelete(this.meta.className, cond, {
103
105
  ctx: this.ctx,
104
106
  schema: this.schema,
107
+ loggerContext: this.loggerContext,
105
108
  });
106
109
  }
107
110
  }
@@ -122,12 +125,13 @@ class PivotCollectionPersister {
122
125
  schema: this.schema,
123
126
  convertCustomTypes: false,
124
127
  processCollections: false,
128
+ loggerContext: this.loggerContext,
125
129
  });
126
130
  }
127
131
  }
128
132
  else {
129
133
  await core_1.Utils.runSerial(items, item => {
130
- return this.driver.createQueryBuilder(this.meta.className, this.ctx, 'write')
134
+ return this.driver.createQueryBuilder(this.meta.className, this.ctx, 'write', false, this.loggerContext)
131
135
  .withSchema(this.schema)
132
136
  .insert(item)
133
137
  .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)
@@ -1,4 +1,4 @@
1
- import { type SimpleColumnMeta, type Type, type TransformContext } from '@mikro-orm/core';
1
+ import { type SimpleColumnMeta, type Type, type TransformContext, type IsolationLevel } from '@mikro-orm/core';
2
2
  import { MySqlSchemaHelper } from './MySqlSchemaHelper';
3
3
  import { MySqlExceptionConverter } from './MySqlExceptionConverter';
4
4
  import { AbstractSqlPlatform } from '../../AbstractSqlPlatform';
@@ -13,6 +13,10 @@ export declare class MySqlPlatform extends AbstractSqlPlatform {
13
13
  readonly "desc nulls last": "is null";
14
14
  };
15
15
  getDefaultCharset(): string;
16
+ getBeginTransactionSQL(options?: {
17
+ isolationLevel?: IsolationLevel;
18
+ readOnly?: boolean;
19
+ }): string[];
16
20
  convertJsonToDatabaseValue(value: unknown, context?: TransformContext): unknown;
17
21
  getJsonIndexDefinition(index: IndexDef): string[];
18
22
  getBooleanTypeDeclarationSQL(): string;
@@ -17,6 +17,20 @@ class MySqlPlatform extends AbstractSqlPlatform_1.AbstractSqlPlatform {
17
17
  getDefaultCharset() {
18
18
  return 'utf8mb4';
19
19
  }
20
+ getBeginTransactionSQL(options) {
21
+ if (options?.isolationLevel || options?.readOnly) {
22
+ const parts = [];
23
+ if (options.isolationLevel) {
24
+ parts.push(`isolation level ${options.isolationLevel}`);
25
+ }
26
+ if (options.readOnly) {
27
+ parts.push('read only');
28
+ }
29
+ const sql = `set transaction ${parts.join(', ')}`;
30
+ return [sql, 'begin'];
31
+ }
32
+ return ['begin'];
33
+ }
20
34
  convertJsonToDatabaseValue(value, context) {
21
35
  if (context?.mode === 'query') {
22
36
  return value;
@@ -204,7 +204,14 @@ class MySqlSchemaHelper extends SchemaHelper_1.SchemaHelper {
204
204
  col.defaultTo(null);
205
205
  }
206
206
  else {
207
- col.defaultTo(knex.raw(column.default + (column.extra ? ' ' + column.extra : '')));
207
+ const columnType = column.type.toLowerCase();
208
+ // https://dev.mysql.com/doc/refman/9.0/en/data-type-defaults.html
209
+ const needsExpression = ['blob', 'text', 'json', 'point', 'linestring', 'polygon', 'multipoint', 'multilinestring', 'multipolygon', 'geometrycollection'].some(type => columnType.startsWith(type));
210
+ let defaultSql = needsExpression && !column.default.startsWith('(') ? `(${column.default})` : column.default;
211
+ if (column.extra) {
212
+ defaultSql += ' ' + column.extra;
213
+ }
214
+ col.defaultTo(knex.raw(defaultSql));
208
215
  }
209
216
  }
210
217
  return col;
@@ -1,4 +1,4 @@
1
- import { type EntityProperty } from '@mikro-orm/core';
1
+ import { type EntityProperty, type IsolationLevel } from '@mikro-orm/core';
2
2
  import { AbstractSqlPlatform } from '../../AbstractSqlPlatform';
3
3
  export declare abstract class BaseSqlitePlatform extends AbstractSqlPlatform {
4
4
  usesDefaultKeyword(): boolean;
@@ -7,6 +7,10 @@ export declare abstract class BaseSqlitePlatform extends AbstractSqlPlatform {
7
7
  getDateTimeTypeDeclarationSQL(column: {
8
8
  length: number;
9
9
  }): string;
10
+ getBeginTransactionSQL(options?: {
11
+ isolationLevel?: IsolationLevel;
12
+ readOnly?: boolean;
13
+ }): string[];
10
14
  getEnumTypeDeclarationSQL(column: {
11
15
  items?: unknown[];
12
16
  fieldNames: string[];
@@ -16,6 +16,9 @@ class BaseSqlitePlatform extends AbstractSqlPlatform_1.AbstractSqlPlatform {
16
16
  getDateTimeTypeDeclarationSQL(column) {
17
17
  return 'datetime';
18
18
  }
19
+ getBeginTransactionSQL(options) {
20
+ return ['begin'];
21
+ }
19
22
  getEnumTypeDeclarationSQL(column) {
20
23
  if (column.items?.every(item => core_1.Utils.isString(item))) {
21
24
  return 'text';
package/index.mjs CHANGED
@@ -219,12 +219,16 @@ export const compareBuffers = mod.compareBuffers;
219
219
  export const compareObjects = mod.compareObjects;
220
220
  export const createSqlFunction = mod.createSqlFunction;
221
221
  export const defineConfig = mod.defineConfig;
222
+ export const defineEntity = mod.defineEntity;
222
223
  export const equals = mod.equals;
224
+ export const expandDotPaths = mod.expandDotPaths;
225
+ export const getLoadingStrategy = mod.getLoadingStrategy;
223
226
  export const getOnConflictFields = mod.getOnConflictFields;
224
227
  export const getOnConflictReturningFields = mod.getOnConflictReturningFields;
225
228
  export const helper = mod.helper;
226
229
  export const knex = mod.knex;
227
230
  export const parseJsonSafe = mod.parseJsonSafe;
231
+ export const quote = mod.quote;
228
232
  export const raw = mod.raw;
229
233
  export const ref = mod.ref;
230
234
  export const rel = mod.rel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/knex",
3
- "version": "6.4.17-dev.9",
3
+ "version": "6.4.17-dev.90",
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
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -58,7 +58,7 @@
58
58
  "access": "public"
59
59
  },
60
60
  "dependencies": {
61
- "fs-extra": "11.3.0",
61
+ "fs-extra": "11.3.1",
62
62
  "knex": "3.1.0",
63
63
  "sqlstring": "2.3.3"
64
64
  },
@@ -66,7 +66,7 @@
66
66
  "@mikro-orm/core": "^6.4.16"
67
67
  },
68
68
  "peerDependencies": {
69
- "@mikro-orm/core": "6.4.17-dev.9",
69
+ "@mikro-orm/core": "6.4.17-dev.90",
70
70
  "better-sqlite3": "*",
71
71
  "libsql": "*",
72
72
  "mariadb": "*"
@@ -69,7 +69,7 @@ class CriteriaNode {
69
69
  }
70
70
  }
71
71
  renameFieldToPK(qb) {
72
- let joinAlias = qb.getAliasForJoinPath(this.getPath());
72
+ let joinAlias = qb.getAliasForJoinPath(this.getPath(), { matchPopulateJoins: true });
73
73
  if (!joinAlias && this.parent && [core_1.ReferenceKind.MANY_TO_ONE, core_1.ReferenceKind.ONE_TO_ONE].includes(this.prop.kind) && this.prop.owner) {
74
74
  joinAlias = qb.getAliasForJoinPath(this.parent.getPath());
75
75
  return core_1.Utils.getPrimaryKeyHash(this.prop.joinColumns.map(col => `${joinAlias ?? qb.alias}.${col}`));
@@ -49,13 +49,14 @@ class CriteriaNodeFactory {
49
49
  static createObjectItemNode(metadata, entityName, node, payload, key, meta) {
50
50
  const prop = meta?.properties[key];
51
51
  const childEntity = prop && prop.kind !== core_1.ReferenceKind.SCALAR ? prop.type : entityName;
52
- if (prop?.customType instanceof core_1.JsonType) {
52
+ const isNotEmbedded = prop?.kind !== core_1.ReferenceKind.EMBEDDED;
53
+ if (isNotEmbedded && prop?.customType instanceof core_1.JsonType) {
53
54
  return this.createScalarNode(metadata, childEntity, payload[key], node, key);
54
55
  }
55
56
  if (prop?.kind === core_1.ReferenceKind.SCALAR && payload[key] != null && Object.keys(payload[key]).some(f => core_1.Utils.isGroupOperator(f))) {
56
57
  throw core_1.ValidationError.cannotUseGroupOperatorsInsideScalars(entityName, prop.name, payload);
57
58
  }
58
- if (prop?.kind !== core_1.ReferenceKind.EMBEDDED) {
59
+ if (isNotEmbedded) {
59
60
  return this.createNode(metadata, childEntity, payload[key], node, key);
60
61
  }
61
62
  if (payload[key] == null) {
@@ -72,11 +73,12 @@ class CriteriaNodeFactory {
72
73
  throw core_1.ValidationError.cannotUseOperatorsInsideEmbeddables(entityName, prop.name, payload);
73
74
  }
74
75
  const map = Object.keys(payload[key]).reduce((oo, k) => {
75
- if (!prop.embeddedProps[k] && !allowedOperators.includes(k)) {
76
+ const embeddedProp = prop.embeddedProps[k] ?? Object.values(prop.embeddedProps).find(p => p.name === k);
77
+ if (!embeddedProp && !allowedOperators.includes(k)) {
76
78
  throw core_1.ValidationError.invalidEmbeddableQuery(entityName, k, prop.type);
77
79
  }
78
- if (prop.embeddedProps[k]) {
79
- oo[prop.embeddedProps[k].name] = payload[key][k];
80
+ if (embeddedProp) {
81
+ oo[embeddedProp.name] = payload[key][k];
80
82
  }
81
83
  else if (typeof payload[key][k] === 'object') {
82
84
  oo[k] = JSON.stringify(payload[key][k]);
@@ -9,7 +9,8 @@ const enums_1 = require("./enums");
9
9
  */
10
10
  class ObjectCriteriaNode extends CriteriaNode_1.CriteriaNode {
11
11
  process(qb, options) {
12
- const nestedAlias = qb.getAliasForJoinPath(this.getPath(), options);
12
+ const matchPopulateJoins = options?.matchPopulateJoins || (this.prop && [core_1.ReferenceKind.MANY_TO_ONE, core_1.ReferenceKind.ONE_TO_ONE].includes(this.prop.kind));
13
+ const nestedAlias = qb.getAliasForJoinPath(this.getPath(), { ...options, matchPopulateJoins });
13
14
  const ownerAlias = options?.alias || qb.alias;
14
15
  const keys = Object.keys(this.payload);
15
16
  let alias = options?.alias;
@@ -209,7 +210,23 @@ class ObjectCriteriaNode extends CriteriaNode_1.CriteriaNode {
209
210
  }
210
211
  else {
211
212
  const prev = qb._fields?.slice();
212
- qb[method](field, nestedAlias, undefined, enums_1.JoinType.leftJoin, path);
213
+ const toOneProperty = [core_1.ReferenceKind.MANY_TO_ONE, core_1.ReferenceKind.ONE_TO_ONE].includes(this.prop.kind);
214
+ const joinType = toOneProperty && !this.prop.nullable
215
+ ? enums_1.JoinType.innerJoin
216
+ : enums_1.JoinType.leftJoin;
217
+ qb[method](field, nestedAlias, undefined, joinType, path);
218
+ // if the property is nullable, we need to use left join, so we mimic the inner join behaviour
219
+ // with an exclusive condition on the join columns:
220
+ // - if the owning column is null, the row is missing, we don't apply the filter
221
+ // - if the target column is not null, the row is matched, we apply the filter
222
+ if (toOneProperty && this.prop.nullable && options?.filter) {
223
+ qb.andWhere({
224
+ $or: [
225
+ { [field]: null },
226
+ { [nestedAlias + '.' + core_1.Utils.getPrimaryKeyHash(this.prop.referencedPKs)]: { $ne: null } },
227
+ ],
228
+ });
229
+ }
213
230
  if (!qb.hasFlag(core_1.QueryFlag.INFER_POPULATE)) {
214
231
  qb._fields = prev;
215
232
  }
@@ -299,6 +299,11 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
299
299
  processPopulateHint(): void;
300
300
  private processPopulateWhere;
301
301
  private mergeOnConditions;
302
+ /**
303
+ * When adding an inner join on a left joined relation, we need to nest them,
304
+ * otherwise the inner join could discard rows of the root table.
305
+ */
306
+ private processNestedJoins;
302
307
  private hasToManyJoins;
303
308
  protected wrapPaginateSubQuery(meta: EntityMetadata): void;
304
309
  private wrapModifySubQuery;
@@ -189,10 +189,10 @@ class QueryBuilder {
189
189
  subquery = field[1] instanceof QueryBuilder ? field[1].getFormattedQuery() : field[1].toString();
190
190
  field = field[0];
191
191
  }
192
- const prop = this.joinReference(field, alias, cond, type, path, schema, subquery);
192
+ const { prop, key } = this.joinReference(field, alias, cond, type, path, schema, subquery);
193
193
  const [fromAlias] = this.helper.splitField(field);
194
194
  if (subquery) {
195
- this._joins[`${fromAlias}.${prop.name}#${alias}`].subquery = subquery;
195
+ this._joins[key].subquery = subquery;
196
196
  }
197
197
  const populate = this._joinedProps.get(fromAlias);
198
198
  const item = { field: prop.name, strategy: core_1.LoadStrategy.JOINED, children: [] };
@@ -927,7 +927,8 @@ class QueryBuilder {
927
927
  prop.targetMeta = field.mainAlias.metadata;
928
928
  field = field.getKnexQuery();
929
929
  }
930
- this._joins[`${this.alias}.${prop.name}#${alias}`] = {
930
+ const key = `${this.alias}.${prop.name}#${alias}`;
931
+ this._joins[key] = {
931
932
  prop,
932
933
  alias,
933
934
  type,
@@ -936,7 +937,7 @@ class QueryBuilder {
936
937
  subquery: field.toString(),
937
938
  ownerAlias: this.alias,
938
939
  };
939
- return prop;
940
+ return { prop, key };
940
941
  }
941
942
  if (!subquery && type.includes('lateral')) {
942
943
  throw new Error(`Lateral join can be used only with a sub-query.`);
@@ -965,6 +966,7 @@ class QueryBuilder {
965
966
  path ??= `${(Object.values(this._joins).find(j => j.alias === fromAlias)?.path ?? entityName)}.${prop.name}`;
966
967
  if (prop.kind === core_1.ReferenceKind.ONE_TO_MANY) {
967
968
  this._joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
969
+ this._joins[aliasedName].path ??= path;
968
970
  }
969
971
  else if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY) {
970
972
  let pivotAlias = alias;
@@ -976,17 +978,18 @@ class QueryBuilder {
976
978
  const joins = this.helper.joinManyToManyReference(prop, fromAlias, alias, pivotAlias, type, cond, path, schema);
977
979
  Object.assign(this._joins, joins);
978
980
  this.createAlias(prop.pivotEntity, pivotAlias);
981
+ this._joins[aliasedName].path ??= path;
982
+ aliasedName = Object.keys(joins)[1];
979
983
  }
980
984
  else if (prop.kind === core_1.ReferenceKind.ONE_TO_ONE) {
981
985
  this._joins[aliasedName] = this.helper.joinOneToReference(prop, fromAlias, alias, type, cond, schema);
986
+ this._joins[aliasedName].path ??= path;
982
987
  }
983
988
  else { // MANY_TO_ONE
984
989
  this._joins[aliasedName] = this.helper.joinManyToOneReference(prop, fromAlias, alias, type, cond, schema);
990
+ this._joins[aliasedName].path ??= path;
985
991
  }
986
- if (!this._joins[aliasedName].path && path) {
987
- this._joins[aliasedName].path = path;
988
- }
989
- return prop;
992
+ return { prop, key: aliasedName };
990
993
  }
991
994
  prepareFields(fields, type = 'where') {
992
995
  const ret = [];
@@ -1161,6 +1164,7 @@ class QueryBuilder {
1161
1164
  const meta = this.mainAlias.metadata;
1162
1165
  this.applyDiscriminatorCondition();
1163
1166
  this.processPopulateHint();
1167
+ this.processNestedJoins();
1164
1168
  if (meta && (this._fields?.includes('*') || this._fields?.includes(`${this.mainAlias.aliasName}.*`))) {
1165
1169
  meta.props
1166
1170
  .filter(prop => prop.formula && (!prop.lazy || this.flags.has(core_1.QueryFlag.INCLUDE_LAZY_FORMULAS)))
@@ -1240,7 +1244,7 @@ class QueryBuilder {
1240
1244
  if (typeof this[key] === 'object') {
1241
1245
  const cond = CriteriaNodeFactory_1.CriteriaNodeFactory
1242
1246
  .createNode(this.metadata, this.mainAlias.entityName, this[key])
1243
- .process(this, { matchPopulateJoins: true, ignoreBranching: true, preferNoBranch: true });
1247
+ .process(this, { matchPopulateJoins: true, ignoreBranching: true, preferNoBranch: true, filter });
1244
1248
  // there might be new joins created by processing the `populateWhere` object
1245
1249
  joins = Object.values(this._joins);
1246
1250
  this.mergeOnConditions(joins, cond, filter);
@@ -1281,6 +1285,29 @@ class QueryBuilder {
1281
1285
  }
1282
1286
  }
1283
1287
  }
1288
+ /**
1289
+ * When adding an inner join on a left joined relation, we need to nest them,
1290
+ * otherwise the inner join could discard rows of the root table.
1291
+ */
1292
+ processNestedJoins() {
1293
+ if (this.flags.has(core_1.QueryFlag.DISABLE_NESTED_INNER_JOIN)) {
1294
+ return;
1295
+ }
1296
+ const joins = Object.values(this._joins);
1297
+ for (const join of joins) {
1298
+ if (join.type === enums_1.JoinType.innerJoin) {
1299
+ const parentJoin = joins.find(j => j.alias === join.ownerAlias);
1300
+ // https://stackoverflow.com/a/56815807/3665878
1301
+ if (parentJoin?.type === enums_1.JoinType.leftJoin || parentJoin?.type === enums_1.JoinType.nestedLeftJoin) {
1302
+ const nested = (parentJoin.nested ??= new Set());
1303
+ join.type = join.type === enums_1.JoinType.innerJoin
1304
+ ? enums_1.JoinType.nestedInnerJoin
1305
+ : enums_1.JoinType.nestedLeftJoin;
1306
+ nested.add(join);
1307
+ }
1308
+ }
1309
+ }
1310
+ }
1284
1311
  hasToManyJoins() {
1285
1312
  return Object.values(this._joins).some(join => {
1286
1313
  return [core_1.ReferenceKind.ONE_TO_MANY, core_1.ReferenceKind.MANY_TO_MANY].includes(join.prop.kind);
@@ -1,10 +1,10 @@
1
1
  import { CriteriaNode } from './CriteriaNode';
2
- import type { IQueryBuilder, ICriteriaNodeProcessOptions } from '../typings';
2
+ import type { ICriteriaNodeProcessOptions, IQueryBuilder } from '../typings';
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
  }
@@ -9,7 +9,9 @@ const enums_1 = require("./enums");
9
9
  */
10
10
  class ScalarCriteriaNode extends CriteriaNode_1.CriteriaNode {
11
11
  process(qb, options) {
12
- if (this.shouldJoin()) {
12
+ const matchPopulateJoins = options?.matchPopulateJoins || (this.prop && [core_1.ReferenceKind.MANY_TO_ONE, core_1.ReferenceKind.ONE_TO_ONE].includes(this.prop.kind));
13
+ const nestedAlias = qb.getAliasForJoinPath(this.getPath(), { ...options, matchPopulateJoins });
14
+ if (this.shouldJoin(qb, nestedAlias)) {
13
15
  const path = this.getPath();
14
16
  const parentPath = this.parent.getPath(); // the parent is always there, otherwise `shouldJoin` would return `false`
15
17
  const nestedAlias = qb.getAliasForJoinPath(path) || qb.getNextAlias(this.prop?.pivotTable ?? this.entityName);
@@ -30,10 +32,10 @@ class ScalarCriteriaNode extends CriteriaNode_1.CriteriaNode {
30
32
  return this.payload;
31
33
  }
32
34
  willAutoJoin(qb, alias, options) {
33
- return this.shouldJoin();
35
+ return this.shouldJoin(qb, alias);
34
36
  }
35
- shouldJoin() {
36
- if (!this.parent || !this.prop) {
37
+ shouldJoin(qb, nestedAlias) {
38
+ if (!this.parent || !this.prop || (nestedAlias && [enums_1.QueryType.SELECT, enums_1.QueryType.COUNT].includes(qb.type ?? enums_1.QueryType.SELECT))) {
37
39
  return false;
38
40
  }
39
41
  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';
3
3
  import type { CheckDef, Column, ForeignKey, IndexDef } from '../typings';
4
4
  import type { AbstractSqlPlatform } from '../AbstractSqlPlatform';
@@ -53,12 +53,13 @@ export declare class DatabaseTable {
53
53
  private getPropertyTypeForForeignKey;
54
54
  private getPropertyTypeForColumn;
55
55
  private getPropertyDefaultValue;
56
+ private processIndexExpression;
56
57
  addIndex(meta: EntityMetadata, index: {
57
- properties: string | string[];
58
+ properties?: string | string[];
58
59
  name?: string;
59
60
  type?: string;
60
- expression?: string;
61
- deferMode?: DeferMode;
61
+ expression?: string | IndexCallback<any>;
62
+ deferMode?: DeferMode | `${DeferMode}`;
62
63
  options?: Dictionary;
63
64
  }, type: 'index' | 'unique' | 'primary'): void;
64
65
  addCheck(check: CheckDef): void;
@@ -700,6 +700,18 @@ class DatabaseTable {
700
700
  }
701
701
  return '' + val;
702
702
  }
703
+ processIndexExpression(expression, meta) {
704
+ if (expression instanceof Function) {
705
+ const exp = expression({ name: this.name, schema: this.schema, toString() {
706
+ if (this.schema) {
707
+ return `${this.schema}.${this.name}`;
708
+ }
709
+ return this.name;
710
+ } }, meta.createColumnMappingObject());
711
+ return exp instanceof core_1.RawQueryFragment ? this.platform.formatQuery(exp.sql, exp.params) : exp;
712
+ }
713
+ return expression;
714
+ }
703
715
  addIndex(meta, index, type) {
704
716
  const properties = core_1.Utils.unique(core_1.Utils.flatten(core_1.Utils.asArray(index.properties).map(prop => {
705
717
  const parts = prop.split('.');
@@ -741,7 +753,7 @@ class DatabaseTable {
741
753
  primary: type === 'primary',
742
754
  unique: type !== 'index',
743
755
  type: index.type,
744
- expression: index.expression,
756
+ expression: this.processIndexExpression(index.expression, meta),
745
757
  options: index.options,
746
758
  deferMode: index.deferMode,
747
759
  });
package/typings.d.ts CHANGED
@@ -75,7 +75,7 @@ export interface IndexDef {
75
75
  storageEngineIndexType?: 'hash' | 'btree';
76
76
  predicate?: Knex.QueryBuilder;
77
77
  }>;
78
- deferMode?: DeferMode;
78
+ deferMode?: DeferMode | `${DeferMode}`;
79
79
  }
80
80
  export interface CheckDef<T = unknown> {
81
81
  name: string;
@@ -172,6 +172,7 @@ export interface ICriteriaNodeProcessOptions {
172
172
  ignoreBranching?: boolean;
173
173
  preferNoBranch?: boolean;
174
174
  type?: 'orderBy';
175
+ filter?: boolean;
175
176
  }
176
177
  export interface ICriteriaNode<T extends object> {
177
178
  readonly entityName: string;