@mikro-orm/sql 7.2.0-dev.9 → 7.2.0

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 (39) hide show
  1. package/AbstractSqlConnection.d.ts +14 -3
  2. package/AbstractSqlConnection.js +42 -4
  3. package/AbstractSqlDriver.d.ts +1 -8
  4. package/AbstractSqlDriver.js +63 -35
  5. package/AbstractSqlPlatform.d.ts +4 -2
  6. package/AbstractSqlPlatform.js +35 -1
  7. package/README.md +1 -0
  8. package/SqlEntityManager.d.ts +2 -2
  9. package/SqlEntityManager.js +4 -2
  10. package/dialects/mysql/BaseMySqlPlatform.d.ts +2 -0
  11. package/dialects/mysql/BaseMySqlPlatform.js +4 -0
  12. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +3 -1
  13. package/dialects/postgresql/BasePostgreSqlPlatform.js +35 -2
  14. package/dialects/postgresql/PostgreSqlExceptionConverter.js +8 -1
  15. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +18 -1
  16. package/dialects/postgresql/PostgreSqlSchemaHelper.js +153 -1
  17. package/dialects/sqlite/SqlitePlatform.d.ts +2 -0
  18. package/dialects/sqlite/SqlitePlatform.js +4 -0
  19. package/dialects/sqlite/SqliteSchemaHelper.js +1 -1
  20. package/package.json +3 -3
  21. package/plugin/transformer.d.ts +7 -1
  22. package/plugin/transformer.js +60 -1
  23. package/query/CriteriaNodeFactory.js +4 -0
  24. package/query/ObjectCriteriaNode.d.ts +1 -0
  25. package/query/ObjectCriteriaNode.js +30 -5
  26. package/query/QueryBuilder.d.ts +26 -3
  27. package/query/QueryBuilder.js +139 -23
  28. package/query/QueryBuilderHelper.d.ts +5 -0
  29. package/query/QueryBuilderHelper.js +33 -8
  30. package/schema/DatabaseSchema.d.ts +4 -0
  31. package/schema/DatabaseSchema.js +107 -1
  32. package/schema/DatabaseTable.d.ts +13 -1
  33. package/schema/DatabaseTable.js +50 -1
  34. package/schema/SchemaComparator.d.ts +2 -0
  35. package/schema/SchemaComparator.js +94 -4
  36. package/schema/SchemaHelper.d.ts +6 -0
  37. package/schema/SchemaHelper.js +15 -0
  38. package/schema/SqlSchemaGenerator.js +8 -0
  39. package/typings.d.ts +18 -0
@@ -1,7 +1,7 @@
1
1
  import { type Dictionary, type Transaction } from '@mikro-orm/core';
2
2
  import { SchemaHelper } from '../../schema/SchemaHelper.js';
3
3
  import type { AbstractSqlConnection } from '../../AbstractSqlConnection.js';
4
- import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, TablePartitioning, SqlTriggerDef, SqlRoutineDef } from '../../typings.js';
4
+ import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, TablePartitioning, SqlPolicyDef, SqlTriggerDef, SqlRoutineDef } from '../../typings.js';
5
5
  import type { DatabaseSchema } from '../../schema/DatabaseSchema.js';
6
6
  import type { DatabaseTable } from '../../schema/DatabaseTable.js';
