@mikro-orm/knex 7.0.0-dev.3 → 7.0.0-dev.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/AbstractSqlConnection.d.ts +6 -4
  2. package/AbstractSqlConnection.js +33 -19
  3. package/AbstractSqlDriver.d.ts +7 -7
  4. package/AbstractSqlDriver.js +169 -163
  5. package/AbstractSqlPlatform.js +3 -3
  6. package/PivotCollectionPersister.d.ts +3 -2
  7. package/PivotCollectionPersister.js +6 -2
  8. package/README.md +1 -2
  9. package/dialects/mssql/MsSqlNativeQueryBuilder.d.ts +2 -0
  10. package/dialects/mssql/MsSqlNativeQueryBuilder.js +48 -5
  11. package/dialects/mysql/MySqlPlatform.js +2 -1
  12. package/dialects/sqlite/BaseSqliteConnection.d.ts +1 -1
  13. package/dialects/sqlite/BaseSqliteConnection.js +8 -2
  14. package/dialects/sqlite/BaseSqlitePlatform.d.ts +0 -1
  15. package/dialects/sqlite/BaseSqlitePlatform.js +0 -4
  16. package/dialects/sqlite/SqliteSchemaHelper.js +1 -1
  17. package/package.json +4 -4
  18. package/query/CriteriaNode.d.ts +1 -1
  19. package/query/CriteriaNode.js +5 -9
  20. package/query/CriteriaNodeFactory.js +10 -5
  21. package/query/NativeQueryBuilder.js +1 -1
  22. package/query/ObjectCriteriaNode.js +30 -7
  23. package/query/QueryBuilder.d.ts +15 -1
  24. package/query/QueryBuilder.js +92 -17
  25. package/query/QueryBuilderHelper.js +4 -10
  26. package/query/ScalarCriteriaNode.d.ts +3 -3
  27. package/query/ScalarCriteriaNode.js +7 -5
  28. package/schema/DatabaseSchema.js +18 -2
  29. package/schema/DatabaseTable.d.ts +5 -4
  30. package/schema/DatabaseTable.js +39 -6
  31. package/schema/SchemaComparator.js +1 -1
  32. package/schema/SchemaHelper.d.ts +2 -0
  33. package/schema/SchemaHelper.js +9 -5
  34. package/schema/SqlSchemaGenerator.d.ts +6 -1
  35. package/schema/SqlSchemaGenerator.js +25 -5
  36. package/typings.d.ts +7 -2
@@ -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';
@@ -63,6 +63,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
63
63
  if (options.lockMode) {
64
64
  qb.setLockMode(options.lockMode, options.lockTableAliases);
65
65
  }
66
+ if (options.em) {
67
+ await qb.applyJoinedFilters(options.em, options.filters);
68
+ }
66
69
  const result = await this.rethrow(qb.execute('all'));
