@mikro-orm/sql 7.2.0-dev.12 → 7.2.0-dev.14

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.
@@ -118,13 +118,6 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
118
118
  mergeJoinedResult<T extends object>(rawResults: EntityData<T>[], meta: EntityMetadata<T>, joinedProps: PopulateOptions<T>[]): EntityData<T>[];
119
119
  protected shouldHaveColumn<T, U>(meta: EntityMetadata<T>, prop: EntityProperty<U>, populate: readonly PopulateOptions<U>[], fields?: readonly InternalField<U>[], exclude?: readonly InternalField<U>[]): boolean;
120
120
  protected getFieldsForJoinedLoad<T extends object>(qb: AnyQueryBuilder<T>, meta: EntityMetadata<T>, options: FieldsForJoinedLoadOptions<T>): InternalField<T>[];
121
- /**
122
- * Walks the TPT inheritance chain of `leafMeta` and INNER JOINs each parent table.
123
- * Registers the parent aliases in `qb.state.tptAlias` so column resolution finds them
124
- * when filter conditions reference parent-table columns.
125
- * @internal
126
- */
127
- protected addTPTParentJoinsForRelation<T extends object>(qb: AnyQueryBuilder<T>, leafMeta: EntityMetadata, leafAlias: string, basePath: string): void;
128
121
  /**
129
122
  * Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
130
123
  * @internal
@@ -915,7 +915,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
915
915
  if (!options.upsert && options.unionWhere?.length) {
916
916
  where = (await this.applyUnionWhere(meta, where, options, true));
917
917
  }
918
- if (Utils.hasObjectKeys(data)) {
918
+ if (options.upsert && meta.tptParent) {
919
+ res = await this.nativeUpdateMany(entityName, [where], [data], options);
920
+ }
921
+ else if (Utils.hasObjectKeys(data) || (meta.inheritanceType === 'tpt' && meta.ownsVersionProperty())) {
922
+ // a TPT table declaring the version property is bumped even when only other tables of the hierarchy changed
919
923
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
920
924
  qb.setAbortOptions(pickAbortOptions(options));
921
925
  if (options.upsert) {
@@ -941,7 +945,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
941
945
  qb.update(data).where(where);
942
946
  // reload generated columns and version fields
943
947
  const returning = [];
944
- meta.props
948
+ this.getTableProps(meta)
945
949
  .filter(prop => (prop.generated && !prop.primary) || prop.version)
946
950
  .forEach(prop => returning.push(prop.name));
947
951
  qb.returning(returning);
@@ -957,13 +961,38 @@ export class AbstractSqlDriver extends DatabaseDriver {
957
961
  options.convertCustomTypes ??= true;
958
962
  const meta = this.metadata.get(entityName);
959
963
  if (options.upsert) {
964
+ if (meta.tptParent) {
965
+ // TPT parent tables go first, the PK they provide is the conflict target of this table
966
+ await this.nativeUpdateMany(meta.tptParent.class, where, data, options);
967
+ for (const [i, row] of data.entries()) {
968
+ if (meta.primaryKeys.some(pk => row[pk] == null)) {
969
+ const found = await this.findOne(meta.tptParent.class, where[i], {
970
+ fields: meta.primaryKeys,
971
+ ctx: options.ctx,
972
+ connectionType: 'write',
973
+ schema: options.schema,
974
+ });
975
+ meta.primaryKeys.forEach(pk => (row[pk] = found?.[pk]));
976
+ }
977
+ }
978
+ options = { ...options, onConflictFields: meta.primaryKeys, onConflictWhere: undefined };
979
+ }
960
980
  const uniqueFields = options.onConflictFields ??
961
981
  (Utils.isPlainObject(where[0])
962
982
  ? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key))
963
983
  : meta.primaryKeys);
964
984
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
965
985
  qb.setAbortOptions(pickAbortOptions(options));
966
- const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
986
+ let returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
987
+ if (meta.inheritanceType === 'tpt') {
988
+ // each TPT table only carries its own columns, the entity is reloaded instead of mapping the returned rows
989
+ const own = (key) => meta.primaryKeys.includes(key) ||
990
+ this.getTableProps(meta).some(prop => prop.name === key.split('.')[0]);
991
+ data = data.map(row => Object.fromEntries(Object.entries(row).filter(([key]) => own(key))));
992
+ options.onConflictMergeFields = options.onConflictMergeFields?.filter(f => own(f));
993
+ options.onConflictExcludeFields = options.onConflictExcludeFields?.filter(f => own(f));
994
+ returning = [];
995
+ }
967
996
  qb.insert(data)
968
997
  .onConflict(uniqueFields)
969
998
  .returning(returning);
@@ -977,7 +1006,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
977
1006
  if (options.onConflictWhere) {
978
1007
  qb.where(options.onConflictWhere);
979
1008
  }
980
- return this.rethrow(qb.execute('run', false));
1009
+ const res = await this.rethrow(qb.execute('run', false));
1010
+ return meta.inheritanceType === 'tpt' ? { ...res, row: undefined, rows: [] } : res;
981
1011
  }
982
1012
  const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
983
1013
  const keys = new Set();
@@ -992,7 +1022,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
992
1022
  }
993
1023
  }
994
1024
  // reload generated columns and version fields
995
- meta.props.filter(prop => prop.generated || prop.version || prop.primary).forEach(prop => returning.add(prop.name));
1025
+ meta.getPrimaryProps().forEach(prop => returning.add(prop.name));
1026
+ this.getTableProps(meta)
1027
+ .filter(prop => prop.generated || prop.version)
1028
+ .forEach(prop => returning.add(prop.name));
996
1029
  const pkCond = Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames))
997
1030
  .map(pk => `${this.platform.quoteIdentifier(pk)} = ?`)
998
1031
  .join(' and ');
@@ -1050,7 +1083,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1050
1083
  return sql;
1051
1084
  });
1052
1085
  }
1053
- if (meta.versionProperty) {
1086
+ if (meta.ownsVersionProperty()) {
1054
1087
  const versionProperty = meta.properties[meta.versionProperty];
1055
1088
  const quotedFieldName = this.platform.quoteIdentifier(versionProperty.fieldNames[0]);
1056
1089
  sql += `${quotedFieldName} = `;
@@ -1063,7 +1096,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1063
1096
  sql += `, `;
1064
1097
  }
1065
1098
  sql = sql.substring(0, sql.length - 2) + ' where ';
1066
- const pkProps = meta.primaryKeys.concat(...meta.concurrencyCheckKeys);
1099
+ const pkProps = meta.primaryKeys.concat(...meta.getOwnConcurrencyCheckKeys());
1067
1100
  const pks = Utils.flatten(pkProps.map(pk => meta.properties[pk].fieldNames));
1068
1101
  const useTupleIn = pks.length <= 1 || this.platform.allowsComparingTuples();
1069
1102
  const condTemplate = useTupleIn
@@ -1649,6 +1682,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
1649
1682
  if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
1650
1683
  return false;
1651
1684
  }
1685
+ // Polymorphic to-one flattened from an object embeddable lives inside a JSON column, so the join
1686
+ // conditions would need JSON extraction for both the discriminator and the FK; fall back to SELECT_IN.
1687
+ if (prop.polymorphic && prop.object && prop.embedded) {
1688
+ return false;
1689
+ }
1652
1690
  if (strategy !== LoadStrategy.JOINED) {
1653
1691
  // force joined strategy for explicit 1:1 owner populate hint as it would require a join anyway
1654
1692
  return prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner;
@@ -1779,7 +1817,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1779
1817
  // INNER JOINs get nested inside the polymorphic LEFT JOIN by processNestedJoins, which
1780
1818
  // keeps the resulting query valid for rows pointing to other polymorphic targets.
1781
1819
  if (targetMeta.inheritanceType === 'tpt' && targetMeta.tptParent) {
1782
- this.addTPTParentJoinsForRelation(qb, targetMeta, tableAlias, targetPath);
1820
+ qb.addTPTParentJoins(targetMeta, tableAlias, targetPath);
1783
1821
  }
1784
1822
  // For polymorphic targets that are TPT base classes, also LEFT JOIN
1785
1823
  // all descendant tables so child-specific fields can be selected.
@@ -1827,10 +1865,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
1827
1865
  : JoinType.leftJoin;
1828
1866
  const schema = prop.targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : prop.targetMeta.schema;
1829
1867
  qb.join(field, tableAlias, {}, joinType, path, schema);
1830
- // For relations to TPT child entities, INNER JOIN parent tables (GH #7469)
1831
- if (meta2.inheritanceType === 'tpt' && meta2.tptParent) {
1832
- this.addTPTParentJoinsForRelation(qb, meta2, tableAlias, path);
1833
- }
1834
1868
  // For relations to TPT base classes, add LEFT JOINs for all child tables (polymorphic loading)
1835
1869
  if (meta2.inheritanceType === 'tpt' && meta2.tptChildren?.length && !ref) {
1836
1870
  // Use the registry metadata to ensure allTPTDescendants is available
@@ -1877,25 +1911,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
1877
1911
  }
1878
1912
  return fields;
1879
1913
  }
1880
- /**
1881
- * Walks the TPT inheritance chain of `leafMeta` and INNER JOINs each parent table.
1882
- * Registers the parent aliases in `qb.state.tptAlias` so column resolution finds them
1883
- * when filter conditions reference parent-table columns.
1884
- * @internal
1885
- */
1886
- addTPTParentJoinsForRelation(qb, leafMeta, leafAlias, basePath) {
1887
- let childAlias = leafAlias;
1888
- let childMeta = leafMeta;
1889
- while (childMeta.tptParent) {
1890
- const parentMeta = childMeta.tptParent;
1891
- const parentAlias = qb.getNextAlias(parentMeta.className);
1892
- qb.createAlias(parentMeta.class, parentAlias);
1893
- qb.state.tptAlias[`${leafAlias}:${parentMeta.className}`] = parentAlias;
1894
- qb.addPropertyJoin(childMeta.tptParentProp, childAlias, parentAlias, JoinType.innerJoin, `${basePath}.[tpt]${childMeta.className}`);
1895
- childAlias = parentAlias;
1896
- childMeta = parentMeta;
1897
- }
1898
- }
1899
1914
  /**
1900
1915
  * Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
1901
1916
  * @internal
@@ -28,7 +28,7 @@ export declare abstract class AbstractSqlPlatform extends Platform {
28
28
  getReleaseSavepointSQL(savepointName: string): string;
29
29
  quoteValue(value: any): string;
30
30
  getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string | RawQueryFragment;
31
- getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string | RawQueryFragment;
31
+ getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean | string, value?: unknown): string | RawQueryFragment;
32
32
  /**
33
33
  * Quotes a key for use inside a JSON path expression (e.g. `$.key`).
34
34
  * Simple alphanumeric keys are left unquoted; others are wrapped in double quotes
@@ -75,6 +75,9 @@ export class AbstractSqlPlatform extends Platform {
75
75
  getSearchJsonPropertyKey(path, type, aliased, value) {
76
76
  const [a, ...b] = path;
77
77
  const jsonPath = this.quoteValue(`$.${b.map(this.quoteJsonKey).join('.')}`);
78
+ if (typeof aliased === 'string') {
79
+ return raw(`json_extract(${this.quoteIdentifier(`${aliased}.${a}`)}, ${jsonPath})`);
80
+ }
78
81
  if (aliased) {
79
82
  return raw(alias => `json_extract(${this.quoteIdentifier(`${alias}.${a}`)}, ${jsonPath})`);
80
83
  }
@@ -81,7 +81,9 @@ export class SqlEntityManager extends EntityManager {
81
81
  const { where: rawWhere, ...countOptions } = options;
82
82
  await em.tryFlush(entityName, options);
83
83
  const where = await em.processWhere(entityName, rawWhere ?? {}, options, 'read');
84
- const qb = em.createQueryBuilder(meta.class);
84
+ // match `em.count()` semantics: an active transaction always wins over the requested connection type
85
+ const connectionType = em.getTransactionContext() ? 'write' : options.connectionType;
86
+ const qb = em.createQueryBuilder(meta.class, undefined, connectionType);
85
87
  qb
86
88
  .select([...fields, raw('count(*) as cnt')])
87
89
  .where(where)
@@ -88,7 +88,7 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
88
88
  }): string;
89
89
  getBlobDeclarationSQL(): string;
90
90
  getJsonDeclarationSQL(): string;
91
- getSearchJsonPropertyKey(path: string[], type: string | undefined | Type, aliased: boolean, value?: unknown): string | RawQueryFragment;
91
+ getSearchJsonPropertyKey(path: string[], type: string | undefined | Type, aliased: boolean | string, value?: unknown): string | RawQueryFragment;
92
92
  getJsonIndexDefinition(index: IndexDef): string[];
93
93
  quoteIdentifier(id: string | {
94
94
  toString: () => string;
@@ -285,7 +285,8 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
285
285
  getSearchJsonPropertyKey(path, type, aliased, value) {
286
286
  const first = path.shift();
287
287
  const last = path.pop();
288
- const root = this.quoteIdentifier(aliased ? `${ALIAS_REPLACEMENT}.${first}` : first);
288
+ const alias = typeof aliased === 'string' ? aliased : ALIAS_REPLACEMENT;
289
+ const root = this.quoteIdentifier(aliased ? `${alias}.${first}` : first);
289
290
  type = typeof type === 'string' ? this.getMappedType(type).runtimeType : String(type);
290
291
  const cast = (key) => raw(type in this.#jsonTypeCasts ? `(${key})::${this.#jsonTypeCasts[type]}` : key);
291
292
  let lastOperator = '->>';
@@ -494,7 +494,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
494
494
  // SchemaHelper.createCheck).
495
495
  const m = /^check \(\((.*)\)\)$/is.exec(check.expression);
496
496
  const single = m ? null : /^check \((.*)\)$/is.exec(check.expression);
497
- const def = m ? m[1].replace(/\(([^()]*)\)::\w+/g, '$1') : single ? single[1] : check.expression;
497
+ const def = m ? m[1].replace(/\(([^()]*)\)::\w+(?:\[\])?/g, '$1') : single ? single[1] : check.expression;
498
498
  ret[key].push({
499
499
  name: check.name,
500
500
  columnName: check.column_name,
@@ -489,7 +489,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
489
489
  * Foreign key references can only point to tables in the same database.
490
490
  */
