@mikro-orm/sql 7.3.0-dev.0 → 7.3.0-dev.10

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.
@@ -850,9 +850,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
850
850
  props.forEach(prop => {
851
851
  // For STI with conflicting fieldNames, use discriminator to determine which field gets value
852
852
  if (prop.stiFieldNames && prop.stiFieldNameMap && meta.discriminatorColumn) {
853
- const activeField = prop.stiFieldNameMap[row[meta.discriminatorColumn]];
853
+ // the map misses subtypes whose props were not initialized yet during discovery (root column owner, grandchildren)
854
+ const discriminator = row[meta.discriminatorColumn];
855
+ const childClass = meta.discriminatorMap?.[discriminator];
856
+ const activeField = prop.stiFieldNameMap[discriminator] ??
857
+ (childClass && this.metadata.find(childClass)?.properties[prop.name]?.fieldNames[0]);
854
858
  for (const field of prop.stiFieldNames) {
855
- params.push(field === activeField ? row[prop.name] : null);
859
+ params.push(field === activeField ? (row[prop.name] ?? null) : null);
856
860
  keys.push('?');
857
861
  }
858
862
  return;
@@ -1809,7 +1813,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
1809
1813
  if (!this.platform.shouldHaveColumn(prop, populate, exclude)) {
1810
1814
  return false;
1811
1815
  }
1812
- if (!fields || fields.includes('*') || prop.primary || meta.root.discriminatorColumn === prop.name) {
1816
+ if (!fields ||
1817
+ fields.includes('*') ||
1818
+ prop.primary ||
1819
+ meta.root.discriminatorColumn === prop.name ||
1820
+ meta.root.targetKeys?.includes(prop.name)) {
1813
1821
  return true;
1814
1822
  }
1815
1823
  return fields.some(f => f === prop.name || f.toString().startsWith(prop.name + '.'));
@@ -1865,11 +1873,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
1865
1873
  const tptMeta = this.metadata.get(targetMeta.class);
1866
1874
  this.addTPTPolymorphicJoinsForRelation(qb, tptMeta, tableAlias, fields);
1867
1875
  }
1868
- if (ref) {
1869
- // For filter :ref hints, schedule filter check for each target (no field selection)
1870
- qb.scheduleFilterCheck(targetPath);
1871
- }
1872
- else {
1876
+ // target filters apply per target, both for filter :ref hints and populated relations
1877
+ qb.scheduleFilterCheck(targetPath);
1878
+ if (!ref) {
1873
1879
  // Select fields from each target table
1874
1880
  fields.push(...this.getFieldsForJoinedLoad(qb, targetMeta, {
1875
1881
  ...options,
@@ -2469,6 +2475,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
2469
2475
  }
2470
2476
  if (!options.fields.includes('*') && !options.fields.includes(`${qb.alias}.*`)) {
2471
2477
  ret.unshift(...meta.primaryKeys.filter(pk => !options.fields.includes(pk)));
2478
+ // `targetKey` values identify the entity like its PK, references resolve it by them
2479
+ ret.push(...(meta.root.targetKeys ?? []).filter(key => meta.properties[key] && !options.fields.includes(key)));
2472
2480
  }
2473
2481
  if (meta.root.inheritanceType === 'sti' &&
2474
2482
  !options.fields.includes(`${qb.alias}.${meta.root.discriminatorColumn}`)) {
@@ -1,4 +1,4 @@
1
- import { EntityManager, raw, Utils, } from '@mikro-orm/core';
1
+ import { EntityManager, QueryFlag, raw, Utils, } from '@mikro-orm/core';
2
2
  import { MikroKyselyPlugin } from './plugin/index.js';
3
3
  /**
4
4
  * @inheritDoc
@@ -87,6 +87,7 @@ export class SqlEntityManager extends EntityManager {
87
87
  // match `em.count()` semantics: an active transaction always wins over the requested connection type
88
88
  const connectionType = em.getTransactionContext() ? 'write' : options.connectionType;
89
89
  const qb = em.createQueryBuilder(meta.class, undefined, connectionType);
90
+ qb.unsetFlag(QueryFlag.CONVERT_CUSTOM_TYPES);
90
91
  qb
91
92
  .select([...fields, raw('count(*) as cnt')])
92
93
  .where(where)
@@ -387,6 +387,8 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
387
387
  data_type,
388
388
  is_identity,
389
389
  identity_generation,
390
+ identity_start,
391
+ identity_increment,
390
392
  generation_expression,
391
393
  pg_catalog.col_description(pgc.oid, cols.ordinal_position::int) column_comment,
392
394
  coll.collname as collation_name
@@ -433,6 +435,14 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
433
435
  type += `(${col.numeric_precision},${col.numeric_scale})`;
434
436
  }
435
437
  const length = this.inferLengthFromColumnType(type) === -1 ? -1 : col.length;
438
+ // a descending sequence starts at -1 by default
439
+ const defaultIdentityStart = col.identity_increment?.startsWith('-') ? '-1' : '1';
440
+ // read back non-default identity options, so `identity (start with 1000)` does not diff against plain `identity`
441
+ const identityOptions = [
442
+ col.identity_start !== defaultIdentityStart && `start with ${col.identity_start}`,
443
+ col.identity_increment !== '1' && `increment by ${col.identity_increment}`,
444
+ ].filter(Boolean);
445
+ const identitySuffix = identityOptions.length > 0 ? ` (${identityOptions.join(' ')})` : '';
436
446
  const column = {
437
447
  name: col.column_name,
438
448
  type,
@@ -445,9 +455,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
445
455
  unsigned: increments,
446
456
  autoincrement: increments,
447
457
  generated: col.is_identity === 'YES'
448
- ? col.identity_generation === 'BY DEFAULT'
449
- ? 'by default as identity'
450
- : 'identity'
458
+ ? (col.identity_generation === 'BY DEFAULT' ? 'by default as identity' : 'identity') + identitySuffix
451
459
  : col.generation_expression
452
460
  ? col.generation_expression + ' stored'
453
461
  : undefined,
@@ -1032,7 +1040,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
1032
1040
  columnType = this.quote(columnType);
1033
1041
  }
1034
1042
  }
1035
- if (column.generated === 'by default as identity') {
1043
+ if (column.generated?.startsWith('by default as identity')) {
1036
1044
  columnType += ` generated ${column.generated}`;
1037
1045
  }
1038
1046
  else if (column.generated) {
@@ -499,7 +499,9 @@ export class SqliteSchemaHelper extends SchemaHelper {
499
499
  if (Utils.hasObjectKeys(diff.removedChecks) ||
500
500
  Utils.hasObjectKeys(diff.changedChecks) ||
501
501
  Utils.hasObjectKeys(diff.changedForeignKeys) ||
502
- Utils.hasObjectKeys(diff.changedColumns)) {
502
+ Utils.hasObjectKeys(diff.changedColumns) ||
503
+ // sqlite can only add or change a primary key by rebuilding the table
504
+ [...Object.values(diff.addedIndexes), ...Object.values(diff.changedIndexes)].some(index => index.primary)) {
503
505
  return this.getAlterTempTableSQL(diff);
504
506
  }
505
507
  for (const index of Object.values(diff.removedIndexes)) {
@@ -644,10 +646,11 @@ export class SqliteSchemaHelper extends SchemaHelper {
644
646
  first.replace(`create table ${quotedName}`, `create table ${quotedTempName}`),
645
647
  ];
646
648
  const columns = [];
649
+ const renamedFrom = Object.fromEntries(Object.entries(changedTable.renamedColumns).map(([oldName, column]) => [column.name, oldName]));
647
650
  for (const column of changedTable.toTable.getColumns()) {
648
- const fromColumn = changedTable.fromTable.getColumn(column.name);
649
- if (fromColumn) {
650
- columns.push(this.quote(column.name));
651
+ const fromName = renamedFrom[column.name] ?? column.name;
652
+ if (changedTable.fromTable.getColumn(fromName)) {
653
+ columns.push(this.quote(fromName));
651
654
  }
652
655
  else {
653
656
  columns.push(`null as ${this.quote(column.name)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.3.0-dev.0",
3
+ "version": "7.3.0-dev.10",
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
  "keywords": [
6
6
  "data-mapper",
@@ -50,10 +50,10 @@
50
50
  "kysely": "0.29.6"
51
51
  },
52
52
  "devDependencies": {
53
- "@mikro-orm/core": "^7.2.1"
53
+ "@mikro-orm/core": "^7.2.2"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.3.0-dev.0"
56
+ "@mikro-orm/core": "7.3.0-dev.10"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -92,6 +92,7 @@ export declare class MikroTransformer extends OperationNodeTransformer {
92
92
  * This is called for SELECT queries when columnNamingStrategy is 'property'
93
93
  */
94
94
  transformResult(rows: Record<string, any>[] | undefined, entityMap: Map<string, EntityMetadata>): Record<string, any>[] | undefined;
95
+ private getResultMaps;
95
96
  buildGlobalFieldMap(entityMap: Map<string, EntityMetadata>): Record<string, EntityProperty>;
96
97
  buildGlobalRelationFieldMap(entityMap: Map<string, EntityMetadata>): Record<string, string>;
97
98
  /**
@@ -108,6 +109,6 @@ export declare class MikroTransformer extends OperationNodeTransformer {
108
109
  /**
109
110
  * Transform a single row by mapping column names to property names
110
111
  */
111
- transformRow(row: Record<string, any>, fieldToPropertyMap: Record<string, EntityProperty>, relationFieldMap: Record<string, string>): Record<string, any>;
112
+ transformRow(row: Record<string, any>, fieldEntries: [string, EntityProperty][], relationEntries: [string, string][]): Record<string, any>;
112
113
  prepareOutputValue(prop: EntityProperty | undefined, value: unknown): unknown;
113
114
  }
@@ -1,5 +1,12 @@
1
1
  import { ReferenceKind, isRaw, } from '@mikro-orm/core';
2
2
  import { AliasNode, ColumnNode, ColumnUpdateNode, IdentifierNode, OperationNodeTransformer, PrimitiveValueListNode, RawNode, ReferenceNode, SchemableIdentifierNode, SelectAllNode, SelectionNode, TableNode, ValueListNode, ValueNode, ValuesNode, } from 'kysely';
3
+ /** Past this many query shapes the cache of one metadata storage starts over, so generated aliases cannot grow it. */
4
+ const RESULT_MAPS_LIMIT = 1000;
5
+ /**
6
+ * Result maps per query shape (the aliases and entities of its entity map). They are derived from metadata alone, so
7
+ * they are built once and shared by every transformer, as `getKysely()` creates a new one per call.
8
+ */
9
+ const resultMapsCache = new WeakMap();
3
10
  const EXPANDABLE_KINDS = new Set([
4
11
  ReferenceKind.SCALAR,
5
12
  ReferenceKind.EMBEDDED,
@@ -884,12 +891,7 @@ export class MikroTransformer extends OperationNodeTransformer {
884
891
  if (byEntity) {
885
892
  return byEntity;
886
893
  }
887
- const allMetadata = Array.from(this.#metadata);
888
- const byTable = allMetadata.find(m => m.tableName === name);
889
- if (byTable) {
890
- return byTable;
891
- }
892
- return undefined;
894
+ return this.#metadata.getByTableName(name);
893
895
  }
894
896
  /**
895
897
  * Transform result rows by mapping database column names to property names
@@ -906,11 +908,35 @@ export class MikroTransformer extends OperationNodeTransformer {
906
908
  if (entityMap.size === 0) {
907
909
  return rows;
908
910
  }
909
- // Build a global mapping from database field names to property objects
910
- const fieldToPropertyMap = this.buildGlobalFieldMap(entityMap);
911
- const relationFieldMap = this.buildGlobalRelationFieldMap(entityMap);
911
+ // The global mapping from database field names to property objects, built once per query shape
912
+ const { fields, relations } = this.getResultMaps(entityMap);
912
913
  // Transform each row
913
- return rows.map(row => this.transformRow(row, fieldToPropertyMap, relationFieldMap));
914
+ return rows.map(row => this.transformRow(row, fields, relations));
915
+ }
916
+ getResultMaps(entityMap) {
917
+ let byShape = resultMapsCache.get(this.#metadata);
918
+ if (!byShape) {
919
+ byShape = new Map();
920
+ resultMapsCache.set(this.#metadata, byShape);
921
+ }
922
+ let shape = '';
923
+ for (const [alias, meta] of entityMap) {
924
+ shape += `${alias}\0${meta._id}\0`;
925
+ }
926
+ const cached = byShape.get(shape);
927
+ if (cached && Array.from(entityMap.values()).every((meta, i) => meta.props === cached.props[i])) {
928
+ return cached;
929
+ }
930
+ if (byShape.size >= RESULT_MAPS_LIMIT) {
931
+ byShape.clear();
932
+ }
933
+ const maps = {
934
+ props: Array.from(entityMap.values(), meta => meta.props),
935
+ fields: Object.entries(this.buildGlobalFieldMap(entityMap)),
936
+ relations: Object.entries(this.buildGlobalRelationFieldMap(entityMap)),
937
+ };
938
+ byShape.set(shape, maps);
939
+ return maps;
914
940
  }
915
941
  buildGlobalFieldMap(entityMap) {
916
942
  const map = {};
@@ -986,10 +1012,10 @@ export class MikroTransformer extends OperationNodeTransformer {
986
1012
  /**
987
1013
  * Transform a single row by mapping column names to property names
988
1014
  */
989
- transformRow(row, fieldToPropertyMap, relationFieldMap) {
1015
+ transformRow(row, fieldEntries, relationEntries) {
990
1016
  const transformed = { ...row };
991
1017
  // First pass: map regular fields from fieldName to propertyName and convert values
992
- for (const [fieldName, prop] of Object.entries(fieldToPropertyMap)) {
1018
+ for (const [fieldName, prop] of fieldEntries) {
993
1019
  if (!(fieldName in transformed)) {
994
1020
  continue;
995
1021
  }
@@ -1011,7 +1037,7 @@ export class MikroTransformer extends OperationNodeTransformer {
1011
1037
  // Second pass: handle relation fields
1012
1038
  // Only run if columnNamingStrategy is 'property', as we don't want to rename FKs otherwise
1013
1039
  if (this.#options.columnNamingStrategy === 'property') {
1014
- for (const [fieldName, relationPropertyName] of Object.entries(relationFieldMap)) {
1040
+ for (const [fieldName, relationPropertyName] of relationEntries) {
1015
1041
  if (fieldName in transformed && !(relationPropertyName in transformed)) {
1016
1042
  // Move the foreign key value to the relation property name
1017
1043
  transformed[relationPropertyName] = transformed[fieldName];
@@ -17,6 +17,8 @@ export declare class CriteriaNode<T extends object> implements ICriteriaNode<T>
17
17
  index?: number;
18
18
  constructor(metadata: MetadataStorage, entityName: EntityName<T>, parent?: ICriteriaNode<T> | undefined, key?: EntityKey<T> | RawQueryFragmentSymbol | undefined, validate?: boolean, strict?: boolean);
19
19
  process(qb: IQueryBuilder<T>, options?: ICriteriaNodeProcessOptions): any;
20
+ /** Whether this node holds the condition for a single target of a polymorphic relation (e.g. `imageable[Article]`). */
21
+ isPolymorphicBranch(): boolean;
20
22
  unwrap(): any;
21
23
  shouldInline(payload: any): boolean;
22
24
  willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions): boolean;
@@ -1,4 +1,4 @@
1
- import { RawQueryFragment, ReferenceKind, Utils, inspect, } from '@mikro-orm/core';
1
+ import { QueryHelper, RawQueryFragment, ReferenceKind, Utils, inspect, } from '@mikro-orm/core';
2
2
  /**
3
3
  * Helper for working with deeply nested where/orderBy/having criteria. Uses composite pattern to build tree from the payload.
4
4
  * Auto-joins relations and converts payload from { books: { publisher: { name: '...' } } } to { 'publisher_alias.name': '...' }
@@ -28,7 +28,9 @@ export class CriteriaNode {
28
28
  return;
29
29
  }
30
30
  for (const k of pks) {
31
- this.prop = meta.props.find(prop => prop.name === k || (prop.fieldNames?.length === 1 && prop.fieldNames[0] === k && prop.persist !== false));
31
+ const [name, target] = QueryHelper.splitPolymorphicKey(k);
32
+ this.prop = meta.props.find(prop => (prop.name === name && (!target || QueryHelper.findTargetMetas(prop, k).length > 0)) ||
33
+ (prop.fieldNames?.length === 1 && prop.fieldNames[0] === k && prop.persist !== false));
32
34
  const isProp = this.prop || meta.props.find(prop => (prop.fieldNames || []).includes(k));
33
35
  // do not validate if the key is prefixed or type casted (e.g. `k::text`)
34
36
  if (validate && !isProp && !k.includes('.') && !k.includes('::') && !Utils.isOperator(k)) {
@@ -40,6 +42,10 @@ export class CriteriaNode {
40
42
  process(qb, options) {
41
43
  return this.payload;
42
44
  }
45
+ /** Whether this node holds the condition for a single target of a polymorphic relation (e.g. `imageable[Article]`). */
46
+ isPolymorphicBranch() {
47
+ return !!this.prop?.polymorphic && typeof this.key === 'string' && this.key.endsWith(']');
48
+ }
43
49
  unwrap() {
44
50
  return this.payload;
45
51
  }
@@ -1,4 +1,4 @@
1
- import { GroupOperator, isRaw, JsonType, RawQueryFragment, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';
1
+ import { GroupOperator, isRaw, JsonType, QueryHelper, RawQueryFragment, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';
2
2
  import { ObjectCriteriaNode } from './ObjectCriteriaNode.js';
3
3
  import { ArrayCriteriaNode } from './ArrayCriteriaNode.js';
4
4
  import { ScalarCriteriaNode } from './ScalarCriteriaNode.js';
@@ -50,8 +50,9 @@ export class CriteriaNodeFactory {
50
50
  }
51
51
  static createObjectItemNode(metadata, entityName, node, payload, key, meta, validate = true) {
52
52
  const rawField = RawQueryFragment.isKnownFragmentSymbol(key);
53
- const prop = rawField ? null : meta?.properties[key];
54
- const childEntity = prop && prop.kind !== ReferenceKind.SCALAR ? prop.targetMeta.class : entityName;
53
+ const prop = rawField ? null : meta?.properties[QueryHelper.splitPolymorphicKey(key)[0]];
54
+ const target = rawField ? undefined : QueryHelper.findTargetMeta(prop, key);
55
+ const childEntity = prop && prop.kind !== ReferenceKind.SCALAR && target ? target.class : entityName;
55
56
  const isNotEmbedded = rawField || prop?.kind !== ReferenceKind.EMBEDDED;
56
57
  const val = payload[key];
57
58
  if (isNotEmbedded && prop?.customType instanceof JsonType) {
@@ -5,6 +5,8 @@ import type { ICriteriaNodeProcessOptions, IQueryBuilder } from '../typings.js';
5
5
  */
6
6
  export declare class ObjectCriteriaNode<T extends object> extends CriteriaNode<T> {
7
7
  process(qb: IQueryBuilder<T>, options?: ICriteriaNodeProcessOptions): any;
8
+ /** Orders by the first non-null value of the property across the joined polymorphic targets. */
9
+ private processPolymorphicOrderBy;
8
10
  isStrict(): boolean;
9
11
  unwrap(): any;
10
12
  willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions): boolean;
@@ -1,4 +1,4 @@
1
- import { ALIAS_REPLACEMENT, GroupOperator, QueryFlag, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core';
1
+ import { ALIAS_REPLACEMENT, GroupOperator, QueryFlag, QueryHelper, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core';
2
2
  import { CriteriaNode } from './CriteriaNode.js';
3
3
  import { JoinType, QueryType } from './enums.js';
4
4
  const COLLECTION_OPERATORS = ['$some', '$none', '$every', '$size', '$all'];
@@ -7,6 +7,10 @@ const COLLECTION_OPERATORS = ['$some', '$none', '$every', '$size', '$all'];
7
7
  */
8
8
  export class ObjectCriteriaNode extends CriteriaNode {
9
9
  process(qb, options) {
10
+ const targets = this.isPolymorphicBranch() ? QueryHelper.findTargetMetas(this.prop, this.key) : [];
11
+ if (options?.type === 'orderBy' && targets.length > 1) {
12
+ return this.processPolymorphicOrderBy(qb, targets, options);
13
+ }
10
14
  const matchPopulateJoins = options?.matchPopulateJoins ||
11
15
  (this.prop && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind));
12
16
  const nestedAlias = qb.getAliasForJoinPath(this.getPath(options), { ...options, matchPopulateJoins });
@@ -99,7 +103,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
99
103
  // with an exclusive condition on the join columns:
100
104
  // - if the owning column is null, the row is missing, we don't apply the filter
101
105
  // - if the target column is not null, the row is matched, we apply the filter
102
- if (toOneProperty && this.prop.nullable && this.isStrict()) {
106
+ if (toOneProperty && this.prop.nullable && this.isStrict() && !this.isPolymorphicBranch()) {
103
107
  const key = this.prop.owner ? this.prop.name : this.prop.referencedPKs;
104
108
  qb.andWhere({
105
109
  $or: [
@@ -109,7 +113,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
109
113
  });
110
114
  }
111
115
  }
112
- return keys.reduce((o, field) => {
116
+ const ret = keys.reduce((o, field) => {
113
117
  const childNode = this.payload[field];
114
118
  const payload = childNode.process(qb, { ...options, alias: this.prop ? alias : ownerAlias });
115
119
  const operator = Utils.isOperator(field);
@@ -149,6 +153,31 @@ export class ObjectCriteriaNode extends CriteriaNode {
149
153
  }
150
154
  return o;
151
155
  }, {});
156
+ // the target is left joined, so its condition must only match rows that point to it
157
+ if (this.isPolymorphicBranch() && alias && options?.type !== 'orderBy') {
158
+ const pk = this.metadata.find(this.entityName).primaryKeys[0];
159
+ this.inlineCondition(`${alias}.${pk}`, ret, { $ne: null });
160
+ }
161
+ return ret;
162
+ }
163
+ /** Orders by the first non-null value of the property across the joined polymorphic targets. */
164
+ processPolymorphicOrderBy(qb, targets, options) {
165
+ const ownerAlias = options.alias || qb.alias;
166
+ const aliases = targets.map(target => {
167
+ const path = `${this.parent.getPath()}.${this.prop.name}[${target.className}]`;
168
+ const alias = qb.getAliasForJoinPath(path, { ...options, matchPopulateJoins: true });
169
+ if (alias) {
170
+ return alias;
171
+ }
172
+ const newAlias = qb.getNextAlias(target.class);
173
+ qb.addPolymorphicJoin(this.prop, target, ownerAlias, newAlias, JoinType.leftJoin, path);
174
+ return newAlias;
175
+ });
176
+ return Utils.getObjectQueryKeys(this.payload).reduce((o, field) => {
177
+ const columns = targets.map((target, i) => `${aliases[i]}.${target.properties[field].fieldNames[0]}`);
178
+ o[raw(`coalesce(${columns.map(() => '??').join(', ')})`, columns)] = this.payload[field].payload;
179
+ return o;
180
+ }, {});
152
181
  }
153
182
  isStrict() {
154
183
  return (this.strict ||
@@ -186,8 +215,9 @@ export class ObjectCriteriaNode extends CriteriaNode {
186
215
  const operator = Utils.isObject(payload) &&
187
216
  Utils.getObjectQueryKeys(payload).every(k => {
188
217
  if (k === '$not' && Utils.isPlainObject(payload[k])) {
189
- // $not wrapping non-operator conditions (entity props) should be inlined
190
- return Utils.getObjectQueryKeys(payload[k]).every(ik => Utils.isOperator(ik, false));
218
+ // $not wrapping non-operator conditions (entity props) or an empty one should be inlined
219
+ const inner = Utils.getObjectQueryKeys(payload[k]);
220
+ return inner.length > 0 && inner.every(ik => Utils.isOperator(ik, false));
191
221
  }
192
222
  return Utils.isOperator(k, false);
193
223
  });
@@ -211,16 +241,19 @@ export class ObjectCriteriaNode extends CriteriaNode {
211
241
  this.inlineCondition(key, obj, value);
212
242
  }
213
243
  inlineChildPayload(o, payload, field, alias, childAlias) {
214
- const prop = this.metadata.find(this.entityName).properties[field];
244
+ const prop = this.payload[field].prop;
215
245
  for (const k of Utils.getObjectQueryKeys(payload)) {
216
246
  if (RawQueryFragment.isKnownFragmentSymbol(k)) {
217
247
  o[k] = payload[k];
218
248
  }
219
249
  else if (k === '$not' &&
220
250
  Utils.isPlainObject(payload[k]) &&
221
- Utils.getObjectQueryKeys(payload[k]).some(ik => !Utils.isOperator(ik, false))) {
222
- // $not wraps entity conditions (from auto-join), inline at current level
223
- this.inlineCondition(k, o, payload[k]);
251
+ (!Utils.hasObjectKeys(payload[k]) ||
252
+ Utils.getObjectQueryKeys(payload[k]).some(ik => !Utils.isOperator(ik, false)))) {
253
+ // $not wraps entity conditions (from auto-join) or an empty one, inline at current level with the same key mapping
254
+ const inner = {};
255
+ this.inlineChildPayload(inner, payload[k], field, alias, childAlias);
256
+ this.inlineCondition(k, o, inner);
224
257
  }
225
258
  else if (Utils.isOperator(k, false)) {
226
259
  const tmp = payload[k];
@@ -273,6 +306,10 @@ export class ObjectCriteriaNode extends CriteriaNode {
273
306
  if (keys.some(k => this.isCollectionOperator(k))) {
274
307
  return true;
275
308
  }
309
+ // the target's discriminator is checked in the join condition, so it is needed even for PK conditions
310
+ if (this.isPolymorphicBranch()) {
311
+ return !nestedAlias;
312
+ }
276
313
  const meta = this.metadata.find(this.entityName);
277
314
  const embeddable = this.prop.kind === ReferenceKind.EMBEDDED;
278
315
  const knownKey = [ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE, ReferenceKind.EMBEDDED].includes(this.prop.kind) ||
@@ -291,6 +328,13 @@ export class ObjectCriteriaNode extends CriteriaNode {
291
328
  });
292
329
  const primaryKeys = knownKey &&
293
330
  keys.every(key => {
331
+ if (key === '$not') {
332
+ // e.g. `{ author: { $not: { id: 1 } } }` compares the FK column, relation PKs still need the join
333
+ const childPayload = this.payload[key].payload;
334
+ return (Utils.isPlainObject(childPayload) &&
335
+ Utils.getObjectQueryKeys(childPayload).every(k => meta.primaryKeys.includes(k) &&
336
+ meta.properties[k].kind === ReferenceKind.SCALAR));
337
+ }
294
338
  if (typeof key !== 'string' || !meta.primaryKeys.includes(key)) {
295
339
  return false;
296
340
  }
@@ -317,10 +361,16 @@ export class ObjectCriteriaNode extends CriteriaNode {
317
361
  if (this.prop.kind === ReferenceKind.MANY_TO_MANY && (scalar || operator)) {
318
362
  qb.join(field, nestedAlias, undefined, JoinType.pivotJoin, path);
319
363
  }
364
+ else if (this.isPolymorphicBranch()) {
365
+ const targetMeta = this.metadata.find(this.entityName);
366
+ qb.addPolymorphicJoin(this.prop, targetMeta, alias, nestedAlias, JoinType.leftJoin, path);
367
+ }
320
368
  else {
321
369
  const prev = qb.state.fields?.slice();
322
370
  const toOneProperty = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind);
323
- const joinType = toOneProperty && !this.prop.nullable ? JoinType.innerJoin : JoinType.leftJoin;
371
+ // ordering must not drop rows whose polymorphic relation points to another target
372
+ const orderByPolymorphic = this.prop.polymorphic && options?.type === 'orderBy';
373
+ const joinType = toOneProperty && !this.prop.nullable && !orderByPolymorphic ? JoinType.innerJoin : JoinType.leftJoin;
324
374
  qb[method](field, nestedAlias, undefined, joinType, path);
325
375
  if (!qb.hasFlag(QueryFlag.INFER_POPULATE)) {
326
376
  qb.state.fields = prev;
@@ -1032,9 +1032,11 @@ export class QueryBuilder {
1032
1032
  * @internal
1033
1033
  */
1034
1034
  addPolymorphicJoin(prop, targetMeta, ownerAlias, alias, type, path, schema) {
1035
- // Override referencedColumnNames to use the specific target's PK columns
1035
+ // Override referencedColumnNames to use the specific target's PK (or `targetKey`) columns
1036
1036
  // (polymorphic targets may have different PK column names, e.g. org_id vs user_id)
1037
- const referencedColumnNames = targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
1037
+ const referencedColumnNames = prop.targetKey
1038
+ ? targetMeta.properties[prop.targetKey].fieldNames
1039
+ : targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
1038
1040
  const targetProp = { ...prop, targetMeta, referencedColumnNames };
1039
1041
  const aliasedName = `${ownerAlias}.${prop.name}[${targetMeta.className}]#${alias}`;
1040
1042
  this.#state.joins[aliasedName] = this.helper.joinManyToOneReference(targetProp, ownerAlias, alias, type, {}, schema);
@@ -474,10 +474,16 @@ export class SchemaHelper {
474
474
  for (const foreignKey of Object.values(diff.changedForeignKeys)) {
475
475
  ret.push(this.createForeignKey(diff.toTable, foreignKey));
476
476
  }
477
- for (const index of Object.values(diff.addedIndexes)) {
478
- ret.push(this.createIndex(index, diff.toTable));
479
- }
480
- for (const index of Object.values(diff.changedIndexes)) {
477
+ for (const index of [...Object.values(diff.addedIndexes), ...Object.values(diff.changedIndexes)]) {
478
+ const addedColumn = diff.addedColumns[index.columnNames[0]];
479
+ // an added autoincrement PK column already declares the primary key inline
480
+ if (index.primary &&
481
+ !index.composite &&
482
+ addedColumn?.autoincrement &&
483
+ addedColumn.primary &&
484
+ !this.hasNonDefaultPrimaryKeyName(diff.toTable)) {
485
+ continue;
486
+ }
481
487
  ret.push(this.createIndex(index, diff.toTable, true));
482
488
  }
483
489
  for (const [oldIndexName, index] of Object.entries(diff.renamedIndexes)) {
package/typings.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Generated, Kysely } from 'kysely';
2
- import type { CheckCallback, DeferMode, Dictionary, EntityName, EntityProperty, EntitySchemaWithMeta, ExtractDefineEntityProperties, FilterQuery, GroupOperator, IndexColumnOptions, InferEntityName, Opt, Primary, PrimaryProperty, QueryFlag, QueryOrderMap, RawQueryFragment, RoutineIgnoreField, Scalar, Type } from '@mikro-orm/core';
2
+ import type { CheckCallback, DeferMode, Dictionary, EntityName, EntityMetadata, EntityProperty, EntitySchemaWithMeta, ExtractDefineEntityProperties, FilterQuery, GroupOperator, IndexColumnOptions, InferEntityName, Opt, Primary, PrimaryProperty, QueryFlag, QueryOrderMap, RawQueryFragment, RoutineIgnoreField, Scalar, Type } from '@mikro-orm/core';
3
3
  import type { JoinType, QueryType } from './query/enums.js';
4
4
  import type { DatabaseSchema } from './schema/DatabaseSchema.js';
5
5
  import type { DatabaseTable } from './schema/DatabaseTable.js';
@@ -295,6 +295,8 @@ export interface IQueryBuilder<T> {
295
295
  scheduleFilterCheck(path: string): void;
296
296
  /** @internal */
297
297
  ensureTPTJoins(): void;
298
+ /** @internal */
299
+ addPolymorphicJoin(prop: EntityProperty, targetMeta: EntityMetadata, ownerAlias: string, alias: string, type: JoinType, path: string, schema?: string): void;
298
300
  withSchema(schema: string): this;
299
301
  }
300
302
  export interface ICriteriaNodeProcessOptions {