67
70
  if (isCursorPagination && !first && !!last) {
68
71
  result.reverse();
@@ -235,7 +238,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
235
238
  let relationPojo = {};
236
239
  meta2.props
237
240
  .filter(prop => !ref && prop.persist === false && prop.fieldNames)
238
- .filter(prop => !prop.lazy || populate.some(p => p.field === prop.name || p.all))
239
241
  .forEach(prop => {
240
242
  /* v8 ignore next 3 */
241
243
  if (prop.fieldNames.length > 1) { // composite keys
@@ -252,6 +254,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
252
254
  : meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
253
255
  const tz = this.platform.getTimezone();
254
256
  for (const prop of targetProps) {
257
+ if (prop.fieldNames.every(name => typeof root[`${relationAlias}__${name}`] === 'undefined')) {
258
+ continue;
259
+ }
255
260
  if (prop.fieldNames.length > 1) { // composite keys
256
261
  const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
257
262
  const pk = Utils.mapFlatCompositePrimaryKey(fk, prop);
@@ -306,15 +311,20 @@ export class AbstractSqlDriver extends DatabaseDriver {
306
311
  });
307
312
  }
308
313
  async count(entityName, where, options = {}) {
309
- const meta = this.metadata.find(entityName);
310
- if (meta?.virtual) {
314
+ const meta = this.metadata.get(entityName);
315
+ if (meta.virtual) {
311
316
  return this.countVirtual(entityName, where, options);
312
317
  }
313
- const joinedProps = meta ? this.joinedProps(meta, options.populate ?? []) : [];
314
- const populateWhere = meta ? this.buildPopulateWhere(meta, joinedProps, options) : undefined;
315
- const populate = options.populate ?? [];
316
- const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false, options.logging)
317
- .indexHint(options.indexHint)
318
+ options = { populate: [], ...options };
319
+ const populate = options.populate;
320
+ const joinedProps = this.joinedProps(meta, populate, options);
321
+ const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false, options.logging);
322
+ const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
323
+ if (meta && !Utils.isEmpty(populate)) {
324
+ this.buildFields(meta, populate, joinedProps, qb, qb.alias, options);
325
+ }
326
+ qb.__populateWhere = options._populateWhere;
327
+ qb.indexHint(options.indexHint)
318
328
  .comment(options.comments)
319
329
  .hintComment(options.hintComments)
320
330
  .groupBy(options.groupBy)
@@ -322,8 +332,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
322
332
  .populate(populate, joinedProps.length > 0 ? populateWhere : undefined, joinedProps.length > 0 ? options.populateFilter : undefined)
323
333
  .withSchema(this.getSchemaName(meta, options))
324
334
  .where(where);
325
- if (meta && !Utils.isEmpty(populate)) {
326
- this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, true);
335
+ if (options.em) {
336
+ await qb.applyJoinedFilters(options.em, options.filters);
327
337
  }
328
338
  return this.rethrow(qb.getCount());
329
339
  }
@@ -332,7 +342,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
332
342
  const meta = this.metadata.find(entityName);
333
343
  const collections = this.extractManyToMany(entityName, data);
334
344
  const pks = meta?.primaryKeys ?? [this.config.getNamingStrategy().referenceColumnName()];
335
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes).withSchema(this.getSchemaName(meta, options));
345
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
336
346
  const res = await this.rethrow(qb.insert(data).execute('run', false));
337
347
  res.row = res.row || {};
338
348
  let pk;
@@ -380,7 +390,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
380
390
  sql += ' ' + data.map(() => `select null as ${this.platform.quoteIdentifier(pks[0])}`).join(' union all ');
381
391
  }
382
392
  const addParams = (prop, row) => {
383
- let value = row[prop.name] ?? prop.default;
393
+ const rowValue = row[prop.name];
394
+ if (prop.nullable && rowValue === null) {
395
+ params.push(null);
396
+ return;
397
+ }
398
+ let value = rowValue ?? prop.default;
384
399
  if (prop.kind === ReferenceKind.EMBEDDED && prop.object) {
385
400
  if (prop.array && value) {
386
401
  value = this.platform.cloneEmbeddable(value);
@@ -393,14 +408,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
393
408
  value = this.mapDataToFieldNames(value, false, prop.embeddedProps, options.convertCustomTypes);
394
409
  }
395
410
  }
396
- if (options.convertCustomTypes && prop.customType) {
397
- params.push(prop.customType.convertToDatabaseValue(value, this.platform, { key: prop.name, mode: 'query-data' }));
398
- return;
399
- }
400
411
  if (typeof value === 'undefined' && this.platform.usesDefaultKeyword()) {
401
412
  params.push(raw('default'));
402
413
  return;
403
414
  }
415
+ if (options.convertCustomTypes && prop.customType) {
416
+ params.push(prop.customType.convertToDatabaseValue(value, this.platform, { key: prop.name, mode: 'query-data' }));
417
+ return;
418
+ }
404
419
  params.push(value);
405
420
  };
406
421
  if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
@@ -434,7 +449,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
434
449
  else {
435
450
  const field = prop.fieldNames[0];
436
451
  if (!duplicates.includes(field) || !usedDups.includes(field)) {
437
- if (prop.customType && !prop.object && 'convertToDatabaseValueSQL' in prop.customType && !isRaw(row[prop.name])) {
452
+ if (prop.customType && !prop.object && 'convertToDatabaseValueSQL' in prop.customType && row[prop.name] != null && !isRaw(row[prop.name])) {
438
453
  keys.push(prop.customType.convertToDatabaseValueSQL('?', this.platform));
439
454
  }
440
455
  else {
@@ -459,7 +474,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
459
474
  if (transform) {
460
475
  sql = transform(sql);
461
476
  }
462
- const res = await this.execute(sql, params, 'run', options.ctx);
477
+ const res = await this.execute(sql, params, 'run', options.ctx, options.loggerContext);
463
478
  let pk;
464
479
  /* v8 ignore next 3 */
465
480
  if (pks.length > 1) { // owner has composite pk
@@ -487,7 +502,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
487
502
  where = { [meta?.primaryKeys[0] ?? pks[0]]: where };
488
503
  }
489
504
  if (Utils.hasObjectKeys(data)) {
490
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes)
505
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext)
491
506
  .withSchema(this.getSchemaName(meta, options));
492
507
  if (options.upsert) {
493
508
  /* v8 ignore next */
@@ -526,7 +541,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
526
541
  const meta = this.metadata.get(entityName);
527
542
  if (options.upsert) {
528
543
  const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(where[0]) ? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
529
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes).withSchema(this.getSchemaName(meta, options));
544
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
530
545
  const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
531
546
  qb.insert(data)
532
547
  .onConflict(uniqueFields)
@@ -585,7 +600,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
585
600
  if (key in data[idx]) {
586
601
  const pks = Utils.getOrderedPrimaryKeys(cond, meta);
587
602
  sql += ` when (${pkCond}) then `;
588
- if (prop.customType && !prop.object && 'convertToDatabaseValueSQL' in prop.customType && !isRaw(data[idx][key])) {
603
+ if (prop.customType && !prop.object && 'convertToDatabaseValueSQL' in prop.customType && data[idx][prop.name] != null && !isRaw(data[idx][key])) {
589
604
  sql += prop.customType.convertToDatabaseValueSQL('?', this.platform);
590
605
  }
591
606
  else {
@@ -639,7 +654,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
639
654
  /* v8 ignore next */
640
655
  sql += returningFields.length > 0 ? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}` : '';
641
656
  }
642
- const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx));
657
+ const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, options.loggerContext));
643
658
  for (let i = 0; i < collections.length; i++) {
644
659
  await this.processManyToMany(meta, where[i], collections[i], false, options);
645
660
  }
@@ -651,7 +666,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
651
666
  if (Utils.isPrimaryKey(where) && pks.length === 1) {
652
667
  where = { [pks[0]]: where };
653
668
  }
654
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false).delete(where).withSchema(this.getSchemaName(meta, options));
669
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext).delete(where).withSchema(this.getSchemaName(meta, options));
655
670
  return this.rethrow(qb.execute('run', false));
656
671
  }
657
672
  /**
@@ -725,14 +740,20 @@ export class AbstractSqlDriver extends DatabaseDriver {
725
740
  const pivotMeta = this.metadata.find(coll.property.pivotEntity);
726
741
  let schema = pivotMeta.schema;
727
742
  if (schema === '*') {
728
- const ownerSchema = wrapped.getSchema() === '*' ? this.config.get('schema') : wrapped.getSchema();
729
- schema = coll.property.owner ? ownerSchema : this.config.get('schema');
743
+ if (coll.property.owner) {
744
+ schema = wrapped.getSchema() === '*' ? options?.schema ?? this.config.get('schema') : wrapped.getSchema();
745
+ }
746
+ else {
747
+ const targetMeta = coll.property.targetMeta;
748
+ const targetSchema = (coll[0] ?? snap?.[0]) && helper(coll[0] ?? snap?.[0]).getSchema();
749
+ schema = targetMeta.schema === '*' ? options?.schema ?? targetSchema ?? this.config.get('schema') : targetMeta.schema;
750
+ }
730
751
  }
731
752
  else if (schema == null) {
732
753
  schema = this.config.get('schema');
733
754
  }
734
755
  const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
735
- const persister = groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema);
756
+ const persister = groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext);
736
757
  persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks);
737
758
  }
738
759
  for (const persister of Utils.values(groups)) {
@@ -740,110 +761,64 @@ export class AbstractSqlDriver extends DatabaseDriver {
740
761
  }
741
762
  }
742
763
  async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
764
+ if (owners.length === 0) {
765
+ return {};
766
+ }
743
767
  const pivotMeta = this.metadata.find(prop.pivotEntity);
744
768
  const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
745
769
  const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
746
770
  const ownerMeta = this.metadata.find(pivotProp2.type);
747
- options = { ...options };
748
- const qb = this.createQueryBuilder(prop.pivotEntity, ctx, options.connectionType, undefined, options?.logging)
749
- .withSchema(this.getSchemaName(pivotMeta, options))
750
- .indexHint(options.indexHint)
751
- .comment(options.comments)
752
- .hintComment(options.hintComments);
753
- const pivotAlias = qb.alias;
754
- const pivotKey = pivotProp2.joinColumns.map(column => `${pivotAlias}.${column}`).join(Utils.PK_SEPARATOR);
755
771
  const cond = {
756
- [pivotKey]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) },
772
+ [pivotProp2.name]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) },
757
773
  };
758
- /* v8 ignore next 3 */
759
- if (!Utils.isEmpty(where) && Object.keys(where).every(k => Utils.isOperator(k, false))) {
760
- where = cond;
761
- }
762
- else {
763
- where = { ...where, ...cond };
764
- }
765
- orderBy = this.getPivotOrderBy(prop, pivotProp1, pivotAlias, orderBy);
766
- const populate = this.autoJoinOneToOneOwner(prop.targetMeta, []);
767
- const fields = [];
768
- const k1 = !prop.owner ? 'joinColumns' : 'inverseJoinColumns';
769
- const k2 = prop.owner ? 'joinColumns' : 'inverseJoinColumns';
770
- const cols = [
771
- ...prop[k1].map(col => `${pivotAlias}.${col} as fk__${col}`),
772
- ...prop[k2].map(col => `${pivotAlias}.${col} as fk__${col}`),
773
- ];
774
- fields.push(...cols);
775
- if (!pivotJoin) {
776
- const targetAlias = qb.getNextAlias(prop.targetMeta.tableName);
777
- const targetSchema = this.getSchemaName(prop.targetMeta, options) ?? this.platform.getDefaultSchemaName();
778
- qb.innerJoin(pivotProp1.name, targetAlias, {}, targetSchema);
779
- const targetFields = this.buildFields(prop.targetMeta, (options.populate ?? []), [], qb, targetAlias, options);
780
- for (const field of targetFields) {
781
- const f = field.toString();
782
- fields.unshift(f.includes('.') ? field : `${targetAlias}.${f}`);
783
- if (RawQueryFragment.isKnownFragment(field)) {
784
- qb.rawFragments.add(f);
785
- }
786
- }
787
- // we need to handle 1:1 owner auto-joins explicitly, as the QB type is the pivot table, not the target
788
- populate.forEach(hint => {
789
- const alias = qb.getNextAlias(prop.targetMeta.tableName);
790
- qb.leftJoin(`${targetAlias}.${hint.field}`, alias);
791
- // eslint-disable-next-line dot-notation
792
- for (const join of Object.values(qb['_joins'])) {
793
- const [propName] = hint.field.split(':', 2);
794
- if (join.alias === alias && join.prop.name === propName) {
795
- fields.push(...qb.helper.mapJoinColumns(qb.type, join));
796
- }
797
- }
798
- });
799
- }
800
- qb.select(fields)
801
- .where({ [pivotProp1.name]: where })
802
- .orderBy(orderBy)
803
- .setLockMode(options.lockMode, options.lockTableAliases);
804
- if (owners.length === 1 && (options.offset != null || options.limit != null)) {
805
- qb.limit(options.limit, options.offset);
806
- }
807
- const res = owners.length ? await this.rethrow(qb.execute('all', { mergeResults: false, mapResults: false })) : [];
808
- const tmp = {};
809
- const items = res.map((row) => {
810
- const root = super.mapResult(row, prop.targetMeta);
811
- this.mapJoinedProps(root, prop.targetMeta, populate, qb, root, tmp, pivotMeta.className + '.' + pivotProp1.name);
812
- return root;
774
+ if (!Utils.isEmpty(where)) {
775
+ cond[pivotProp1.name] = { ...where };
776
+ }
777
+ where = cond;
778
+ const populateField = pivotJoin ? `${pivotProp1.name}:ref` : pivotProp1.name;
779
+ const populate = this.autoJoinOneToOneOwner(prop.targetMeta, options?.populate ?? [], options?.fields);
780
+ const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${pivotProp1.name}.${f}`) : [];
781
+ const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${pivotProp1.name}.${f}`) : [];
782
+ const fields = pivotJoin
783
+ ? [pivotProp1.name, pivotProp2.name]
784
+ : [pivotProp1.name, pivotProp2.name, ...childFields];
785
+ const res = await this.find(pivotMeta.className, where, {
786
+ ctx,
787
+ ...options,
788
+ fields,
789
+ exclude: childExclude,
790
+ orderBy: this.getPivotOrderBy(prop, pivotProp1, orderBy, options?.orderBy),
791
+ populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate }],
792
+ populateWhere: undefined,
793
+ // @ts-ignore
794
+ _populateWhere: 'infer',
795
+ populateFilter: !Utils.isEmpty(options?.populateFilter) ? { [pivotProp2.name]: options?.populateFilter } : undefined,
813
796
  });
814
- qb.clearRawFragmentsCache();
815
797
  const map = {};
816
- const pkProps = ownerMeta.getPrimaryProps();
817
798
  for (const owner of owners) {
818
- const key = Utils.getPrimaryKeyHash(prop.joinColumns.map((_col, idx) => {
819
- const pkProp = pkProps[idx];
820
- return pkProp.customType ? pkProp.customType.convertToJSValue(owner[idx], this.platform) : owner[idx];
821
- }));
799
+ const key = Utils.getPrimaryKeyHash(owner);
822
800
  map[key] = [];
823
801
  }
824
- for (const item of items) {
825
- const key = Utils.getPrimaryKeyHash(prop.joinColumns.map((col, idx) => {
826
- const pkProp = pkProps[idx];
827
- return pkProp.customType ? pkProp.customType.convertToJSValue(item[`fk__${col}`], this.platform) : item[`fk__${col}`];
828
- }));
829
- map[key].push(item);
830
- prop.joinColumns.forEach(col => delete item[`fk__${col}`]);
831
- prop.inverseJoinColumns.forEach((col, idx) => {
832
- Utils.renameKey(item, `fk__${col}`, prop.targetMeta.primaryKeys[idx]);
833
- });
802
+ for (const item of res) {
803
+ const key = Utils.getPrimaryKeyHash(Utils.asArray(item[pivotProp2.name]));
804
+ map[key].push(item[pivotProp1.name]);
834
805
  }
835
806
  return map;
836
807
  }
837
- getPivotOrderBy(prop, pivotProp, pivotAlias, orderBy) {
838
- // FIXME this is ignoring the rest of the array items
808
+ getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
839
809
  if (!Utils.isEmpty(orderBy)) {
840
- return [{ [pivotProp.name]: Utils.asArray(orderBy)[0] }];
810
+ return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
811
+ }
812
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && Utils.asArray(parentOrderBy).some(o => o[prop.name])) {
813
+ return Utils.asArray(parentOrderBy)
814
+ .filter(o => o[prop.name])
815
+ .map(o => ({ [pivotProp.name]: o[prop.name] }));
841
816
  }
842
817
  if (!Utils.isEmpty(prop.orderBy)) {
843
- return [{ [pivotProp.name]: Utils.asArray(prop.orderBy)[0] }];
818
+ return Utils.asArray(prop.orderBy).map(o => ({ [pivotProp.name]: o }));
844
819
  }
845
820
  if (prop.fixedOrder) {
846
- return [{ [`${pivotAlias}.${prop.fixedOrderColumn}`]: QueryOrder.ASC }];
821
+ return [{ [prop.fixedOrderColumn]: QueryOrder.ASC }];
847
822
  }
848
823
  return [];
849
824
  }
@@ -861,7 +836,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
861
836
  const toPopulate = meta.relations
862
837
  .filter(prop => prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && !prop.lazy && !relationsToPopulate.includes(prop.name))
863
838
  .filter(prop => fields.length === 0 || fields.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)))
864
- .map(prop => ({ field: `${prop.name}:ref`, strategy: prop.strategy }));
839
+ .map(prop => ({ field: `${prop.name}:ref`, strategy: LoadStrategy.JOINED }));
865
840
  return [...populate, ...toPopulate];
866
841
  }
867
842
  /**
@@ -869,16 +844,17 @@ export class AbstractSqlDriver extends DatabaseDriver {
869
844
  */
870
845
  joinedProps(meta, populate, options) {
871
846
  return populate.filter(hint => {
872
- const [propName, ref] = hint.field.split(':', 2);
847
+ const [propName] = hint.field.split(':', 2);
873
848
  const prop = meta.properties[propName] || {};
874
- if (hint.filter && hint.strategy === LoadStrategy.JOINED) {
849
+ const strategy = getLoadingStrategy(hint.strategy || prop.strategy || options?.strategy || this.config.get('loadStrategy'), prop.kind);
850
+ if (hint.filter && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) && !prop.nullable) {
875
851
  return true;
876
852
  }
877
853
  // skip redundant joins for 1:1 owner population hints when using `mapToPk`
878
854
  if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
879
855
  return false;
880
856
  }
881
- if ((options?.strategy || hint.strategy || prop.strategy || this.config.get('loadStrategy')) !== LoadStrategy.JOINED) {
857
+ if (strategy !== LoadStrategy.JOINED) {
882
858
  // force joined strategy for explicit 1:1 owner populate hint as it would require a join anyway
883
859
  return prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner;
884
860
  }
@@ -889,31 +865,26 @@ export class AbstractSqlDriver extends DatabaseDriver {
889
865
  * @internal
890
866
  */
891
867
  mergeJoinedResult(rawResults, meta, joinedProps) {
868
+ if (rawResults.length <= 1) {
869
+ return rawResults;
870
+ }
892
871
  const res = [];
893
872
  const map = {};
873
+ const collectionsToMerge = {};
874
+ const hints = joinedProps.map(hint => {
875
+ const [propName, ref] = hint.field.split(':', 2);
876
+ return { propName, ref, children: hint.children };
877
+ });
894
878
  for (const item of rawResults) {
895
879
  const pk = Utils.getCompositeKeyHash(item, meta);
896
880
  if (map[pk]) {
897
- for (const hint of joinedProps) {
898
- const [propName, ref] = hint.field.split(':', 2);
899
- const prop = meta.properties[propName];
881
+ for (const { propName } of hints) {
900
882
  if (!item[propName]) {
901
883
  continue;
902
884
  }
903
- if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind) && ref) {
904
- map[pk][propName] = [...map[pk][propName], ...item[propName]];
905
- continue;
906
- }
907
- switch (prop.kind) {
908
- case ReferenceKind.ONE_TO_MANY:
909
- case ReferenceKind.MANY_TO_MANY:
910
- map[pk][propName] = this.mergeJoinedResult([...map[pk][propName], ...item[propName]], prop.targetMeta, hint.children ?? []);
911
- break;
912
- case ReferenceKind.MANY_TO_ONE:
913
- case ReferenceKind.ONE_TO_ONE:
914
- map[pk][propName] = this.mergeJoinedResult([map[pk][propName], item[propName]], prop.targetMeta, hint.children ?? [])[0];
915
- break;
916
- }
885
+ collectionsToMerge[pk] ??= {};
886
+ collectionsToMerge[pk][propName] ??= [map[pk][propName]];
887
+ collectionsToMerge[pk][propName].push(item[propName]);
917
888
  }
918
889
  }
919
890
  else {
@@ -921,17 +892,42 @@ export class AbstractSqlDriver extends DatabaseDriver {
921
892
  res.push(item);
922
893
  }
923
894
  }
895
+ for (const pk in collectionsToMerge) {
896
+ const entity = map[pk];
897
+ const collections = collectionsToMerge[pk];
898
+ for (const { propName, ref, children } of hints) {
899
+ if (!collections[propName]) {
900
+ continue;
901
+ }
902
+ const prop = meta.properties[propName];
903
+ const items = collections[propName].flat();
904
+ if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind) && ref) {
905
+ entity[propName] = items;
906
+ continue;
907
+ }
908
+ switch (prop.kind) {
909
+ case ReferenceKind.ONE_TO_MANY:
910
+ case ReferenceKind.MANY_TO_MANY:
911
+ entity[propName] = this.mergeJoinedResult(items, prop.targetMeta, children ?? []);
912
+ break;
913
+ case ReferenceKind.MANY_TO_ONE:
914
+ case ReferenceKind.ONE_TO_ONE:
915
+ entity[propName] = this.mergeJoinedResult(items, prop.targetMeta, children ?? [])[0];
916
+ break;
917
+ }
918
+ }
919
+ }
924
920
  return res;
925
921
  }
926
922
  getFieldsForJoinedLoad(qb, meta, options) {
927
923
  const fields = [];
928
924
  const populate = options.populate ?? [];
929
925
  const joinedProps = this.joinedProps(meta, populate, options);
930
- const shouldHaveColumn = (prop, populate, fields) => {
926
+ const shouldHaveColumn = (meta, prop, populate, fields) => {
931
927
  if (!this.platform.shouldHaveColumn(prop, populate, options.exclude)) {
932
928
  return false;
933
929
  }
934
- if (!fields || fields.includes('*') || prop.primary) {
930
+ if (!fields || fields.includes('*') || prop.primary || meta.root.discriminatorColumn === prop.name) {
935
931
  return true;
936
932
  }
937
933
  return fields.some(f => f === prop.name || f.toString().startsWith(prop.name + '.'));
@@ -941,17 +937,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
941
937
  if (options.parentJoinPath) {
942
938
  // alias all fields in the primary table
943
939
  meta.props
944
- .filter(prop => shouldHaveColumn(prop, populate, options.explicitFields))
940
+ .filter(prop => shouldHaveColumn(meta, prop, populate, options.explicitFields))
945
941
  .forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, options.parentTableAlias)));
946
942
  }
947
943
  for (const hint of joinedProps) {
948
944
  const [propName, ref] = hint.field.split(':', 2);
949
945
  const prop = meta.properties[propName];
950
946
  // ignore ref joins of known FKs unless it's a filter hint
951
- if (ref && !hint.filter && (prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner))) {
952
- continue;
953
- }
954
- if (options.count && !options?.populateFilter?.[prop.name]) {
947
+ if (ref && !hint.filter && (prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))) {
955
948
  continue;
956
949
  }
957
950
  const meta2 = this.metadata.find(prop.type);
@@ -962,11 +955,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
962
955
  if (!options.parentJoinPath && populateWhereAll && !hint.filter && !path.startsWith('[populate]')) {
963
956
  path = '[populate]' + path;
964
957
  }
958
+ const mandatoryToOneProperty = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.nullable;
965
959
  const joinType = pivotRefJoin
966
960
  ? JoinType.pivotJoin
967
- : hint.filter && !prop.nullable
968
- ? JoinType.innerJoin
969
- : JoinType.leftJoin;
961
+ : hint.joinType
962
+ ? hint.joinType
963
+ : (hint.filter && !prop.nullable) || mandatoryToOneProperty
964
+ ? JoinType.innerJoin
965
+ : JoinType.leftJoin;
970
966
  qb.join(field, tableAlias, {}, joinType, path);
971
967
  if (pivotRefJoin) {
972
968
  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}`)));
@@ -998,7 +994,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
998
994
  parentJoinPath: path,
999
995
  }));
