@mikro-orm/sql 7.1.0-dev.9 → 7.1.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 (56) hide show
  1. package/AbstractSqlConnection.d.ts +1 -1
  2. package/AbstractSqlConnection.js +27 -6
  3. package/AbstractSqlDriver.d.ts +15 -1
  4. package/AbstractSqlDriver.js +143 -26
  5. package/AbstractSqlPlatform.d.ts +15 -3
  6. package/AbstractSqlPlatform.js +25 -7
  7. package/PivotCollectionPersister.d.ts +2 -2
  8. package/PivotCollectionPersister.js +6 -1
  9. package/README.md +2 -1
  10. package/SqlEntityManager.d.ts +44 -5
  11. package/SqlEntityManager.js +41 -6
  12. package/SqlMikroORM.d.ts +23 -0
  13. package/SqlMikroORM.js +23 -0
  14. package/dialects/mysql/BaseMySqlPlatform.d.ts +3 -5
  15. package/dialects/mysql/BaseMySqlPlatform.js +6 -10
  16. package/dialects/mysql/MySqlSchemaHelper.d.ts +16 -3
  17. package/dialects/mysql/MySqlSchemaHelper.js +197 -49
  18. package/dialects/oracledb/OracleDialect.d.ts +1 -1
  19. package/dialects/oracledb/OracleDialect.js +2 -1
  20. package/dialects/postgresql/BasePostgreSqlEntityManager.d.ts +19 -0
  21. package/dialects/postgresql/BasePostgreSqlEntityManager.js +24 -0
  22. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +11 -5
  23. package/dialects/postgresql/BasePostgreSqlPlatform.js +75 -17
  24. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +31 -1
  25. package/dialects/postgresql/PostgreSqlSchemaHelper.js +269 -28
  26. package/dialects/postgresql/index.d.ts +2 -0
  27. package/dialects/postgresql/index.js +2 -0
  28. package/dialects/postgresql/typeOverrides.d.ts +14 -0
  29. package/dialects/postgresql/typeOverrides.js +12 -0
  30. package/dialects/sqlite/SqlitePlatform.d.ts +2 -1
  31. package/dialects/sqlite/SqlitePlatform.js +4 -0
  32. package/dialects/sqlite/SqliteSchemaHelper.d.ts +4 -1
  33. package/dialects/sqlite/SqliteSchemaHelper.js +49 -19
  34. package/index.d.ts +2 -0
  35. package/index.js +2 -0
  36. package/package.json +4 -4
  37. package/plugin/transformer.d.ts +11 -3
  38. package/plugin/transformer.js +138 -29
  39. package/query/CriteriaNode.d.ts +1 -1
  40. package/query/CriteriaNode.js +2 -2
  41. package/query/ObjectCriteriaNode.js +1 -1
  42. package/query/QueryBuilder.d.ts +42 -1
  43. package/query/QueryBuilder.js +78 -7
  44. package/schema/DatabaseSchema.d.ts +29 -2
  45. package/schema/DatabaseSchema.js +131 -4
  46. package/schema/DatabaseTable.d.ts +14 -1
  47. package/schema/DatabaseTable.js +165 -32
  48. package/schema/SchemaComparator.d.ts +18 -0
  49. package/schema/SchemaComparator.js +196 -1
  50. package/schema/SchemaHelper.d.ts +67 -1
  51. package/schema/SchemaHelper.js +255 -25
  52. package/schema/SqlSchemaGenerator.d.ts +2 -2
  53. package/schema/SqlSchemaGenerator.js +40 -10
  54. package/schema/partitioning.d.ts +13 -0
  55. package/schema/partitioning.js +326 -0
  56. package/typings.d.ts +59 -5
@@ -60,7 +60,7 @@ export declare abstract class AbstractSqlConnection extends Connection {
60
60
  /** Executes a SQL query and returns the result based on the method: `'all'` for rows, `'get'` for single row, `'run'` for affected count. */
61
61
  execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[], method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
62
62
  /** Executes a SQL query and returns an async iterable that yields results row by row. */
63
- stream<T extends EntityData<AnyEntity>>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[], ctx?: Transaction<Kysely<any>>, loggerContext?: LoggingOptions): AsyncIterableIterator<T>;
63
+ stream<T extends EntityData<AnyEntity>>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[], ctx?: Transaction<Kysely<any>>, loggerContext?: LoggingOptions, chunkSize?: number): AsyncIterableIterator<T>;
64
64
  /** @inheritDoc */
65
65
  executeDump(dump: string): Promise<void>;
66
66
  protected getSql(query: string, formatted: string, context?: LogContext): string;
@@ -1,6 +1,23 @@
1
1
  import { CompiledQuery, Kysely } from 'kysely';
2
2
  import { Connection, EventType, isRaw, Utils, } from '@mikro-orm/core';
3
3
  import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