491
491
  getReferencedTableName(referencedTableName, schema) {
492
- const [schemaName, tableName] = this.splitTableName(referencedTableName);
492
+ const [, tableName] = this.splitTableName(referencedTableName);
493
493
  // Strip any schema prefix - SQLite REFERENCES clause doesn't support it
494
494
  return tableName;
495
495
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.2.0-dev.12",
3
+ "version": "7.2.0-dev.14",
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.5"
51
51
  },
52
52
  "devDependencies": {
53
- "@mikro-orm/core": "^7.1.11"
53
+ "@mikro-orm/core": "^7.1.15"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.2.0-dev.12"
56
+ "@mikro-orm/core": "7.2.0-dev.14"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -1,5 +1,5 @@
1
1
  import { type EntityMetadata, type EntityProperty, type Type } from '@mikro-orm/core';
2
- import { type CommonTableExpressionNameNode, type DeleteQueryNode, type InsertQueryNode, type JoinNode, type MergeQueryNode, type OperationNode, type QueryId, type SelectQueryNode, type UpdateQueryNode, type WithNode, ColumnNode, IdentifierNode, OperationNodeTransformer, SelectionNode, TableNode, ValueNode } from 'kysely';
2
+ import { type BinaryOperationNode, type CommonTableExpressionNameNode, type DeleteQueryNode, type InsertQueryNode, type JoinNode, type MergeQueryNode, type OperationNode, type QueryId, type SelectQueryNode, type UpdateQueryNode, type WithNode, ColumnNode, IdentifierNode, OperationNodeTransformer, SelectionNode, TableNode, ValueNode } from 'kysely';
3
3
  import type { MikroKyselyPluginOptions } from './index.js';
4
4
  import type { SqlEntityManager } from '../SqlEntityManager.js';
5
5
  export declare class MikroTransformer extends OperationNodeTransformer {
@@ -28,6 +28,12 @@ export declare class MikroTransformer extends OperationNodeTransformer {
28
28
  processOnUpdateHooks(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode;
29
29
  processInsertValues(node: InsertQueryNode, meta: EntityMetadata): InsertQueryNode;
30
30
  processUpdateValues(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode;
31
+ transformBinaryOperation(node: BinaryOperationNode, queryId: QueryId): BinaryOperationNode;
32
+ /** Resolve the entity property a comparison's left operand refers to, so its value operand can be converted. */
33
+ resolveOperandProperty(operand: OperationNode): {
34
+ prop: EntityProperty;
35
+ fieldName: string;
36
+ } | undefined;
31
37
  processInputValueNode(prop: EntityProperty | undefined, fieldName: string | undefined, valueNode: ValueNode): OperationNode;
32
38
  expandSelections(selections: readonly SelectionNode[]): readonly SelectionNode[];
33
39
  expandSelection(sel: SelectionNode): SelectionNode[] | null;
@@ -455,6 +455,60 @@ export class MikroTransformer extends OperationNodeTransformer {
455
455
  updates,
456
456
  };
457
457
  }
458
+ transformBinaryOperation(node, queryId) {
459
+ const transformed = super.transformBinaryOperation(node, queryId);
460
+ if (!this.#options.convertValues) {
461
+ return transformed;
462
+ }
463
+ const resolved = this.resolveOperandProperty(transformed.leftOperand);
464
+ if (!resolved) {
465
+ return transformed;
466
+ }
467
+ const { prop, fieldName } = resolved;
468
+ const right = transformed.rightOperand;
469
+ if (ValueNode.is(right)) {
470
+ const converted = this.processInputValueNode(prop, fieldName, right);
471
+ return converted === right ? transformed : { ...transformed, rightOperand: converted };
472
+ }
473
+ if (PrimitiveValueListNode.is(right)) {
474
+ // upgrade to ValueListNode when the type needs SQL-side wrapping, since
475
+ // PrimitiveValueListNode can only hold primitives
476
+ if (prop.hasConvertToDatabaseValueSQL) {
477
+ const values = right.values.map(value => this.processInputValueNode(prop, fieldName, ValueNode.create(value)));
478
+ return { ...transformed, rightOperand: ValueListNode.create(values) };
479
+ }
480
+ const values = right.values.map(value => this.prepareInputValue(prop, value, true));
481
+ return values.every((value, idx) => value === right.values[idx])
482
+ ? transformed
483
+ : { ...transformed, rightOperand: PrimitiveValueListNode.create(values) };
484
+ }
485
+ if (ValueListNode.is(right)) {
486
+ let changed = false;
487
+ const values = right.values.map(valueNode => {
488
+ if (!ValueNode.is(valueNode)) {
489
+ return valueNode;
490
+ }
491
+ const converted = this.processInputValueNode(prop, fieldName, valueNode);
492
+ if (converted !== valueNode) {
493
+ changed = true;
494
+ }
495
+ return converted;
496
+ });
497
+ return changed ? { ...transformed, rightOperand: ValueListNode.create(values) } : transformed;
498
+ }
499
+ return transformed;
500
+ }
501
+ /** Resolve the entity property a comparison's left operand refers to, so its value operand can be converted. */
502
+ resolveOperandProperty(operand) {
503
+ if (!ReferenceNode.is(operand) || !ColumnNode.is(operand.column)) {
504
+ return undefined;
505
+ }
506
+ const tableName = operand.table ? this.getTableName(operand.table) : undefined;
507
+ const meta = this.findOwnerMeta(tableName);
508
+ const fieldName = this.normalizeColumnName(operand.column.column);
509
+ const prop = this.findProperty(meta, fieldName);
510
+ return prop ? { prop, fieldName } : undefined;
511
+ }
458
512
  processInputValueNode(prop, fieldName, valueNode) {
459
513
  const converted = this.prepareInputValue(prop, valueNode.value, true);
460
514
  const newValueNode = converted === valueNode.value
@@ -543,8 +597,13 @@ export class MikroTransformer extends OperationNodeTransformer {
543
597
  if (name) {
544
598
  return this.lookupInContextStack(name) ?? this.#subqueryAliasMap.get(name) ?? this.findEntityMetadata(name);
545
599
  }
600
+ // the stack can be empty when transforming a raw root node with embedded expressions
601
+ const context = this.#contextStack[this.#contextStack.length - 1];
602
+ if (!context) {
603
+ return undefined;
604
+ }
546
605
  let single;
547
- for (const meta of this.#contextStack[this.#contextStack.length - 1].values()) {
606
+ for (const meta of context.values()) {
548
607
  if (!meta) {
549
608
  continue;
550
609
  }
@@ -57,6 +57,10 @@ export class CriteriaNodeFactory {
57
57
  if (isNotEmbedded && prop?.customType instanceof JsonType) {
58
58
  return this.createScalarNode(metadata, childEntity, val, node, key, validate);
59
59
  }
60
+ // operator payloads under an unresolvable alias-prefixed key (e.g. `a.meta`) are opaque values, not entity criteria
61
+ if (!prop && !rawField && Utils.isOperator(key, false) && String(node.key).includes('.')) {
62
+ return this.createScalarNode(metadata, entityName, val, node, key, validate);
63
+ }
60
64
  if (prop?.kind === ReferenceKind.SCALAR && val != null && Object.keys(val).some(f => f in GroupOperator)) {
61
65
  throw ValidationError.cannotUseGroupOperatorsInsideScalars(entityName, prop.name, payload);
62
66
  }
@@ -113,7 +113,8 @@ export class ObjectCriteriaNode extends CriteriaNode {
113
113
  }
114
114
  else if (isRawField) {
115
115
  const rawField = RawQueryFragment.getKnownFragment(field);
116
- o[raw(rawField.sql.replaceAll(ALIAS_REPLACEMENT, alias), rawField.params)] = payload;
116
+ qb.ensureTPTJoins();
117
+ o[raw(qb.helper.replaceAliases(rawField.sql, alias), rawField.params)] = payload;
117
118
  }
118
119
  else if (!childNode.validate && !childNode.prop && !field.includes('.') && !operator) {
119
120
  // wrap unknown fields in raw() to prevent alias prefixing (e.g. raw SQL aliases in HAVING)
@@ -439,7 +439,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
439
439
  */
440
440
  join<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field, alias: Alias, cond?: JoinCondition<JoinedEntityType<Entity, Context, Field & string>, Alias>, type?: JoinType, path?: string, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>, RawAliases, Fields, CTEs>;
441
441
  /**
442
- * Adds a JOIN clause to the query for a subquery.
442
+ * Adds a JOIN clause to the query for a subquery. Use `sql.ref('...')` to join a table or CTE by name.
443
443
  */
444
444
  join<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, type?: JoinType, path?: string, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
445
445
  /**
@@ -447,7 +447,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
447
447
  */
448
448
  innerJoin<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field, alias: Alias, cond?: JoinCondition<JoinedEntityType<Entity, Context, Field & string>, Alias>, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>, RawAliases, Fields, CTEs>;
449
449
  /**
450
- * Adds an INNER JOIN clause to the query for a subquery.
450
+ * Adds an INNER JOIN clause to the query for a subquery. Use `sql.ref('...')` to join a table or CTE by name.
451
451
  */
452
452
  innerJoin<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
453
453
  innerJoinLateral<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
@@ -456,7 +456,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
456
456
  */
457
457
  leftJoin<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field, alias: Alias, cond?: JoinCondition<JoinedEntityType<Entity, Context, Field & string>, Alias>, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>, RawAliases, Fields, CTEs>;
458
458
  /**
459
- * Adds a LEFT JOIN clause to the query for a subquery.
459
+ * Adds a LEFT JOIN clause to the query for a subquery. Use `sql.ref('...')` to join a table or CTE by name.
460
460
  */
461
461
  leftJoin<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
462
462
  leftJoinLateral<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
@@ -782,6 +782,8 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
782
782
  * @internal
783
783
  */
784
784
  getJoinForPath(path: string, options?: ICriteriaNodeProcessOptions): JoinOptions | undefined;
785
+ /** Branch-insensitive path matching still must not cross branches — segments with different explicit branch markers (e.g. `Mobile[0]` vs `Mobile[1]`) belong to sibling joins. */
786
+ private branchesConflict;
785
787
  /**
786
788
  * @internal
787
789
  */
@@ -948,6 +950,13 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
948
950
  */
949
951
  addPropertyJoin(prop: EntityProperty, ownerAlias: string, alias: string, type: JoinType, path: string, schema?: string): string;
950
952
  private joinReference;
953
+ /**
954
+ * Walks the TPT inheritance chain of `leafMeta` and INNER JOINs each parent table.
955
+ * Registers the parent aliases in `state.tptAlias` so column resolution finds them
956
+ * when conditions reference parent-table columns.
957
+ * @internal
958
+ */
959
+ addTPTParentJoins(leafMeta: EntityMetadata, leafAlias: string, basePath: string): void;
951
960
  protected prepareFields<T>(fields: InternalField<T>[], type?: 'where' | 'groupBy' | 'sub-query', schema?: string): (string | RawQueryFragment)[];
952
961
  /**
953
962
  * Resolves nested paths like `a.books.title` to their actual field references.
@@ -994,6 +1003,20 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
994
1003
  processPopulateHint(): void;
995
1004
  private processPopulateWhere;
996
1005
  private mergeOnConditions;
1006
+ /**
1007
+ * `$or` branches can be moved to a join's `on` clause only when they all target that same join
1008
+ * and stay flat — a partial `$or` in the `on` clause would drop rows matching a sibling branch,
1009
+ * and nested operators cannot be preserved inside a distributed disjunction, as `mergeOnConditions`
1010
+ * would flatten them into `and` conjuncts.
1011
+ */
1012
+ private canDistributeOrBranches;
1013
+ private getOrBranchAliases;
1014
+ /**
1015
+ * An entity filter's `$or` that cannot be distributed is applied intact to the `on` clause of the
1016
+ * outermost join common to all targeted joins, nesting the targeted joins under it so the clause
1017
+ * can reference their aliases.
1018
+ */
1019
+ private mergeFilterOrCondition;
997
1020
  /**
998
1021
  * When adding an inner join on a left joined relation, we need to nest them,
999
1022
  * otherwise the inner join could discard rows of the root table.
@@ -397,7 +397,10 @@ export class QueryBuilder {
397
397
  * @internal
398
398
  */
399
399
  scheduleFilterCheck(path) {
400
- this.#state.autoJoinedPaths.push(path);
400
+ // deduplicate so filters forming a relation cycle cannot reschedule an already visited path forever
401
+ if (!this.#state.autoJoinedPaths.includes(path)) {
402
+ this.#state.autoJoinedPaths.push(path);
403
+ }
401
404
  }
402
405
  /**
403
406
  * @internal
@@ -492,7 +495,10 @@ export class QueryBuilder {
492
495
  return cond.some(c => this.condReferencesAlias(c, alias));
493
496
  }
494
497
  if (Utils.isPlainObject(cond)) {
495
- return Object.entries(cond).some(([key, value]) => key.startsWith(`${alias}.`) || this.condReferencesAlias(value, alias));
498
+ return Object.entries(cond).some(([key, value]) => {
499
+ const [keyAlias, field] = this.helper.splitField(key);
500
+ return this.helper.getTPTAliasForProperty(field, keyAlias) === alias || this.condReferencesAlias(value, alias);
501
+ });
496
502
  }
497
503
  return false;
498
504
  }
@@ -901,6 +907,8 @@ export class QueryBuilder {
901
907
  if (this.type === QueryType.INSERT) {
902
908
  const returningProps = meta.hydrateProps
903
909
  .filter(prop => prop.returning || (prop.persist !== false && ((prop.primary && prop.autoincrement) || prop.defaultRaw)))
910
+ // a TPT table can only return its own columns
911
+ .filter(prop => meta.inheritanceType !== 'tpt' || prop.primary || meta.ownProps.some(p => p.name === prop.name))
904
912
  .filter(prop => !data || !(prop.name in data));
905
913
  if (returningProps.length > 0) {
906
914
  qb.returning(Utils.flatten(returningProps.map(prop => prop.fieldNames)));
@@ -990,12 +998,13 @@ export class QueryBuilder {
990
998
  }
991
999
  if (!join && options?.ignoreBranching) {
992
1000
  join = joins.find(j => {
993
- return j.path?.replace(/\[\d+]/g, '') === path.replace(/\[\d+]/g, '');
1001
+ return j.path?.replace(/\[\d+]/g, '') === path.replace(/\[\d+]/g, '') && !this.branchesConflict(j.path, path);
994
1002
  });
995
1003
  }
996
1004
  if (!join && options?.matchPopulateJoins && options?.ignoreBranching) {
997
1005
  join = joins.find(j => {
998
- return j.path?.replace(/\[\d+]|\[populate]/g, '') === path.replace(/\[\d+]|\[populate]/g, '');
1006
+ return (j.path?.replace(/\[\d+]|\[populate]/g, '') === path.replace(/\[\d+]|\[populate]/g, '') &&
1007
+ !this.branchesConflict(j.path, path));
999
1008
  });
1000
1009
  }
1001
1010
  if (!join && options?.matchPopulateJoins) {
@@ -1005,6 +1014,12 @@ export class QueryBuilder {
1005
1014
  }
1006
1015
  return join;
1007
1016
  }
1017
+ /** Branch-insensitive path matching still must not cross branches — segments with different explicit branch markers (e.g. `Mobile[0]` vs `Mobile[1]`) belong to sibling joins. */
1018
+ branchesConflict(path1, path2) {
1019
+ const markers = (path) => path.split('.').map(segment => /\[(\d+)]/.exec(segment)?.[1]);
1020
+ const markers2 = markers(path2);
1021
+ return markers(path1).some((m, idx) => m != null && markers2[idx] != null && m !== markers2[idx]);
1022
+ }
1008
1023
  /**
1009
1024
  * @internal
1010
1025
  */
@@ -1410,19 +1425,19 @@ export class QueryBuilder {
1410
1425
  prop.targetMeta = field.mainAlias.meta;
1411
1426
  field = field.getNativeQuery();
1412
1427
  }
1413
- if (isRaw(field)) {
1414
- field = this.platform.formatQuery(field.sql, field.params);
1415
- }
1416
1428
  const key = `${this.alias}.${prop.name}#${alias}`;
1417
- this.#state.joins[key] = {
1418
- prop,
1419
- alias,
1420
- type,
1421
- cond,
1422
- schema,
1423
- subquery: field.toString(),
1424
- ownerAlias: this.alias,
1425
- };
1429
+ const join = { prop, alias, type, cond, schema, ownerAlias: this.alias };
1430
+ // `sql.ref('...')` is a bare table/CTE reference, join it by name instead of wrapping it as a sub-query
1431
+ if (isRaw(field) && field.sql === '??' && field.params.length === 1) {
1432
+ join.table = String(field.params[0]);
1433
+ }
1434
+ else {
1435
+ if (isRaw(field)) {
1436
+ field = this.platform.formatQuery(field.sql, field.params);
1437
+ }
1438
+ join.subquery = field.toString();
1439
+ }
1440
+ this.#state.joins[key] = join;
1426
1441
  return { prop, key };
1427
1442
  }
1428
1443
  if (!subquery && type.includes('lateral')) {
@@ -1483,9 +1498,31 @@ export class QueryBuilder {
1483
1498
  this.#state.joins[aliasedName] = this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, cond, schema);
1484
1499
  this.#state.joins[aliasedName].path ??= path;
1485
1500
  }
1501
+ if (prop.targetMeta.inheritanceType === 'tpt' && prop.targetMeta.tptParent) {
1502
+ this.addTPTParentJoins(prop.targetMeta, alias, path);
1503
+ }
1486
1504
  this.nestReferencedJoins(this.#state.joins[aliasedName]);
1487
1505
  return { prop, key: aliasedName };
1488
1506
  }
1507
+ /**
1508
+ * Walks the TPT inheritance chain of `leafMeta` and INNER JOINs each parent table.
1509
+ * Registers the parent aliases in `state.tptAlias` so column resolution finds them
1510
+ * when conditions reference parent-table columns.
1511
+ * @internal
1512
+ */
1513
+ addTPTParentJoins(leafMeta, leafAlias, basePath) {
1514
+ let childAlias = leafAlias;
1515
+ let childMeta = leafMeta;
1516
+ while (childMeta.tptParent) {
1517
+ const parentMeta = childMeta.tptParent;
1518
+ const parentAlias = this.getNextAlias(parentMeta.className);
1519
+ this.createAlias(parentMeta.class, parentAlias);
1520
+ this.#state.tptAlias[`${leafAlias}:${parentMeta.className}`] = parentAlias;
1521
+ this.addPropertyJoin(childMeta.tptParentProp, childAlias, parentAlias, JoinType.innerJoin, `${basePath}.[tpt]${childMeta.className}`);
1522
+ childAlias = parentAlias;
1523
+ childMeta = parentMeta;
1524
+ }
1525
+ }
1489
1526
  prepareFields(fields, type = 'where', schema) {
1490
1527
  const ret = [];
1491
1528
  const getFieldName = (name, customAlias) => {
@@ -2038,6 +2075,13 @@ export class QueryBuilder {
2038
2075
  for (const k of Object.keys(cond)) {
2039
2076
  if (Utils.isOperator(k)) {
2040
2077
  if (Array.isArray(cond[k])) {
2078
+ if (k === '$or' && !this.canDistributeOrBranches(cond[k], joins)) {
2079
+ // entity filters have no other sink, so their `$or` is kept intact on the outermost targeted join instead
2080
+ if (filter) {
2081
+ this.mergeFilterOrCondition(cond[k], joins);
2082
+ }
2083
+ continue;
2084
+ }
2041
2085
  cond[k].forEach((c) => this.mergeOnConditions(joins, c, filter, k));
2042
2086
  }
2043
2087
  /* v8 ignore next */
@@ -2068,8 +2112,66 @@ export class QueryBuilder {
2068
2112
  else {
2069
2113
  join.cond = { ...join.cond, [k]: cond[k] };
2070
2114
  }
2115
+ this.nestReferencedJoins(join);
2116
+ }
2117
+ }
2118
+ }
2119
+ /**
2120
+ * `$or` branches can be moved to a join's `on` clause only when they all target that same join
2121
+ * and stay flat — a partial `$or` in the `on` clause would drop rows matching a sibling branch,
2122
+ * and nested operators cannot be preserved inside a distributed disjunction, as `mergeOnConditions`
2123
+ * would flatten them into `and` conjuncts.
2124
+ */
2125
+ canDistributeOrBranches(branches, joins) {
2126
+ const aliases = this.getOrBranchAliases(branches);
2127
+ const targeted = joins.filter(j => aliases.has(j.alias));
2128
+ const flat = branches.every(branch => Object.keys(branch).every(k => !Utils.isOperator(k)));
2129
+ return aliases.size === 1 && targeted.length === 1 && flat;
2130
+ }
2131
+ getOrBranchAliases(branches) {
2132
+ const aliases = new Set();
2133
+ const collectAliases = (cond) => {
2134
+ for (const k of Object.keys(cond)) {
2135
+ if (Utils.isOperator(k)) {
2136
+ Utils.asArray(cond[k]).forEach((c) => collectAliases(c));
2137
+ }
2138
+ else {
2139
+ aliases.add(this.helper.splitField(k)[0]);
2140
+ }
2141
+ }
2142
+ };
2143
+ branches.forEach(collectAliases);
2144
+ return aliases;
2145
+ }
2146
+ /**
2147
+ * An entity filter's `$or` that cannot be distributed is applied intact to the `on` clause of the
2148
+ * outermost join common to all targeted joins, nesting the targeted joins under it so the clause
2149
+ * can reference their aliases.
2150
+ */
2151
+ mergeFilterOrCondition(branches, joins) {
2152
+ const aliases = this.getOrBranchAliases(branches);
2153
+ const chainOf = (join) => {
2154
+ const parent = joins.find(j => j.alias === join.ownerAlias);
2155
+ return parent ? [join, ...chainOf(parent)] : [join];
2156
+ };
2157
+ // chains go from each targeted join up to its root, so the first join present in all of them is the outermost common one
2158
+ const chains = joins.filter(j => aliases.has(j.alias)).map(chainOf);
2159
+ const anchor = chains[0]?.find(a => chains.every(chain => chain.includes(a)));
2160
+ /* v8 ignore next 3 */
2161
+ if (!anchor) {
2162
+ return;
2163
+ }
2164
+ for (const chain of chains) {
2165
+ // nest the chain below the anchor, so the anchor's `on` clause can reference the nested aliases
2166
+ for (let i = 0; chain[i] !== anchor; i++) {
2167
+ const nested = (chain[i + 1].nested ??= new Set());
2168
+ if (!nested.has(chain[i])) {
2169
+ chain[i].type = chain[i].type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
2170
+ nested.add(chain[i]);
2171
+ }
2071
2172
  }
2072
2173
  }
2174
+ anchor.cond = anchor.cond.$or ? { $and: [anchor.cond, { $or: branches }] } : { ...anchor.cond, $or: branches };
2073
2175
  }
2074
2176
  /**
2075
2177
  * When adding an inner join on a left joined relation, we need to nest them,
@@ -14,6 +14,11 @@ export declare class QueryBuilderHelper {
14
14
  * Returns the main alias if not a TPT property or if the property belongs to the main entity.
15
15
  */
16
16
  getTPTAliasForProperty(propName: string, defaultAlias: string): string;
17
+ /**
18
+ * Replaces `ALIAS_REPLACEMENT` placeholders, resolving column-qualified ones via
19
+ * `getTPTAliasForProperty()` so TPT inherited columns map to their owning table.
20
+ */
21
+ replaceAliases(sql: string, alias?: string): string;
17
22
  mapper(field: string | Raw | RawQueryFragmentSymbol, type?: QueryType): string;
18
23
  mapper(field: string | Raw | RawQueryFragmentSymbol, type?: QueryType, value?: any, alias?: string | null, schema?: string): string;
19
24
  processData(data: Dictionary, convertCustomTypes: boolean, multi?: boolean): any;
@@ -1,5 +1,7 @@
1
1
  import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, JsonType, inspect, isRaw, LockMode, OptimisticLockError, QueryOperator, QueryOrderNumeric, raw, Raw, QueryHelper, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';
2
2
  import { EMBEDDABLE_ARRAY_OPS, JoinType, QueryType } from './enums.js';
3
+ /** Captures the column name that follows the alias placeholder. */
4
+ const QUALIFYING_ALIAS_RE = new RegExp(ALIAS_REPLACEMENT_RE + '(?=["\'`\\]]*\\.["\'`\\[]?([\\w$]+))', 'g');
3
5
  /**
4
6
  * @internal
5
7
  */
@@ -49,6 +51,15 @@ export class QueryBuilderHelper {
49
51
  // Property not found in hierarchy, return default alias
50
52
  return defaultAlias;
51
53
  }
54
+ /**
55
+ * Replaces `ALIAS_REPLACEMENT` placeholders, resolving column-qualified ones via
56
+ * `getTPTAliasForProperty()` so TPT inherited columns map to their owning table.
57
+ */
58
+ replaceAliases(sql, alias = this.#alias) {
59
+ return sql
60
+ .replace(QUALIFYING_ALIAS_RE, (_, column) => this.getTPTAliasForProperty(column, alias))
61
+ .replaceAll(ALIAS_REPLACEMENT, alias);
62
+ }
52
63
  mapper(field, type = QueryType.SELECT, value, alias, schema) {
53
64
  if (isRaw(field)) {
54
65
  return raw(field.sql, field.params);
@@ -97,7 +108,6 @@ export class QueryBuilderHelper {
97
108
  // Only apply TPT resolution when `a` is an actual table alias (in aliasMap),
98
109
  // not when it's an embedded property name like 'profile1.identity.links'
99
110
  const isTableAlias = !!this.#aliasMap[a];
100
- const baseAlias = isTableAlias ? a : this.#alias;
101
111
  const resolvedAlias = isTableAlias ? this.getTPTAliasForProperty(prop?.name ?? f, a) : this.#alias;
102
112
  const aliasPrefix = isTableNameAliasRequired ? resolvedAlias + '.' : '';
103
113
  const fkIdx2 = prop?.fieldNames.findIndex(name => name === f) ?? -1;
@@ -249,7 +259,13 @@ export class QueryBuilderHelper {
249
259
  }[join.type] ?? join.type;
250
260
  const conditions = [];
251
261
  const params = [];
252
- schema = join.schema === '*' ? schema : (join.schema ?? schemaOverride);
262
+ if (join.prop.name === '__subquery__') {
263
+ // bare table/CTE references (`sql.ref()` joins) keep their literal name, only an explicit schema applies
264
+ schema = join.schema === '*' ? undefined : join.schema;
265
+ }
266
+ else {
267
+ schema = join.schema === '*' ? schema : (join.schema ?? schemaOverride);
268
+ }
253
269
  if (schema && schema !== this.#platform.getDefaultSchemaName()) {
254
270
  table = `${schema}.${table}`;
255
271
  }
@@ -465,7 +481,7 @@ export class QueryBuilderHelper {
465
481
  const op = cond[key] === null ? 'is' : '=';
466
482
  if (Raw.isKnownFragmentSymbol(key)) {
467
483
  const raw = Raw.getKnownFragment(key);
468
- const sql = raw.sql.replaceAll(ALIAS_REPLACEMENT, this.#alias);
484
+ const sql = this.replaceAliases(raw.sql);
469
485
  const value = Utils.asArray(cond[key]);
470
486
  params.push(...raw.params);
471
487
  if (value.length > 0) {
@@ -517,6 +533,11 @@ export class QueryBuilderHelper {
517
533
  const replacement = this.getOperatorReplacement(op, value);
518
534
  const rawField = Raw.isKnownFragmentSymbol(key);
519
535
  const fields = rawField ? [key] : Utils.splitPrimaryKeys(key);
536
+ // a raw key's arity is opaque, so the row-value form is detected from the payload instead
537
+ const rowValues = rawField &&
538
+ ['$in', '$nin'].includes(op) &&
539
+ Array.isArray(value[op]) &&
540
+ value[op].every((v) => Array.isArray(v));
520
541
  if (fields.length > 1 && Array.isArray(value[op])) {
521
542
  const singleTuple = !value[op].every((v) => Array.isArray(v));
522
543
  if (!this.#platform.allowsComparingTuples()) {
@@ -576,7 +597,7 @@ export class QueryBuilderHelper {
576
597
  if (opValueIsRaw || typeof opValue?.toRaw === 'function') {
577
598
  const query = opValueIsRaw ? opValue : opValue.toRaw();
578
599
  const mappedKey = this.mapper(key, type, query, null);
579
- let sql = query.sql.replaceAll(ALIAS_REPLACEMENT, this.#alias);
600
+ let sql = this.replaceAliases(query.sql);
580
601
  if (['$in', '$nin'].includes(op)) {
581
602
  sql = `(${sql})`;
582
603
  }
@@ -585,15 +606,15 @@ export class QueryBuilderHelper {
585
606
  }
586
607
  else {
587
608
  const mappedKey = this.mapper(key, type, opValue, null);
588
- const val = this.getValueReplacement(fields, opValue, params, op, prop);
609
+ const val = this.getValueReplacement(fields, opValue, params, op, prop, rowValues);
589
610
  parts.push(`${this.#platform.quoteIdentifier(mappedKey)} ${replacement} ${val}`);
590
611
  }
591
612
  }
592
613
  return { sql: parts.join(' and '), params };
593
614
  }
594
- getValueReplacement(fields, value, params, key, prop) {
615
+ getValueReplacement(fields, value, params, key, prop, rowValues = false) {
595
616
  if (Array.isArray(value)) {
596
- if (fields.length > 1) {
617
+ if (fields.length > 1 || rowValues) {
597
618
  const tmp = [];
598
619
  for (const field of value) {
599
620
  tmp.push(`(${field.map(() => '?').join(', ')})`);
@@ -742,7 +763,7 @@ export class QueryBuilderHelper {
742
763
  }
743
764
  updateVersionProperty(qb, data) {
744
765
  const meta = this.#metadata.find(this.#entityName);
745
- if (!meta?.versionProperty || meta.versionProperty in data) {
766
+ if (!meta?.ownsVersionProperty() || meta.versionProperty in data) {
746
767
  return;
747
768
  }
748
769
  const versionProperty = meta.properties[meta.versionProperty];
@@ -963,13 +963,13 @@ export class SchemaComparator {
963
963
  }
964
964
  diffTrigger(from, to) {
965
965
  // Raw DDL expression cannot be meaningfully compared to introspected
966
- // trigger metadata, so skip diffing when the metadata side uses it.
967
- if (to.expression) {
966
+ // trigger metadata, so it needs special handling when either side uses it.
967
+ if (from.expression || to.expression) {
968
968
  // Both sides have expression — compare the raw DDL directly
969
- if (from.expression) {
969
+ if (from.expression && to.expression) {
970
970
  return this.diffExpression(from.expression, to.expression);
971
971
  }
972
- // Only metadata side has expression — the raw DDL cannot be compared to
972
+ // Only one side has expression — the raw DDL cannot be compared to
973
973
  // introspected metadata. Changes to the expression value won't be detected;
974
974
  // drop and recreate the trigger manually to apply expression changes.
975
975
  return false;
package/typings.d.ts CHANGED
@@ -277,6 +277,8 @@ export interface IQueryBuilder<T> {
277
277
  with(name: string, query: AnyQueryBuilder | NativeQueryBuilder | RawQueryFragment, options?: CteOptions): this;
278
278
  withRecursive(name: string, query: AnyQueryBuilder | NativeQueryBuilder | RawQueryFragment, options?: CteOptions): this;
279
279
  scheduleFilterCheck(path: string): void;
280
+ /** @internal */
281
+ ensureTPTJoins(): void;
280
282
  withSchema(schema: string): this;
281
283
  }
282
284
  export interface ICriteriaNodeProcessOptions {