1000
996
  }
1001
- else if (hint.filter || prop.mapToPk) {
997
+ else if (hint.filter || prop.mapToPk || (ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind))) {
1002
998
  fields.push(...prop.referencedColumnNames.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)));
1003
999
  }
1004
1000
  }
@@ -1008,6 +1004,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
1008
1004
  * @internal
1009
1005
  */
1010
1006
  mapPropToFieldNames(qb, prop, tableAlias) {
1007
+ if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
1008
+ return Object.values(prop.embeddedProps).flatMap(childProp => {
1009
+ return this.mapPropToFieldNames(qb, childProp, tableAlias);
1010
+ });
1011
+ }
1011
1012
  const aliased = this.platform.quoteIdentifier(`${tableAlias}__${prop.fieldNames[0]}`);
1012
1013
  if (prop.customTypes?.some(type => type?.convertToJSValueSQL)) {
1013
1014
  return prop.fieldNames.map((col, idx) => {
@@ -1073,7 +1074,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1073
1074
  for (const prop of meta.relations) {
1074
1075
  if (collections[prop.name]) {
1075
1076
  const pivotMeta = this.metadata.find(prop.pivotEntity);
1076
- const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema);
1077
+ const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext);
1077
1078
  persister.enqueueUpdate(prop, collections[prop.name], clear, pks);
1078
1079
  await this.rethrow(persister.execute());
1079
1080
  }
@@ -1140,7 +1141,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1140
1141
  let path = parentPath;
1141
1142
  const meta2 = this.metadata.find(prop.type);
1142
1143
  const childOrder = orderHint[prop.name];
1143
- if (![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || !prop.owner || Utils.isPlainObject(childOrder)) {
1144
+ if (prop.kind !== ReferenceKind.SCALAR && (![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || !prop.owner || Utils.isPlainObject(childOrder))) {
1144
1145
  path += `.${propName}`;
1145
1146
  }
1146
1147
  if (prop.kind === ReferenceKind.MANY_TO_MANY && typeof childOrder !== 'object') {
@@ -1148,10 +1149,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
1148
1149
  }
1149
1150
  const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
1150
1151
  const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true }) ?? parentAlias;
1151
- if (!join && parentAlias === qb.alias) {
1152
+ if (!join) {
1152
1153
  continue;
1153
1154
  }
1154
- if (![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED].includes(prop.kind) && typeof childOrder === 'object') {
1155
+ if (join && ![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED].includes(prop.kind) && typeof childOrder === 'object') {
1155
1156
  const children = this.buildPopulateOrderBy(qb, meta2, Utils.asArray(childOrder), path, explicit, propAlias);
1156
1157
  orderBy.push(...children);
1157
1158
  continue;
@@ -1229,7 +1230,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1229
1230
  }
1230
1231
  return ret;
1231
1232
  }
1232
- processField(meta, prop, field, ret, populate, joinedProps, qb) {
1233
+ processField(meta, prop, field, ret) {
1233
1234
  if (!prop || (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner)) {
1234
1235
  return;
1235
1236
  }
@@ -1242,7 +1243,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1242
1243
  const top = parts.shift();
1243
1244
  for (const key of Object.keys(prop.embeddedProps)) {
1244
1245
  if (!top || key === top) {
1245
- this.processField(meta, prop.embeddedProps[key], parts.join('.'), ret, populate, joinedProps, qb);
1246
+ this.processField(meta, prop.embeddedProps[key], parts.join('.'), ret);
1246
1247
  }
1247
1248
  }
1248
1249
  return;
@@ -1261,7 +1262,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1261
1262
  }
1262
1263
  return false;
1263
1264
  }
1264
- buildFields(meta, populate, joinedProps, qb, alias, options, count = false) {
1265
+ buildFields(meta, populate, joinedProps, qb, alias, options) {
1265
1266
  const lazyProps = meta.props.filter(prop => prop.lazy && !populate.some(p => this.isPopulated(meta, prop, p)));
1266
1267
  const hasLazyFormulas = meta.props.some(p => p.lazy && p.formula);
1267
1268
  const requiresSQLConversion = meta.props.some(p => p.customType?.convertToJSValueSQL && p.persist !== false);
@@ -1284,11 +1285,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
1284
1285
  where: {},
1285
1286
  aliasMap: qb.getAliasMap(),
1286
1287
  });
1287
- this.processField(meta, prop, parts.join('.'), ret, populate, joinedProps, qb);
1288
+ this.processField(meta, prop, parts.join('.'), ret);
1288
1289
  }
1289
1290
  if (!options.fields.includes('*') && !options.fields.includes(`${qb.alias}.*`)) {
1290
1291
  ret.unshift(...meta.primaryKeys.filter(pk => !options.fields.includes(pk)));
1291
1292
  }
1293
+ if (meta.root.discriminatorColumn && !options.fields.includes(`${qb.alias}.${meta.root.discriminatorColumn}`)) {
1294
+ ret.push(meta.root.discriminatorColumn);
1295
+ }
1292
1296
  }
1293
1297
  else if (!Utils.isEmpty(options.exclude) || lazyProps.some(p => !p.formula && (p.kind !== '1:1' || p.owner))) {
1294
1298
  const props = meta.props.filter(prop => this.platform.shouldHaveColumn(prop, populate, options.exclude, false));
@@ -1303,16 +1307,19 @@ export class AbstractSqlDriver extends DatabaseDriver {
1303
1307
  ret.push('*');
1304
1308
  }
1305
1309
  if (ret.length > 0 && !hasExplicitFields && addFormulas) {
1306
- meta.props
1307
- .filter(prop => prop.formula && !lazyProps.includes(prop))
1308
- .forEach(prop => {
1309
- const a = this.platform.quoteIdentifier(alias);
1310
- const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
1311
- ret.push(raw(`${prop.formula(a)} as ${aliased}`));
1312
- });
1313
- meta.props
1314
- .filter(prop => !prop.object && (prop.hasConvertToDatabaseValueSQL || prop.hasConvertToJSValueSQL))
1315
- .forEach(prop => ret.push(prop.name));
1310
+ for (const prop of meta.props) {
1311
+ if (lazyProps.includes(prop)) {
1312
+ continue;
1313
+ }
1314
+ if (prop.formula) {
1315
+ const a = this.platform.quoteIdentifier(alias);
1316
+ const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
1317
+ ret.push(raw(`${prop.formula(a)} as ${aliased}`));
1318
+ }
1319
+ if (!prop.object && (prop.hasConvertToDatabaseValueSQL || prop.hasConvertToJSValueSQL)) {
1320
+ ret.push(prop.name);
1321
+ }
1322
+ }
1316
1323
  }
1317
1324
  // add joined relations after the root entity fields
1318
1325
  if (joinedProps.length > 0) {
@@ -1321,7 +1328,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
1321
1328
  exclude: options.exclude,
1322
1329
  populate,
1323
1330
  parentTableAlias: alias,
1324
- count,
1325
1331
  ...options,
1326
1332
  }));
1327
1333
  }
@@ -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)) {