4
+ /**
5
+ * Pulls cancellation controls out of a `loggerContext` payload, returning the abort options
6
+ * and a sanitized context that no longer carries them. The QueryBuilder/EM stash
7
+ * `signal`/`inflightQueryAbortStrategy` on `loggerContext` to avoid widening the public
8
+ * connection API; stripping them prevents leakage into user `Logger.logQuery` payloads.
9
+ */
10
+ function extractAbortOptions(loggerContext) {
11
+ const ctx = loggerContext;
12
+ if (ctx?.signal == null && ctx?.inflightQueryAbortStrategy == null) {
13
+ return { loggerContext };
14
+ }
15
+ const { signal, inflightQueryAbortStrategy, ...rest } = ctx;
16
+ return {
17
+ abort: { signal, inflightQueryAbortStrategy },
18
+ loggerContext: rest,
19
+ };
20
+ }
4
21
  /** Base class for SQL database connections, built on top of Kysely. */
5
22
  export class AbstractSqlConnection extends Connection {
6
23
  #client;
@@ -183,17 +200,19 @@ export class AbstractSqlConnection extends Connection {
183
200
  await this.ensureConnection();
184
201
  const q = this.prepareQuery(query, params);
185
202
  const sql = this.getSql(q.query, q.formatted, loggerContext);
203
+ const { abort, loggerContext: cleanCtx } = extractAbortOptions(loggerContext);
186
204
  return this.executeQuery(sql, async () => {
187
205
  const compiled = CompiledQuery.raw(q.formatted);
188
- const res = await (ctx ?? this.#client).executeQuery(compiled);
206
+ const res = await (ctx ?? this.#client).executeQuery(compiled, abort);
189
207
  return this.transformRawResult(res, method);
190
- }, { ...q, ...loggerContext });
208
+ }, { ...q, ...cleanCtx });
191
209
  }
192
210
  /** Executes a SQL query and returns an async iterable that yields results row by row. */
193
- async *stream(query, params = [], ctx, loggerContext) {
211
+ async *stream(query, params = [], ctx, loggerContext, chunkSize) {
194
212
  await this.ensureConnection();
195
213
  const q = this.prepareQuery(query, params);
196
214
  const sql = this.getSql(q.query, q.formatted, loggerContext);
215
+ const { abort, loggerContext: cleanCtx } = extractAbortOptions(loggerContext);
197
216
  // construct the compiled query manually with `kind: 'SelectQueryNode'` to avoid sqlite validation for select queries when streaming
198
217
  const compiled = {
199
218
  query: {
@@ -203,11 +222,13 @@ export class AbstractSqlConnection extends Connection {
203
222
  parameters: [],
204
223
  };
205
224
  try {
206
- const res = (ctx ?? this.getClient()).getExecutor().stream(compiled, 1);
225
+ const res = (ctx ?? this.getClient())
226
+ .getExecutor()
227
+ .stream(compiled, chunkSize ?? 100, abort ? { signal: abort.signal } : undefined);
207
228
  this.logQuery(sql, {
208
229
  sql,
209
230
  params,
210
- ...loggerContext,
231
+ ...cleanCtx,
211
232
  affected: Utils.isPlainObject(res) ? res.affectedRows : undefined,
212
233
  });
213
234
  for await (const items of res) {
@@ -217,7 +238,7 @@ export class AbstractSqlConnection extends Connection {
217
238
  }
218
239
  }
219
240
  catch (e) {
220
- this.logQuery(sql, { sql, params, ...loggerContext, level: 'error' });
241
+ this.logQuery(sql, { sql, params, ...cleanCtx, level: 'error' });
221
242
  throw e;
222
243
  }
223
244
  }
@@ -31,7 +31,7 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
31
31
  protected findFromVirtual<T extends object>(entityName: EntityName<T>, where: ObjectQuery<T>, options: FindOptions<T, any> | CountOptions<T, any>, type: QueryType): Promise<EntityData<T>[] | number>;
32
32
  protected streamFromVirtual<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T, any>): AsyncIterableIterator<EntityData<T>>;
33
33
  protected wrapVirtualExpressionInSubquery<T extends object>(meta: EntityMetadata<T>, expression: string, where: FilterQuery<T>, options: FindOptions<T, any>, type: QueryType): Promise<T[] | number>;
34
- protected wrapVirtualExpressionInSubqueryStream<T extends object>(meta: EntityMetadata<T>, expression: string, where: FilterQuery<T>, options: FindOptions<T, any, any, any>, type: QueryType.SELECT): AsyncIterableIterator<T>;
34
+ protected wrapVirtualExpressionInSubqueryStream<T extends object>(meta: EntityMetadata<T>, expression: string, where: FilterQuery<T>, options: StreamOptions<T, any, any, any>, type: QueryType.SELECT): AsyncIterableIterator<T>;
35
35
  /**
36
36
  * Virtual entities have no PKs, so to-many populate joins can't be deduplicated.
37
37
  * Force balanced strategy to load to-many relations via separate queries.
@@ -118,6 +118,13 @@ 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;
121
128
  /**
122
129
  * Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
123
130
  * @internal
@@ -147,6 +154,13 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
147
154
  mapPropToFieldNames<T extends object>(qb: AnyQueryBuilder<T>, prop: EntityProperty<T>, tableAlias: string, meta: EntityMetadata<T>, schema?: string, explicitFields?: readonly InternalField<T>[]): InternalField<T>[];
148
155
  /** @internal */
149
156
  createQueryBuilder<T extends object>(entityName: EntityName<T> | AnyQueryBuilder<T>, ctx?: Transaction, preferredConnectionType?: ConnectionType, convertCustomTypes?: boolean, loggerContext?: LoggingOptions, alias?: string, em?: SqlEntityManager): AnyQueryBuilder<T>;
157
+ /**
158
+ * Renders a `FilterQuery` predicate into a SQL fragment (without the `WHERE` keyword and
159
+ * without table-alias prefixes) suitable for inlining into a partial-index DDL statement.
160
+ * Used by `DatabaseTable.addIndex` when the user passes an object `where` on `@Index` /
161
+ * `@Unique`. Strings are returned unchanged.
162
+ */
163
+ renderPartialIndexWhere<T extends object>(entityName: EntityName<T>, where: string | FilterQuery<T>): string;
150
164
  protected resolveConnectionType(args: {
151
165
  ctx?: Transaction;
152
166
  connectionType?: ConnectionType;
@@ -3,6 +3,28 @@ import { QueryBuilder } from './query/QueryBuilder.js';
3
3
  import { JoinType, QueryType } from './query/enums.js';
4
4
  import { SqlEntityManager } from './SqlEntityManager.js';
5
5
  import { PivotCollectionPersister } from './PivotCollectionPersister.js';
6
+ /** Extracts cancellation controls from any options bag that extends `AbortQueryOptions`. */
7
+ function pickAbortOptions(options) {
8
+ if (!options || (options.signal == null && options.inflightQueryAbortStrategy == null)) {
9
+ return undefined;
10
+ }
11
+ return {
12
+ signal: options.signal,
13
+ inflightQueryAbortStrategy: options.inflightQueryAbortStrategy,
14
+ };
15
+ }
16
+ /**
17
+ * Returns a `loggerContext` payload that carries the abort fields alongside any existing
18
+ * context. The connection layer strips them before logging — this avoids widening the public
19
+ * `Connection.execute()` signature.
20
+ */
21
+ function withAbortContext(loggerContext, options) {
22
+ const abort = pickAbortOptions(options);
23
+ if (!abort) {
24
+ return loggerContext;
25
+ }
26
+ return { ...loggerContext, ...abort };
27
+ }
6
28
  /** Base class for SQL database drivers, implementing find/insert/update/delete using QueryBuilder. */
7
29
  export class AbstractSqlDriver extends DatabaseDriver {
8
30
  [EntityManagerType];
@@ -57,7 +79,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
57
79
  const populate = this.autoJoinOneToOneOwner(meta, options.populate, options.fields);
58
80
  const joinedProps = this.joinedProps(meta, populate, options);
59
81
  const schema = this.getSchemaName(meta, options);
60
- const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging, undefined, options.em).withSchema(schema);
82
+ const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging, undefined, options.em)
83
+ .withSchema(schema)
84
+ .cache(false);
85
+ qb.setAbortOptions(pickAbortOptions(options));
61
86
  const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
62
87
  const orderBy = this.buildOrderBy(qb, meta, populate, options);
63
88
  const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
@@ -95,6 +120,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
95
120
  if (options.em) {
96
121
  await qb.applyJoinedFilters(options.em, options.filters);
97
122
  }
123
+ if (options._partitionLimit) {
124
+ qb.setPartitionLimit(options._partitionLimit);
125
+ }
98
126
  return qb;
99
127
  }
100
128
  async find(entityName, where, options = {}) {
@@ -211,7 +239,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
211
239
  const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' ';
212
240
  native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
213
241
  const query = native.compile();
214
- const res = await this.execute(query.sql, query.params, 'all', options.ctx);
242
+ const res = await this.execute(query.sql, query.params, 'all', options.ctx, withAbortContext(options.loggerContext, options));
215
243
  if (type === QueryType.COUNT) {
216
244
  return res[0].count;
217
245
  }
@@ -228,7 +256,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
228
256
  native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
229
257
  const query = native.compile();
230
258
  const connectionType = this.resolveConnectionType({ ctx: options.ctx, connectionType: options.connectionType });
231
- const res = this.getConnection(connectionType).stream(query.sql, query.params, options.ctx, options.loggerContext);
259
+ const res = this.getConnection(connectionType).stream(query.sql, query.params, options.ctx, withAbortContext(options.loggerContext, options), options.chunkSize);
232
260
  for await (const row of res) {
233
261
  yield this.mapResult(row, meta);
234
262
  }
@@ -536,6 +564,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
536
564
  const joinedProps = this.joinedProps(meta, populate, options);
537
565
  const schema = this.getSchemaName(meta, options);
538
566
  const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false, options.logging);
567
+ qb.setAbortOptions(pickAbortOptions(options));
539
568
  const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
540
569
  if (meta && !Utils.isEmpty(populate)) {
541
570
  this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
@@ -561,6 +590,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
561
590
  const meta = this.metadata.get(entityName);
562
591
  const collections = this.extractManyToMany(meta, data);
563
592
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
593
+ qb.setAbortOptions(pickAbortOptions(options));
564
594
  const res = await this.rethrow(qb.insert(data).execute('run', false));
565
595
  res.row = res.row || {};
566
596
  let pk;
@@ -593,9 +623,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
593
623
  const props = this.getCloneableProps(meta);
594
624
  const mappedOverrides = this.mapCloneOverrides(overrides, meta, options);
595
625
  const { selectFields, insertColumns } = this.buildCloneFields(props, mappedOverrides, meta);
626
+ const abort = pickAbortOptions(options);
596
627
  const selectQb = this.createQueryBuilder(meta.class, options.ctx, 'read', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
597
628
  selectQb.select(selectFields).where(where);
629
+ selectQb.setAbortOptions(abort);
598
630
  const insertQb = this.createQueryBuilder(meta.class, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
631
+ insertQb.setAbortOptions(abort);
599
632
  return this.rethrow(insertQb
600
633
  .insertFrom(selectQb, { columns: insertColumns })
601
634
  .execute('run', false));
@@ -625,9 +658,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
625
658
  }
626
659
  }
627
660
  const sourceWhere = tableMeta === rootMeta ? where : (Utils.extractPK(where, tableMeta) ?? where);
661
+ const abort = pickAbortOptions(options);
628
662
  const selectQb = this.createQueryBuilder(tableMeta.class, options.ctx, 'read', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(tableMeta, options));
629
663
  selectQb.select(selectFields).where(sourceWhere);
664
+ selectQb.setAbortOptions(abort);
630
665
  const insertQb = this.createQueryBuilder(tableMeta.class, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(tableMeta, options));
666
+ insertQb.setAbortOptions(abort);
631
667
  const res = await this.rethrow(insertQb
632
668
  .insertFrom(selectQb, { columns: insertColumns })
633
669
  .execute('run', false));
@@ -802,13 +838,19 @@ export class AbstractSqlDriver extends DatabaseDriver {
802
838
  else {
803
839
  const field = prop.fieldNames[0];
804
840
  if (!duplicates.includes(field) || !usedDups.includes(field)) {
841
+ const rowValue = row[prop.name];
842
+ const rowValueIsRaw = isRaw(rowValue);
805
843
  if (prop.customType &&
806
844
  !prop.object &&
807
845
  'convertToDatabaseValueSQL' in prop.customType &&
808
- row[prop.name] != null &&
809
- !isRaw(row[prop.name])) {
846
+ rowValue != null &&
847
+ !rowValueIsRaw) {
810
848
  keys.push(prop.customType.convertToDatabaseValueSQL('?', this.platform));
811
849
  }
850
+ else if (rowValueIsRaw && /^\s*(?:with|select)\b/i.test(rowValue.sql)) {
851
+ // raw subqueries must be parenthesized when inlined as a VALUES position
852
+ keys.push('(?)');
853
+ }
812
854
  else {
813
855
  keys.push('?');
814
856
  }
@@ -835,7 +877,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
835
877
  if (transform) {
836
878
  sql = transform(sql);
837
879
  }
838
- const res = await this.execute(sql, params, 'run', options.ctx, options.loggerContext);
880
+ const res = await this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options));
839
881
  let pk;
840
882
  /* v8 ignore next */
841
883
  if (pks.length > 1) {
@@ -868,6 +910,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
868
910
  }
869
911
  if (Utils.hasObjectKeys(data)) {
870
912
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
913
+ qb.setAbortOptions(pickAbortOptions(options));
871
914
  if (options.upsert) {
872
915
  /* v8 ignore next */
873
916
  const uniqueFields = options.onConflictFields ??
@@ -913,6 +956,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
913
956
  ? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key))
914
957
  : meta.primaryKeys);
915
958
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
959
+ qb.setAbortOptions(pickAbortOptions(options));
916
960
  const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
917
961
  qb.insert(data)
918
962
  .onConflict(uniqueFields)
@@ -1058,7 +1102,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1058
1102
  if (transform) {
1059
1103
  sql = transform(sql, params);
1060
1104
  }
1061
- const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, options.loggerContext));
1105
+ const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options)));
1062
1106
  for (let i = 0; i < collections.length; i++) {
1063
1107
  await this.processManyToMany(meta, where[i], collections[i], false, options);
1064
1108
  }