7
7
  export declare class PostgreSqlSchemaHelper extends SchemaHelper {
@@ -71,6 +71,17 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
71
71
  dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
72
72
  /** Flattens `;\n` inside the dollar-quoted blocks of a raw DDL expression, which are not statement boundaries. */
73
73
  private flattenDollarQuotedBodies;
74
+ getRlsCreateSQL(table: DatabaseTable): string[];
75
+ getRlsDropSQL(diff: TableDifference, safe?: boolean): string[];
76
+ getRlsAlterSQL(diff: TableDifference, safe?: boolean): string[];
77
+ /**
78
+ * Quotes a policy or role name as a single identifier. Unlike `quote()`/`platform.quoteIdentifier`, which treat
79
+ * a dot as a schema qualifier, these names are never schema-qualified, so `my.role` must render as `"my.role"`.
80
+ */
81
+ private quoteUnqualified;
82
+ private createPolicy;
83
+ private dropPolicy;
84
+ private formatPolicyRoles;
74
85
  createRoutine(routine: SqlRoutineDef): string;
75
86
  dropRoutine(routine: SqlRoutineDef): string;
76
87
  getAllRoutines(connection: AbstractSqlConnection, schemas?: string[]): Promise<SqlRoutineDef[]>;
@@ -87,6 +98,12 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
87
98
  getDatabaseCollation(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string | undefined>;
88
99
  getAllTriggers(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>): Promise<Dictionary<SqlTriggerDef[]>>;
89
100
  private getTriggersSQL;
101
+ getAllPolicies(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, ctx?: Transaction): Promise<Dictionary<{
102
+ policies: SqlPolicyDef[];
103
+ enabled: boolean;
104
+ forced: boolean;
105
+ }>>;
106
+ private parsePgRoles;
90
107
  getAllForeignKeys(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, ctx?: Transaction): Promise<Dictionary<Dictionary<ForeignKey>>>;
91
108
  getNativeEnumDefinitions(connection: AbstractSqlConnection, schemas: string[], ctx?: Transaction): Promise<Dictionary<{
92
109
  name: string;
@@ -162,6 +162,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
162
162
  const fks = await this.getAllForeignKeys(connection, tablesBySchema, ctx);
163
163
  const partitionings = await this.getPartitions(connection, tablesBySchema, ctx);
164
164
  const triggers = await this.getAllTriggers(connection, tablesBySchema);
165
+ const policies = await this.getAllPolicies(connection, tablesBySchema, ctx);
165
166
  const dbCollation = await this.getDatabaseCollation(connection, ctx);
166
167
  for (const t of tables) {
167
168
  const key = this.getTableKey(t);
@@ -175,6 +176,12 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
175
176
  if (triggers[key]) {
176
177
  table.setTriggers(triggers[key]);
177
178
  }
179
+ const rls = policies[key];
180
+ if (rls) {
181
+ table.setPolicies(rls.policies);
182
+ table.rlsEnabled = rls.enabled;
183
+ table.rlsForced = rls.forced;
184
+ }
178
185
  table.setPartitioning(partitionings[key]);
179
186
  }
180
187
  }
@@ -494,7 +501,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
494
501
  // SchemaHelper.createCheck).
495
502
  const m = /^check \(\((.*)\)\)$/is.exec(check.expression);
496
503
  const single = m ? null : /^check \((.*)\)$/is.exec(check.expression);
497
- const def = m ? m[1].replace(/\(([^()]*)\)::\w+/g, '$1') : single ? single[1] : check.expression;
504
+ const def = m ? m[1].replace(/\(([^()]*)\)::\w+(?:\[\])?/g, '$1') : single ? single[1] : check.expression;
498
505
  ret[key].push({
499
506
  name: check.name,
500
507
  columnName: check.column_name,
@@ -540,6 +547,83 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
540
547
  })
541
548
  .join('');
542
549
  }
550
+ getRlsCreateSQL(table) {
551
+ const ret = [];
552
+ if (table.rlsEnabled) {
553
+ ret.push(`alter table ${table.getQuotedName()} enable row level security`);
554
+ }
555
+ if (table.rlsForced) {
556
+ ret.push(`alter table ${table.getQuotedName()} force row level security`);
557
+ }
558
+ for (const policy of table.getPolicies()) {
559
+ ret.push(this.createPolicy(table, policy));
560
+ }
561
+ return ret;
562
+ }
563
+ getRlsDropSQL(diff, safe) {
564
+ // a policy expression holds a dependency on the columns it references, so removed policies (including the
565
+ // old version of changed ones) must be dropped before the column drops emitted later in the diff;
566
+ // in safe mode only recreated policies (present in both removed + added) are dropped, so a policy that is
567
+ // merely removed is left untouched like removed triggers/columns
568
+ return Object.values(diff.removedPolicies)
569
+ .filter(policy => !safe || policy.name in diff.addedPolicies)
570
+ .map(policy => this.dropPolicy(diff.toTable, policy));
571
+ }
572
+ getRlsAlterSQL(diff, safe) {
573
+ const ret = [];
574
+ const table = diff.toTable;
575
+ if (diff.changedRlsEnabled === true) {
576
+ ret.push(`alter table ${table.getQuotedName()} enable row level security`);
577
+ }
578
+ if (diff.changedRlsForced === true) {
579
+ ret.push(`alter table ${table.getQuotedName()} force row level security`);
580
+ }
581
+ else if (!safe && diff.changedRlsForced === false) {
582
+ ret.push(`alter table ${table.getQuotedName()} no force row level security`);
583
+ }
584
+ for (const policy of Object.values(diff.addedPolicies)) {
585
+ ret.push(this.createPolicy(table, policy));
586
+ }
587
+ // disable only after its policies are gone (removed ones were dropped via `getRlsDropSQL`); skipped in
588
+ // safe mode so a safe run never lifts row level security off an existing table
589
+ if (!safe && diff.changedRlsEnabled === false) {
590
+ ret.push(`alter table ${table.getQuotedName()} disable row level security`);
591
+ }
592
+ return ret;
593
+ }
594
+ /**
595
+ * Quotes a policy or role name as a single identifier. Unlike `quote()`/`platform.quoteIdentifier`, which treat
596
+ * a dot as a schema qualifier, these names are never schema-qualified, so `my.role` must render as `"my.role"`.
597
+ */
598
+ quoteUnqualified(name) {
599
+ return `"${name.replaceAll('"', '""')}"`;
600
+ }
601
+ createPolicy(table, policy) {
602
+ const parts = [`create policy ${this.quoteUnqualified(policy.name)} on ${table.getQuotedName()}`];
603
+ if (policy.type === 'restrictive') {
604
+ parts.push('as restrictive');
605
+ }
606
+ if (policy.command !== 'all') {
607
+ parts.push(`for ${policy.command}`);
608
+ }
609
+ if (policy.roles.length > 0) {
610
+ parts.push(`to ${this.formatPolicyRoles(policy.roles)}`);
611
+ }
612
+ if (policy.using) {
613
+ parts.push(`using (${policy.using})`);
614
+ }
615
+ if (policy.check) {
616
+ parts.push(`with check (${policy.check})`);
617
+ }
618
+ return parts.join(' ');
619
+ }
620
+ dropPolicy(table, policy) {
621
+ return `drop policy ${this.quoteUnqualified(policy.name)} on ${table.getQuotedName()}`;
622
+ }
623
+ // `public` is a keyword and must stay unquoted; other roles are quoted like any identifier
624
+ formatPolicyRoles(roles) {
625
+ return roles.map(role => (role === 'public' ? 'public' : this.quoteUnqualified(role))).join(', ');
626
+ }
543
627
  createRoutine(routine) {
544
628
  if (routine.expression) {
545
629
  return this.flattenDollarQuotedBodies(routine.expression);
@@ -732,6 +816,56 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
732
816
  where (${conditions.join(' or ')})
733
817
  order by t.trigger_name, t.event_manipulation`;
734
818
  }
819
+ async getAllPolicies(connection, tablesBySchemas, ctx) {
820
+ const conditionsFor = (schemaColumn, tableColumn) => [...tablesBySchemas.entries()].map(([schema, tables]) => {
821
+ const names = tables.map(t => this.platform.quoteValue(t.table_name)).join(', ');
822
+ const schemaName = this.platform.quoteValue(schema ?? this.platform.getDefaultSchemaName());
823
+ return `(${schemaColumn} = ${schemaName} and ${tableColumn} in (${names}))`;
824
+ });
825
+ const flagConditions = conditionsFor('ns.nspname', 'cls.relname');
826
+ const policyConditions = conditionsFor('schemaname', 'tablename');
827
+ // pg_class carries the enable/force flags (a table can enable RLS with zero policies)
828
+ const flagRows = await connection.execute(`select cls.relname as table_name, ns.nspname as schema_name, cls.relrowsecurity as enabled, cls.relforcerowsecurity as forced
829
+ from pg_class cls
830
+ join pg_namespace ns on ns.oid = cls.relnamespace
831
+ where (${flagConditions.join(' or ')})`, [], 'all', ctx);
832
+ const policyRows = await connection.execute(`select tablename as table_name, schemaname as schema_name, policyname as name, permissive, roles, cmd, qual, with_check
833
+ from pg_policies
834
+ where (${policyConditions.join(' or ')})
835
+ order by policyname`, [], 'all', ctx);
836
+ const policiesByTable = {};
837
+ for (const row of policyRows) {
838
+ const key = this.getTableKey(row);
839
+ (policiesByTable[key] ??= []).push({
840
+ name: row.name,
841
+ command: row.cmd.toLowerCase(),
842
+ type: row.permissive === 'PERMISSIVE' ? 'permissive' : 'restrictive',
843
+ roles: this.parsePgRoles(row.roles),
844
+ using: row.qual ?? undefined,
845
+ check: row.with_check ?? undefined,
846
+ });
847
+ }
848
+ const ret = {};
849
+ for (const row of flagRows) {
850
+ const key = this.getTableKey(row);
851
+ ret[key] = { policies: policiesByTable[key] ?? [], enabled: row.enabled, forced: row.forced };
852
+ }
853
+ return ret;
854
+ }
855
+ // node-postgres returns `pg_policies.roles` as an unparsed array literal (`{public}`), pglite as an array
856
+ parsePgRoles(value) {
857
+ if (Array.isArray(value)) {
858
+ return value;
859
+ }
860
+ // tokenize the array literal instead of splitting on commas — quoted role names can contain
861
+ // commas, and quoted elements escape `"` and `\` with a backslash
862
+ const roles = [];
863
+ const re = /"((?:[^"\\]|\\.)*)"|[^,]+/g;
864
+ for (const match of value.replace(/^\{|\}$/g, '').matchAll(re)) {
865
+ roles.push(match[1] != null ? match[1].replace(/\\(.)/g, '$1') : match[0]);
866
+ }
867
+ return roles;
868
+ }
735
869
  async getAllForeignKeys(connection, tablesBySchemas, ctx) {
736
870
  const sql = `select nsp1.nspname schema_name, cls1.relname table_name, nsp2.nspname referenced_schema_name,
737
871
  cls2.relname referenced_table_name, a.attname column_name, af.attname referenced_column_name, conname constraint_name,
@@ -966,6 +1100,24 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
966
1100
  for (const { table: localTable, foreignKey } of inboundForeignKeys) {
967
1101
  this.append(ret, this.createForeignKey(localTable, foreignKey));
968
1102
  }
1103
+ // re-enable row level security and recreate the policies (createTable skips them during a rebuild);
1104
+ // emitted after the data copy so `force row level security` can't block the owner's insert
1105
+ this.append(ret, this.getRlsCreateSQL(table));
1106
+ // with `ignorePolicies`, hand-written policies (and the RLS flags they imply) may exist only on the
1107
+ // introspected side — recreate them verbatim, or the rebuild would silently strip them
1108
+ if (this.options.ignorePolicies) {
1109
+ if (diff.fromTable.rlsEnabled && !table.rlsEnabled) {
1110
+ ret.push(`alter table ${table.getQuotedName()} enable row level security`);
1111
+ }
1112
+ if (diff.fromTable.rlsForced && !table.rlsForced) {
1113
+ ret.push(`alter table ${table.getQuotedName()} force row level security`);
1114
+ }
1115
+ for (const policy of diff.fromTable.getPolicies()) {
1116
+ if (!table.hasPolicy(policy.name)) {
1117
+ ret.push(this.createPolicy(table, policy));
1118
+ }
1119
+ }
1120
+ }
969
1121
  }
970
1122
  if (safe) {
971
1123
  ret.push(`-- safe mode: original tables kept in schema "${tmpSchema}"; drop that schema manually once the data is verified`);
@@ -6,6 +6,8 @@ import { SqliteExceptionConverter } from './SqliteExceptionConverter.js';
6
6
  export declare class SqlitePlatform extends AbstractSqlPlatform {
7
7
  protected readonly schemaHelper: SqliteSchemaHelper;
8
8
  protected readonly exceptionConverter: SqliteExceptionConverter;
9
+ /** sqlite treats null as the lowest value when no placement is requested. */
10
+ sortsNullsLowest(): boolean;
9
11
  /** @internal */
10
12
  createNativeQueryBuilder(): SqliteNativeQueryBuilder;
11
13
  usesDefaultKeyword(): boolean;
@@ -5,6 +5,10 @@ import { SqliteExceptionConverter } from './SqliteExceptionConverter.js';
5
5
  export class SqlitePlatform extends AbstractSqlPlatform {
6
6
  schemaHelper = new SqliteSchemaHelper(this);
7
7
  exceptionConverter = new SqliteExceptionConverter();
8
+ /** sqlite treats null as the lowest value when no placement is requested. */
9
+ sortsNullsLowest() {
10
+ return true;
11
+ }
8
12
  /** @internal */
9
13
  createNativeQueryBuilder() {
10
14
  return new SqliteNativeQueryBuilder(this);
@@ -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.9",
3
+ "version": "7.2.0",
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.2.0"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.2.0-dev.9"
56
+ "@mikro-orm/core": "7.2.0"
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
  }
@@ -13,6 +13,7 @@ export declare class ObjectCriteriaNode<T extends object> extends CriteriaNode<T
13
13
  private inlineArrayChildPayload;
14
14
  private inlineChildPayload;
15
15
  private inlineCondition;
16
+ private isCollectionOperator;
16
17
  private shouldAutoJoin;
17
18
  private autoJoin;
18
19
  private isPrefixed;
@@ -1,7 +1,7 @@
1
1
  import { ALIAS_REPLACEMENT, GroupOperator, QueryFlag, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core';
2
2
  import { CriteriaNode } from './CriteriaNode.js';
3
3
  import { JoinType, QueryType } from './enums.js';
4
- const COLLECTION_OPERATORS = ['$some', '$none', '$every', '$size'];
4
+ const COLLECTION_OPERATORS = ['$some', '$none', '$every', '$size', '$all'];
5
5
  /**
6
6
  * @internal
7
7
  */
@@ -17,7 +17,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
17
17
  alias = nestedAlias;
18
18
  }
19
19
  if (this.shouldAutoJoin(qb, nestedAlias)) {
20
- if (keys.some(k => COLLECTION_OPERATORS.includes(k))) {
20
+ if (keys.some(k => this.isCollectionOperator(k))) {
21
21
  if (![ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(this.prop.kind)) {
22
22
  // ignore collection operators when used on a non-relational property - this can happen when they get into
23
23
  // populateWhere via `infer` on m:n properties with select-in strategy
@@ -34,11 +34,24 @@ export class ObjectCriteriaNode extends CriteriaNode {
34
34
  const primaryKeys = parentMeta.primaryKeys.map(pk => {
35
35
  return [QueryType.SELECT, QueryType.COUNT].includes(qb.type) ? `${knownKey ? alias : ownerAlias}.${pk}` : pk;
36
36
  });
37
+ const conditions = [];
38
+ let matchNothing = false;
37
39
  for (const key of keys) {
38
40
  if (typeof key !== 'string' || !COLLECTION_OPERATORS.includes(key)) {
39
41
  throw new Error('Mixing collection operators with other filters is not allowed.');
40
42
  }
41
43
  const payload = this.payload[key].unwrap();
44
+ // `$all` requires every listed item to be present, which is an intersection of `$some` conditions
45
+ if (key === '$all') {
46
+ // an empty `$all` matches nothing, same as in mongo
47
+ matchNothing ||= payload.length === 0;
48
+ conditions.push(...payload.map(item => ['$some', item]));
49
+ }
50
+ else {
51
+ conditions.push([key, payload]);
52
+ }
53
+ }
54
+ for (const [key, payload] of conditions) {
42
55
  // entities with a fixed schema must resolve the `from` table's own schema in the subquery,
43
56
  // otherwise a nested operator inherits the root entity's schema (GH #7894); for wildcard or
44
57
  // schema-less entities the schema is resolved dynamically and needs to be carried over
@@ -70,6 +83,9 @@ export class ObjectCriteriaNode extends CriteriaNode {
70
83
  [Utils.getPrimaryKeyHash(primaryKeys)]: { [op]: sub.getNativeQuery().toRaw() },
71
84
  });
72
85
  }
86
+ if (matchNothing) {
87
+ $and.push({ [Utils.getPrimaryKeyHash(primaryKeys)]: { $in: [] } });
88
+ }
73
89
  if ($and.length === 1) {
74
90
  return $and[0];
75
91
  }
@@ -113,7 +129,8 @@ export class ObjectCriteriaNode extends CriteriaNode {
113
129
  }
114
130
  else if (isRawField) {
115
131
  const rawField = RawQueryFragment.getKnownFragment(field);
116
- o[raw(rawField.sql.replaceAll(ALIAS_REPLACEMENT, alias), rawField.params)] = payload;
132
+ qb.ensureTPTJoins();
133
+ o[raw(qb.helper.replaceAliases(rawField.sql, alias), rawField.params)] = payload;
117
134
  }
118
135
  else if (!childNode.validate && !childNode.prop && !field.includes('.') && !operator) {
119
136
  // wrap unknown fields in raw() to prevent alias prefixing (e.g. raw SQL aliases in HAVING)
@@ -153,7 +170,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
153
170
  alias = nestedAlias;
154
171
  }
155
172
  if (this.shouldAutoJoin(qb, nestedAlias)) {
156
- return !keys.some(k => COLLECTION_OPERATORS.includes(k));
173
+ return !keys.some(k => this.isCollectionOperator(k));
157
174
  }
158
175
  return keys.some(field => {
159
176
  const childNode = this.payload[field];
@@ -237,6 +254,14 @@ export class ObjectCriteriaNode extends CriteriaNode {
237
254
  delete o[key];
238
255
  o.$and = $and;
239
256
  }
257
+ isCollectionOperator(key) {
258
+ if (typeof key !== 'string' || !COLLECTION_OPERATORS.includes(key)) {
259
+ return false;
260
+ }
261
+ // `$all` is primarily a mongo array operator, in SQL it is supported only on collections
262
+ return (key !== '$all' ||
263
+ (!!this.prop && [ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(this.prop.kind)));
264
+ }
240
265
  shouldAutoJoin(qb, nestedAlias) {
241
266
  if (!this.prop || !this.parent) {
242
267
  return false;
@@ -245,7 +270,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
245
270
  if (keys.every(k => typeof k === 'string' && k.includes('.') && k.startsWith(`${qb.alias}.`))) {
246
271
  return false;
247
272
  }
248
- if (keys.some(k => COLLECTION_OPERATORS.includes(k))) {
273
+ if (keys.some(k => this.isCollectionOperator(k))) {
249
274
  return true;
250
275
  }
251
276
  const meta = this.metadata.find(this.entityName);
@@ -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.