@@ -1076,6 +1120,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1076
1120
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext)
1077
1121
  .delete(where)
1078
1122
  .withSchema(this.getSchemaName(meta, options));
1123
+ qb.setAbortOptions(pickAbortOptions(options));
1079
1124
  return this.rethrow(qb.execute('run', false));
1080
1125
  }
1081
1126
  /**
@@ -1146,6 +1191,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1146
1191
  if (coll.property.kind === ReferenceKind.ONE_TO_MANY) {
1147
1192
  const cols = coll.property.referencedColumnNames;
1148
1193
  const qb = this.createQueryBuilder(coll.property.targetMeta.class, options?.ctx, 'write').withSchema(this.getSchemaName(meta, options));
1194
+ qb.setAbortOptions(pickAbortOptions(options));
1149
1195
  if (coll.getSnapshot() === undefined) {
1150
1196
  if (coll.property.orphanRemoval) {
1151
1197
  const query = qb
@@ -1187,7 +1233,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1187
1233
  schema = this.config.get('schema');
1188
1234
  }
1189
1235
  const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
1190
- const persister = (groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext));
1236
+ const persister = (groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext, pickAbortOptions(options)));
1191
1237
  persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks, coll.isInitialized());
1192
1238
  }
1193
1239
  for (const persister of Utils.values(groups)) {
@@ -1224,7 +1270,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1224
1270
  const fields = pivotJoin
1225
1271
  ? [pivotProp1.name, pivotProp2.name]
1226
1272
  : [pivotProp1.name, pivotProp2.name, ...childFields];
1227
- const res = await this.find(pivotMeta.class, where, {
1273
+ const pivotFindOptions = {
1228
1274
  ctx,
1229
1275
  ...options,
1230
1276
  fields,
@@ -1240,10 +1286,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
1240
1286
  },
1241
1287
  ],
1242
1288
  populateWhere: undefined,
1243
- // @ts-ignore
1244
1289
  _populateWhere: 'infer',
1245
1290
  populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
1246
- });
1291
+ };
1292
+ if (pivotFindOptions._partitionLimit) {
1293
+ pivotFindOptions._partitionLimit.partitionBy = pivotProp2.name;
1294
+ }
1295
+ const res = await this.find(pivotMeta.class, where, pivotFindOptions);
1247
1296
  // Convert result FK values back to JS format so key hashing
1248
1297
  // in buildPivotResultMap is consistent with the owner keys.
1249
1298
  if (needsConversion) {
@@ -1684,6 +1733,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
1684
1733
  const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
1685
1734
  const schema = targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : targetMeta.schema;
1686
1735
  qb.addPolymorphicJoin(prop, targetMeta, options.parentTableAlias, tableAlias, JoinType.leftJoin, targetPath, schema);
1736
+ // For polymorphic targets that are TPT child entities, INNER JOIN parent tables so that
1737
+ // filter conditions referencing parent-table columns resolve to the correct alias. The
1738
+ // INNER JOINs get nested inside the polymorphic LEFT JOIN by processNestedJoins, which
1739
+ // keeps the resulting query valid for rows pointing to other polymorphic targets.
1740
+ if (targetMeta.inheritanceType === 'tpt' && targetMeta.tptParent) {
1741
+ this.addTPTParentJoinsForRelation(qb, targetMeta, tableAlias, targetPath);
1742
+ }
1687
1743
  // For polymorphic targets that are TPT base classes, also LEFT JOIN
1688
1744
  // all descendant tables so child-specific fields can be selected.
1689
1745
  if (targetMeta.inheritanceType === 'tpt' && targetMeta.tptChildren?.length && !ref) {
@@ -1732,17 +1788,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1732
1788
  qb.join(field, tableAlias, {}, joinType, path, schema);
1733
1789
  // For relations to TPT child entities, INNER JOIN parent tables (GH #7469)
1734
1790
  if (meta2.inheritanceType === 'tpt' && meta2.tptParent) {
1735
- let childAlias = tableAlias;
1736
- let childMeta = meta2;
1737
- while (childMeta.tptParent) {
1738
- const parentMeta = childMeta.tptParent;
1739
- const parentAlias = qb.getNextAlias(parentMeta.className);
1740
- qb.createAlias(parentMeta.class, parentAlias);
1741
- qb.state.tptAlias[`${tableAlias}:${parentMeta.className}`] = parentAlias;
1742
- qb.addPropertyJoin(childMeta.tptParentProp, childAlias, parentAlias, JoinType.innerJoin, `${path}.[tpt]${childMeta.className}`);
1743
- childAlias = parentAlias;
1744
- childMeta = parentMeta;
1745
- }
1791
+ this.addTPTParentJoinsForRelation(qb, meta2, tableAlias, path);
1746
1792
  }
1747
1793
  // For relations to TPT base classes, add LEFT JOINs for all child tables (polymorphic loading)
1748
1794
  if (meta2.inheritanceType === 'tpt' && meta2.tptChildren?.length && !ref) {
@@ -1790,6 +1836,25 @@ export class AbstractSqlDriver extends DatabaseDriver {
1790
1836
  }
1791
1837
  return fields;
1792
1838
  }
1839
+ /**
1840
+ * Walks the TPT inheritance chain of `leafMeta` and INNER JOINs each parent table.
1841
+ * Registers the parent aliases in `qb.state.tptAlias` so column resolution finds them
1842
+ * when filter conditions reference parent-table columns.
1843
+ * @internal
1844
+ */
1845
+ addTPTParentJoinsForRelation(qb, leafMeta, leafAlias, basePath) {
1846
+ let childAlias = leafAlias;
1847
+ let childMeta = leafMeta;
1848
+ while (childMeta.tptParent) {
1849
+ const parentMeta = childMeta.tptParent;
1850
+ const parentAlias = qb.getNextAlias(parentMeta.className);
1851
+ qb.createAlias(parentMeta.class, parentAlias);
1852
+ qb.state.tptAlias[`${leafAlias}:${parentMeta.className}`] = parentAlias;
1853
+ qb.addPropertyJoin(childMeta.tptParentProp, childAlias, parentAlias, JoinType.innerJoin, `${basePath}.[tpt]${childMeta.className}`);
1854
+ childAlias = parentAlias;
1855
+ childMeta = parentMeta;
1856
+ }
1857
+ }
1793
1858
  /**
1794
1859
  * Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
1795
1860
  * @internal
@@ -1901,18 +1966,19 @@ export class AbstractSqlDriver extends DatabaseDriver {
1901
1966
  });
1902
1967
  }
1903
1968
  const aliased = this.platform.quoteIdentifier(`${tableAlias}__${prop.fieldNames[0]}`);
1969
+ const sourceAlias = qb.helper.getTPTAliasForProperty(prop.name, tableAlias);
1904
1970
  if (prop.customTypes?.some(type => !!type?.convertToJSValueSQL)) {
1905
1971
  return prop.fieldNames.map((col, idx) => {
1906
1972
  if (!prop.customTypes[idx]?.convertToJSValueSQL) {
1907
1973
  return col;
1908
1974
  }
1909
- const prefixed = this.platform.quoteIdentifier(`${tableAlias}.${col}`);
1975
+ const prefixed = this.platform.quoteIdentifier(`${sourceAlias}.${col}`);
1910
1976
  const aliased = this.platform.quoteIdentifier(`${tableAlias}__${col}`);
1911
1977
  return raw(`${prop.customTypes[idx].convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`);
1912
1978
  });
1913
1979
  }
1914
1980
  if (prop.customType?.convertToJSValueSQL) {
1915
- const prefixed = this.platform.quoteIdentifier(`${tableAlias}.${prop.fieldNames[0]}`);
1981
+ const prefixed = this.platform.quoteIdentifier(`${sourceAlias}.${prop.fieldNames[0]}`);
1916
1982
  return [raw(`${prop.customType.convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`)];
1917
1983
  }
1918
1984
  if (prop.formula) {
@@ -1921,7 +1987,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
1921
1987
  const columns = meta.createColumnMappingObject(tableAlias);
1922
1988
  return [raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`)];
1923
1989
  }
1924
- const sourceAlias = qb.helper.getTPTAliasForProperty(prop.name, tableAlias);
1925
1990
  return prop.fieldNames.map(fieldName => {
1926
1991
  return raw('?? as ??', [`${sourceAlias}.${fieldName}`, `${tableAlias}__${fieldName}`]);
1927
1992
  });
@@ -1938,6 +2003,57 @@ export class AbstractSqlDriver extends DatabaseDriver {
1938
2003
  }
1939
2004
  return qb;
1940
2005
  }
2006
+ /**
2007
+ * Renders a `FilterQuery` predicate into a SQL fragment (without the `WHERE` keyword and
2008
+ * without table-alias prefixes) suitable for inlining into a partial-index DDL statement.
2009
+ * Used by `DatabaseTable.addIndex` when the user passes an object `where` on `@Index` /
2010
+ * `@Unique`. Strings are returned unchanged.
2011
+ */
2012
+ renderPartialIndexWhere(entityName, where) {
2013
+ if (typeof where === 'string') {
2014
+ return where;
2015
+ }
2016
+ const name = Utils.className(entityName);
2017
+ if (where == null || (Utils.isPlainObject(where) && Object.keys(where).length === 0)) {
2018
+ throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` is empty.`);
2019
+ }
2020
+ const alias = '__p';
2021
+ const qb = this.createQueryBuilder(entityName, undefined, undefined, undefined, undefined, alias);
2022
+ qb.where(where);
2023
+ const sql = qb.getFormattedQuery();
2024
+ // Relation traversal produces join clauses whose aliased identifiers can't be inlined
2025
+ // into a CREATE INDEX ... WHERE clause — reject with a clear error rather than emitting broken DDL.
2026
+ if (/\bjoin\b/i.test(sql.split(/\bwhere\b/i)[0])) {
2027
+ throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` may not traverse relations.`);
2028
+ }
2029
+ // Anchor at end-of-string only — the synthetic QB has no top-level order by / limit /
2030
+ // group by / having / offset, so any such keyword inside the captured predicate is
2031
+ // inside a subquery and must not terminate the match.
2032
+ const match = /\bwhere\s+([\s\S]+)$/i.exec(sql);
2033
+ if (!match) {
2034
+ throw new Error(`Failed to render partial-index predicate for entity '${name}': ${sql}`);
2035
+ }
2036
+ const quote = (s) => this.platform.quoteIdentifier(s);
2037
+ const aliasPrefix = new RegExp(`${quote(alias).replace(/[[\]]/g, '\\$&')}\\.`, 'g');
2038
+ const stripped = match[1].replace(aliasPrefix, '').trim();
2039
+ // Any qualified column reference remaining after the alias strip points at another table or
2040
+ // subquery and can't be inlined into a CREATE INDEX ... WHERE predicate. Covers both
2041
+ // QB-generated sub-aliases (quoted, e.g. `"e0"."col"`) and raw fragments with bare refs
2042
+ // (e.g. `raw('other_table.col = 1')`). String literals are erased first so dots inside
2043
+ // them (e.g. JSON path operands like `'$.path'`) don't trip the guard.
2044
+ // Both patterns use a `(?!\s*\()` lookahead so schema-qualified function calls
2045
+ // (`pg_catalog.lower(name)`, `"public".my_func(col)`) are accepted — only `<id>.<id>` not
2046
+ // followed by `(` is treated as a cross-table column reference.
2047
+ const withoutStrings = stripped.replace(/'(?:[^']|'')*'/g, "''");
2048
+ const quotedIdent = String.raw `(?:"(?:[^"]|"")+"|\`(?:[^\`]|\`\`)+\`|\[(?:[^\]]|\]\])+\])`;
2049
+ const anyIdent = `(?:${quotedIdent}|[A-Za-z_]\\w*)`;
2050
+ const quotedCrossRef = new RegExp(`${quotedIdent}\\s*\\.\\s*${anyIdent}(?!\\s*\\()`);
2051
+ const bareCrossRef = /\b[A-Za-z_]\w*\s*\.\s*[A-Za-z_]\w*\b(?!\s*\()/;
2052
+ if (quotedCrossRef.test(withoutStrings) || bareCrossRef.test(withoutStrings)) {
2053
+ throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` references another table or subquery which cannot be inlined into a CREATE INDEX ... WHERE clause.`);
2054
+ }
2055
+ return stripped;
2056
+ }
1941
2057
  resolveConnectionType(args) {
1942
2058
  if (args.ctx) {
1943
2059
  return 'write';
@@ -1964,7 +2080,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1964
2080
  for (const prop of meta.relations) {
1965
2081
  if (collections[prop.name]) {
1966
2082
  const pivotMeta = this.metadata.get(prop.pivotEntity);
1967
- const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext);
2083
+ const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext, pickAbortOptions(options));
1968
2084
  persister.enqueueUpdate(prop, collections[prop.name], clear, pks);
1969
2085
  await this.rethrow(persister.execute());
1970
2086
  }
@@ -1973,6 +2089,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1973
2089
  async lockPessimistic(entity, options) {
1974
2090
  const meta = helper(entity).__meta;
1975
2091
  const qb = this.createQueryBuilder(meta.class, options.ctx, undefined, undefined, options.logging).withSchema(options.schema ?? meta.schema);
2092
+ qb.setAbortOptions(pickAbortOptions(options));
1976
2093
  const cond = Utils.getPrimaryKeyCond(entity, meta.primaryKeys);
1977
2094
  qb.select(raw('1'))
1978
2095
  .where(cond)
@@ -30,7 +30,8 @@ export declare abstract class AbstractSqlPlatform extends Platform {
30
30
  getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string | RawQueryFragment;
31
31
  /**
32
32
  * Quotes a key for use inside a JSON path expression (e.g. `$.key`).
33
- * Simple alphanumeric keys are left unquoted; others are wrapped in double quotes.
33
+ * Simple alphanumeric keys are left unquoted; others are wrapped in double quotes
34
+ * with embedded `\` and `"` escaped per the JSON path string syntax.
34
35
  * @internal
35
36
  */
36
37
  quoteJsonKey(key: string): string;
@@ -50,8 +51,19 @@ export declare abstract class AbstractSqlPlatform extends Platform {
50
51
  * @internal
51
52
  */
52
53
  quoteCollation(collation: string): string;
53
- /** @internal */
54
- protected validateCollationName(collation: string): void;
54
+ /**
55
+ * PG ICU locale names include hyphens (`en-US-x-icu`) and libc locales include dots (`en_US.utf8`),
56
+ * so word-chars alone would reject valid real-world collations.
57
+ * @internal
58
+ */
59
+ validateCollationName(collation: string): void;
60
+ /**
61
+ * Whether collation names compare case-insensitively in this dialect. MySQL/MariaDB, MSSQL, and
62
+ * SQLite use case-insensitive collation identifiers; PostgreSQL stores them as case-sensitive
63
+ * names in `pg_collation` (e.g. `en-US-x-icu` is distinct from `EN-US-X-ICU`).
64
+ * @internal
65
+ */
66
+ caseInsensitiveCollationNames(): boolean;
55
67
  /** @internal */
56
68
  validateJsonPropertyName(name: string): void;
57
69
  /**
@@ -66,18 +66,23 @@ export class AbstractSqlPlatform extends Platform {
66
66
  }
67
67
  getSearchJsonPropertyKey(path, type, aliased, value) {
68
68
  const [a, ...b] = path;
69
+ const jsonPath = this.quoteValue(`$.${b.map(this.quoteJsonKey).join('.')}`);
69
70
  if (aliased) {
70
- return raw(alias => `json_extract(${this.quoteIdentifier(`${alias}.${a}`)}, '$.${b.map(this.quoteJsonKey).join('.')}')`);
71
+ return raw(alias => `json_extract(${this.quoteIdentifier(`${alias}.${a}`)}, ${jsonPath})`);
71
72
  }
72
- return raw(`json_extract(${this.quoteIdentifier(a)}, '$.${b.map(this.quoteJsonKey).join('.')}')`);
73
+ return raw(`json_extract(${this.quoteIdentifier(a)}, ${jsonPath})`);
73
74
  }
74
75
  /**
75
76
  * Quotes a key for use inside a JSON path expression (e.g. `$.key`).
76
- * Simple alphanumeric keys are left unquoted; others are wrapped in double quotes.
77
+ * Simple alphanumeric keys are left unquoted; others are wrapped in double quotes
78
+ * with embedded `\` and `"` escaped per the JSON path string syntax.
77
79
  * @internal
78
80
  */
79
81
  quoteJsonKey(key) {
80
- return /^[a-z]\w*$/i.exec(key) ? key : `"${key}"`;
82
+ if (/^[a-z]\w*$/i.test(key)) {
83
+ return key;
84
+ }
85
+ return `"${key.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
81
86
  }
82
87
  getJsonIndexDefinition(index) {
83
88
  return index.columnNames.map(column => {
@@ -123,12 +128,25 @@ export class AbstractSqlPlatform extends Platform {
123
128
  this.validateCollationName(collation);
124
129
  return this.quoteIdentifier(collation);
125
130
  }
126
- /** @internal */
131
+ /**
132
+ * PG ICU locale names include hyphens (`en-US-x-icu`) and libc locales include dots (`en_US.utf8`),
133
+ * so word-chars alone would reject valid real-world collations.
134
+ * @internal
135
+ */
127
136
  validateCollationName(collation) {
128
- if (!/^[\w]+$/.test(collation)) {
129
- throw new Error(`Invalid collation name: '${collation}'. Collation names must contain only word characters.`);
137
+ if (!/^[\w\-.]+$/.test(collation)) {
138
+ throw new Error(`Invalid collation name: '${collation}'. Collation names must contain only word characters, hyphens, and dots.`);
130
139
  }
131
140
  }
141
+ /**
142
+ * Whether collation names compare case-insensitively in this dialect. MySQL/MariaDB, MSSQL, and
143
+ * SQLite use case-insensitive collation identifiers; PostgreSQL stores them as case-sensitive
144
+ * names in `pg_collation` (e.g. `en-US-x-icu` is distinct from `EN-US-X-ICU`).
145
+ * @internal
146
+ */
147
+ caseInsensitiveCollationNames() {
148
+ return true;
149
+ }
132
150
  /** @internal */
133
151
  validateJsonPropertyName(name) {
134
152
  if (!AbstractSqlPlatform.#JSON_PROPERTY_NAME_RE.test(name)) {
@@ -1,8 +1,8 @@
1
- import { type Dictionary, type EntityMetadata, type EntityProperty, type Primary, type Transaction } from '@mikro-orm/core';
1
+ import { type AbortQueryOptions, type Dictionary, type EntityMetadata, type EntityProperty, type Primary, type Transaction } from '@mikro-orm/core';
2
2
  import { type AbstractSqlDriver } from './AbstractSqlDriver.js';
3
3
  export declare class PivotCollectionPersister<Entity extends object> {
4
4
  #private;
5
- constructor(meta: EntityMetadata<Entity>, driver: AbstractSqlDriver, ctx?: Transaction, schema?: string, loggerContext?: Dictionary);
5
+ constructor(meta: EntityMetadata<Entity>, driver: AbstractSqlDriver, ctx?: Transaction, schema?: string, loggerContext?: Dictionary, abort?: AbortQueryOptions);
6
6
  enqueueUpdate(prop: EntityProperty<Entity>, insertDiff: Primary<Entity>[][], deleteDiff: Primary<Entity>[][] | boolean, pks: Primary<Entity>[], isInitialized?: boolean): void;
7
7
  private enqueueInsert;
8
8
  private enqueueUpsert;
@@ -44,12 +44,14 @@ export class PivotCollectionPersister {
44
44
  #ctx;
45
45
  #schema;
46
46
  #loggerContext;
47
- constructor(meta, driver, ctx, schema, loggerContext) {
47
+ #abort;
48
+ constructor(meta, driver, ctx, schema, loggerContext, abort) {
48
49
  this.#meta = meta;
49
50
  this.#driver = driver;
50
51
  this.#ctx = ctx;
51
52
  this.#schema = schema;
52
53
  this.#loggerContext = loggerContext;
54
+ this.#abort = abort;
53
55
  this.#batchSize = this.#driver.config.get('batchSize');
54
56
  }
55
57
  enqueueUpdate(prop, insertDiff, deleteDiff, pks, isInitialized = true) {
@@ -153,6 +155,7 @@ export class PivotCollectionPersister {
153
155
  ctx: this.#ctx,
154
156
  schema: this.#schema,
155
157
  loggerContext: this.#loggerContext,
158
+ ...this.#abort,
156
159
  });
157
160
  }
158
161
  }
@@ -166,6 +169,7 @@ export class PivotCollectionPersister {
166
169
  convertCustomTypes: false,
167
170
  processCollections: false,
168
171
  loggerContext: this.#loggerContext,
172
+ ...this.#abort,
169
173
  });
170
174
  }
171
175
  }
@@ -181,6 +185,7 @@ export class PivotCollectionPersister {
181
185
  upsert: true,
182
186
  onConflictAction: 'ignore',
183
187
  loggerContext: this.#loggerContext,
188
+ ...this.#abort,
184
189
  });
185
190
  }
186
191
  }