@mikro-orm/sql 7.2.0-dev.8 → 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,5 +1,5 @@
1
1
  import { type ControlledTransaction, type Dialect, Kysely } from 'kysely';
2
- import { type AnyEntity, Connection, type Dictionary, type EntityData, type IsolationLevel, type LogContext, type LoggingOptions, type MaybePromise, type QueryResult, type RawQueryFragment, type Transaction, type TransactionEventBroadcaster } from '@mikro-orm/core';
2
+ import { type AnyEntity, Connection, type Dictionary, type EntityData, type IsolationLevel, type LogContext, type LoggingOptions, type MaybePromise, type QueryResult, type RawQueryFragment, type SessionContext, type Transaction, type TransactionEventBroadcaster } from '@mikro-orm/core';
3
3
  import type { AbstractSqlPlatform } from './AbstractSqlPlatform.js';
4
4
  import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
5
5
  /** Base class for SQL database connections, built on top of Kysely. */
@@ -50,6 +50,7 @@ export declare abstract class AbstractSqlConnection extends Connection {
50
50
  ctx?: ControlledTransaction<any>;
51
51
  eventBroadcaster?: TransactionEventBroadcaster;
52
52
  loggerContext?: LogContext;
53
+ sessionContext?: SessionContext;
53
54
  }): Promise<T>;
54
55
  /** Begins a new transaction or creates a savepoint if a transaction context already exists. */
55
56
  begin(options?: {
@@ -58,7 +59,17 @@ export declare abstract class AbstractSqlConnection extends Connection {
58
59
  ctx?: ControlledTransaction<any, any>;
59
60
  eventBroadcaster?: TransactionEventBroadcaster;
60
61
  loggerContext?: LogContext;
62
+ sessionContext?: SessionContext;
61
63
  }): Promise<ControlledTransaction<any, any>>;
64
+ /** Applies session variables (`set_config`) and role (`set local role`) for the current transaction. */
65
+ private applySessionContext;
66
+ /**
67
+ * Quotes a role name as a single PostgreSQL identifier. Unlike `platform.quoteIdentifier`, which treats a dot as a
68
+ * schema qualifier, a role like `my.role` must be quoted whole (`"my.role"`) with embedded quotes doubled.
69
+ */
70
+ protected static quoteRole(role: string): string;
71
+ /** Serializes a session variable for `set_config()`; `Date` values use ISO 8601 so casts like `::timestamptz` parse. */
72
+ protected stringifySessionVariable(value: unknown): string;
62
73
  /** Commits the transaction or releases the savepoint. */
63
74
  commit(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
64
75
  /** Rolls back the transaction or rolls back to the savepoint. */
@@ -74,9 +85,9 @@ export declare abstract class AbstractSqlConnection extends Connection {
74
85
  private waitForIdleTransaction;
75
86
  private prepareQuery;
76
87
  /** Executes a SQL query and returns the result based on the method: `'all'` for rows, `'get'` for single row, `'run'` for affected count. */
77
- 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>;
88
+ execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[] | Dictionary<unknown>, method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
78
89
  /** Executes a SQL query and returns an async iterable that yields results row by row. */
79
- stream<T extends EntityData<AnyEntity>>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[], ctx?: Transaction<Kysely<any>>, loggerContext?: LoggingOptions, chunkSize?: number): AsyncIterableIterator<T>;
90
+ stream<T extends EntityData<AnyEntity>>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[] | Dictionary<unknown>, ctx?: Transaction<Kysely<any>>, loggerContext?: LoggingOptions, chunkSize?: number): AsyncIterableIterator<T>;
80
91
  /** @inheritDoc */
81
92
  executeDump(dump: string): Promise<void>;
82
93
  protected getSql(query: string, formatted: string, context?: LogContext): string;
@@ -1,5 +1,5 @@
1
1
  import { CompiledQuery, Kysely } from 'kysely';
2
- import { Connection, EventType, isRaw, Utils, } from '@mikro-orm/core';
2
+ import { Connection, EventType, isRaw, raw, Utils, } from '@mikro-orm/core';
3
3
  import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
4
4
  /**
5
5
  * Pulls cancellation controls out of a `loggerContext` payload, returning the abort options
@@ -161,9 +161,43 @@ export class AbstractSqlConnection extends Connection {
161
161
  for (const query of this.platform.getBeginTransactionSQL(options)) {
162
162
  this.logQuery(query, options.loggerContext);
163
163
  }
164
+ if (options.sessionContext) {
165
+ try {
166
+ await this.applySessionContext(trx, options.sessionContext, options.loggerContext);
167
+ }
168
+ catch (e) {
169
+ // roll back the freshly opened transaction so the pooled connection is released, not leaked
170
+ await trx.rollback().execute();
171
+ this.logQuery(this.platform.getRollbackTransactionSQL(), options.loggerContext);
172
+ throw e;
173
+ }
174
+ }
164
175
  await options.eventBroadcaster?.dispatchEvent(EventType.afterTransactionStart, trx);
165
176
  return trx;
166
177
  }
178
+ /** Applies session variables (`set_config`) and role (`set local role`) for the current transaction. */
179
+ async applySessionContext(trx, sessionContext, loggerContext) {
180
+ const variables = Object.entries(sessionContext.variables ?? {});
181
+ if (variables.length > 0) {
182
+ const parts = variables.map(() => 'set_config(?, ?, true)').join(', ');
183
+ const params = variables.flatMap(([key, value]) => [key, this.stringifySessionVariable(value)]);
184
+ await this.execute(`select ${parts}`, params, 'run', trx, loggerContext);
185
+ }
186
+ if (sessionContext.role) {
187
+ await this.execute(`set local role ${AbstractSqlConnection.quoteRole(sessionContext.role)}`, [], 'run', trx, loggerContext);
188
+ }
189
+ }
190
+ /**
191
+ * Quotes a role name as a single PostgreSQL identifier. Unlike `platform.quoteIdentifier`, which treats a dot as a
192
+ * schema qualifier, a role like `my.role` must be quoted whole (`"my.role"`) with embedded quotes doubled.
193
+ */
194
+ static quoteRole(role) {
195
+ return `"${role.replaceAll('"', '""')}"`;
196
+ }
197
+ /** Serializes a session variable for `set_config()`; `Date` values use ISO 8601 so casts like `::timestamptz` parse. */
198
+ stringifySessionVariable(value) {
199
+ return value instanceof Date ? value.toISOString() : String(value);
200
+ }
167
201
  /** Commits the transaction or releases the savepoint. */
168
202
  async commit(ctx, eventBroadcaster, loggerContext) {
169
203
  if (ctx.isRolledBack) {
@@ -209,13 +243,17 @@ export class AbstractSqlConnection extends Connection {
209
243
  if (query instanceof NativeQueryBuilder) {
210
244
  query = query.toRaw();
211
245
  }
246
+ if (typeof query === 'string' && !Array.isArray(params)) {
247
+ // plain object params hold named parameters, translate them via the `raw()` helper
248
+ query = raw(query, params);
249
+ }
212
250
  if (isRaw(query)) {
213
251
  params = query.params;
214
252
  query = query.sql;
215
253
  }
216
254
  query = this.config.get('onQuery')(query, params);
217
255
  const formatted = this.platform.formatQuery(query, params);
218
- return { query, params, formatted };
256
+ return { query, params: params, formatted };
219
257
  }
220
258
  /** Executes a SQL query and returns the result based on the method: `'all'` for rows, `'get'` for single row, `'run'` for affected count. */
221
259
  async execute(query, params = [], method = 'all', ctx, loggerContext) {
@@ -249,7 +287,7 @@ export class AbstractSqlConnection extends Connection {
249
287
  .stream(compiled, chunkSize ?? 100, abort ? { signal: abort.signal } : undefined);
250
288
  this.logQuery(sql, {
251
289
  sql,
252
- params,
290
+ params: q.params,
253
291
  ...cleanCtx,
254
292
  affected: Utils.isPlainObject(res) ? res.affectedRows : undefined,
255
293
  });
@@ -260,7 +298,7 @@ export class AbstractSqlConnection extends Connection {
260
298
  }
261
299
  }
262
300
  catch (e) {
263
- this.logQuery(sql, { sql, params, ...cleanCtx, level: 'error' });
301
+ this.logQuery(sql, { sql, params: q.params, ...cleanCtx, level: 'error' });
264
302
  throw e;
265
303
  }
266
304
  }
@@ -100,7 +100,7 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
100
100
  */
101
101
  private convertOwnerPksForPivotQuery;
102
102
  private getPivotOrderBy;
103
- execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[], method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
103
+ execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[] | Dictionary, method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
104
104
  stream<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T, any, any, any>): AsyncIterableIterator<T>;
105
105
  /**
106
106
  * 1:1 owner side needs to be marked for population so QB auto-joins the owner id
@@ -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
@@ -183,7 +183,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
183
183
  if (typeof meta.expression === 'string') {
184
184
  return this.wrapVirtualExpressionInSubquery(meta, meta.expression, where, options, type);
185
185
  }
186
- const em = this.createEntityManager();
186
+ // fork the caller EM so the callback inherits its filters, filter params and session context — a fork never
187
+ // resolves back to the ambient RequestContext EM, which would ignore the transaction/session context set below
188
+ const em = (options.em?.fork() ?? this.createEntityManager(false));
187
189
  em.setTransactionContext(options.ctx);
188
190
  const res = meta.expression(em, where, options);
189
191
  if (typeof res === 'string') {
@@ -209,7 +211,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
209
211
  yield* this.wrapVirtualExpressionInSubqueryStream(meta, meta.expression, where, options, QueryType.SELECT);
210
212
  return;
211
213
  }
212
- const em = this.createEntityManager();
214
+ // fork the caller EM for the same reason as in `findFromVirtual`
215
+ const em = (options.em?.fork() ?? this.createEntityManager(false));
213
216
  em.setTransactionContext(options.ctx);
214
217
  const res = meta.expression(em, where, options, true);
215
218
  if (typeof res === 'string') {
@@ -239,7 +242,17 @@ export class AbstractSqlDriver extends DatabaseDriver {
239
242
  const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' ';
240
243
  native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
241
244
  const query = native.compile();
242
- const res = await this.execute(query.sql, query.params, 'all', options.ctx, withAbortContext(options.loggerContext, options));
245
+ const loggerContext = withAbortContext(options.loggerContext, options);
246
+ // virtual entities execute directly (not via QueryBuilder), so wrap in a short implicit transaction outside an
247
+ // existing one when a session context (row level security) needs to apply — mirrors the QueryBuilder wrap
248
+ const sessionContext = options.ctx ? undefined : options.em?.getTransactionSessionContext();
249
+ const conn = this.getConnection(this.resolveConnectionType(options));
250
+ const res = await (sessionContext
251
+ ? conn.transactional(trx => this.execute(query.sql, query.params, 'all', trx, loggerContext), {
252
+ sessionContext,
253
+ loggerContext,
254
+ })
255
+ : this.execute(query.sql, query.params, 'all', options.ctx, loggerContext));
243
256
  if (type === QueryType.COUNT) {
244
257
  return res[0].count;
245
258
  }
@@ -915,7 +928,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
915
928
  if (!options.upsert && options.unionWhere?.length) {
916
929
  where = (await this.applyUnionWhere(meta, where, options, true));
917
930
  }
918
- if (Utils.hasObjectKeys(data)) {
931
+ if (options.upsert && meta.tptParent) {
932
+ res = await this.nativeUpdateMany(entityName, [where], [data], options);
933
+ }
934
+ else if (Utils.hasObjectKeys(data) || (meta.inheritanceType === 'tpt' && meta.ownsVersionProperty())) {
935
+ // a TPT table declaring the version property is bumped even when only other tables of the hierarchy changed
919
936
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
920
937
  qb.setAbortOptions(pickAbortOptions(options));
921
938
  if (options.upsert) {
@@ -941,7 +958,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
941
958
  qb.update(data).where(where);
942
959
  // reload generated columns and version fields
943
960
  const returning = [];
944
- meta.props
961
+ this.getTableProps(meta)
945
962
  .filter(prop => (prop.generated && !prop.primary) || prop.version)
946
963
  .forEach(prop => returning.push(prop.name));
947
964
  qb.returning(returning);
@@ -957,13 +974,38 @@ export class AbstractSqlDriver extends DatabaseDriver {
957
974
  options.convertCustomTypes ??= true;
958
975
  const meta = this.metadata.get(entityName);
959
976
  if (options.upsert) {
977
+ if (meta.tptParent) {
978
+ // TPT parent tables go first, the PK they provide is the conflict target of this table
979
+ await this.nativeUpdateMany(meta.tptParent.class, where, data, options);
980
+ for (const [i, row] of data.entries()) {
981
+ if (meta.primaryKeys.some(pk => row[pk] == null)) {
982
+ const found = await this.findOne(meta.tptParent.class, where[i], {
983
+ fields: meta.primaryKeys,
984
+ ctx: options.ctx,
985
+ connectionType: 'write',
986
+ schema: options.schema,
987
+ });
988
+ meta.primaryKeys.forEach(pk => (row[pk] = found?.[pk]));
989
+ }
990
+ }
991
+ options = { ...options, onConflictFields: meta.primaryKeys, onConflictWhere: undefined };
992
+ }
960
993
  const uniqueFields = options.onConflictFields ??
961
994
  (Utils.isPlainObject(where[0])
962
995
  ? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key))
963
996
  : meta.primaryKeys);
964
997
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
965
998
  qb.setAbortOptions(pickAbortOptions(options));
966
- const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
999
+ let returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
1000
+ if (meta.inheritanceType === 'tpt') {
1001
+ // each TPT table only carries its own columns, the entity is reloaded instead of mapping the returned rows
1002
+ const own = (key) => meta.primaryKeys.includes(key) ||
1003
+ this.getTableProps(meta).some(prop => prop.name === key.split('.')[0]);
1004
+ data = data.map(row => Object.fromEntries(Object.entries(row).filter(([key]) => own(key))));
1005
+ options.onConflictMergeFields = options.onConflictMergeFields?.filter(f => own(f));
1006
+ options.onConflictExcludeFields = options.onConflictExcludeFields?.filter(f => own(f));
1007
+ returning = [];
1008
+ }
967
1009
  qb.insert(data)
968
1010
  .onConflict(uniqueFields)
969
1011
  .returning(returning);
@@ -977,7 +1019,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
977
1019
  if (options.onConflictWhere) {
978
1020
  qb.where(options.onConflictWhere);
979
1021
  }
980
- return this.rethrow(qb.execute('run', false));
1022
+ const res = await this.rethrow(qb.execute('run', false));
1023
+ return meta.inheritanceType === 'tpt' ? { ...res, row: undefined, rows: [] } : res;
981
1024
  }
982
1025
  const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
983
1026
  const keys = new Set();
@@ -992,7 +1035,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
992
1035
  }
993
1036
  }
994
1037
  // reload generated columns and version fields
995
- meta.props.filter(prop => prop.generated || prop.version || prop.primary).forEach(prop => returning.add(prop.name));
1038
+ meta.getPrimaryProps().forEach(prop => returning.add(prop.name));
1039
+ this.getTableProps(meta)
1040
+ .filter(prop => prop.generated || prop.version)
1041
+ .forEach(prop => returning.add(prop.name));
996
1042
  const pkCond = Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames))
997
1043
  .map(pk => `${this.platform.quoteIdentifier(pk)} = ?`)
998
1044
  .join(' and ');
@@ -1050,7 +1096,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1050
1096
  return sql;
1051
1097
  });
1052
1098
  }
1053
- if (meta.versionProperty) {
1099
+ if (meta.ownsVersionProperty()) {
1054
1100
  const versionProperty = meta.properties[meta.versionProperty];
1055
1101
  const quotedFieldName = this.platform.quoteIdentifier(versionProperty.fieldNames[0]);
1056
1102
  sql += `${quotedFieldName} = `;
@@ -1063,7 +1109,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1063
1109
  sql += `, `;
1064
1110
  }
1065
1111
  sql = sql.substring(0, sql.length - 2) + ' where ';
1066
- const pkProps = meta.primaryKeys.concat(...meta.concurrencyCheckKeys);
1112
+ const pkProps = meta.primaryKeys.concat(...meta.getOwnConcurrencyCheckKeys());
1067
1113
  const pks = Utils.flatten(pkProps.map(pk => meta.properties[pk].fieldNames));
1068
1114
  const useTupleIn = pks.length <= 1 || this.platform.allowsComparingTuples();
1069
1115
  const condTemplate = useTupleIn
@@ -1300,7 +1346,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1300
1346
  ],
1301
1347
  populateWhere: undefined,
1302
1348
  _populateWhere: 'infer',
1303
- populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
1349
+ populateFilter: this.wrapPopulateFilter(options, pivotProp1.name),
1304
1350
  };
1305
1351
  if (pivotFindOptions._partitionLimit) {
1306
1352
  pivotFindOptions._partitionLimit.partitionBy = pivotProp2.name;
@@ -1649,6 +1695,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
1649
1695
  if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
1650
1696
  return false;
1651
1697
  }
1698
+ // Polymorphic to-one flattened from an object embeddable lives inside a JSON column, so the join
1699
+ // conditions would need JSON extraction for both the discriminator and the FK; fall back to SELECT_IN.
1700
+ if (prop.polymorphic && prop.object && prop.embedded) {
1701
+ return false;
1702
+ }
1652
1703
  if (strategy !== LoadStrategy.JOINED) {
1653
1704
  // force joined strategy for explicit 1:1 owner populate hint as it would require a join anyway
1654
1705
  return prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner;
@@ -1779,7 +1830,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1779
1830
  // INNER JOINs get nested inside the polymorphic LEFT JOIN by processNestedJoins, which
1780
1831
  // keeps the resulting query valid for rows pointing to other polymorphic targets.
1781
1832
  if (targetMeta.inheritanceType === 'tpt' && targetMeta.tptParent) {
1782
- this.addTPTParentJoinsForRelation(qb, targetMeta, tableAlias, targetPath);
1833
+ qb.addTPTParentJoins(targetMeta, tableAlias, targetPath);
1783
1834
  }
1784
1835
  // For polymorphic targets that are TPT base classes, also LEFT JOIN
1785
1836
  // all descendant tables so child-specific fields can be selected.
@@ -1827,10 +1878,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
1827
1878
  : JoinType.leftJoin;
1828
1879
  const schema = prop.targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : prop.targetMeta.schema;
1829
1880
  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
1881
  // For relations to TPT base classes, add LEFT JOINs for all child tables (polymorphic loading)
1835
1882
  if (meta2.inheritanceType === 'tpt' && meta2.tptChildren?.length && !ref) {
1836
1883
  // Use the registry metadata to ensure allTPTDescendants is available
@@ -1877,25 +1924,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
1877
1924
  }
1878
1925
  return fields;
1879
1926
  }
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
1927
  /**
1900
1928
  * Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
1901
1929
  * @internal
@@ -1,4 +1,4 @@
1
- import { type RawQueryFragment, type Constructor, type EntityManager, type EntityRepository, type IDatabaseDriver, type IsolationLevel, type MikroORM, Platform } from '@mikro-orm/core';
1
+ import { type RawQueryFragment, type Constructor, type EntityManager, type EntityProperty, type EntityRepository, type FormulaColumns, type IDatabaseDriver, type IsolationLevel, type MikroORM, Platform } from '@mikro-orm/core';
2
2
  import { SqlSchemaGenerator } from './schema/SqlSchemaGenerator.js';
3
3
  import { type SchemaHelper } from './schema/SchemaHelper.js';
4
4
  import type { IndexDef } from './typings.js';
@@ -17,6 +17,8 @@ export declare abstract class AbstractSqlPlatform extends Platform {
17
17
  getSchemaGenerator(driver: IDatabaseDriver, em?: EntityManager): SqlSchemaGenerator;
18
18
  /** @internal */
19
19
  createNativeQueryBuilder(): NativeQueryBuilder;
20
+ /** @inheritDoc */
21
+ getThroughRelationFormula(prop: EntityProperty, columns: FormulaColumns<any>): string;
20
22
  getBeginTransactionSQL(options?: {
21
23
  isolationLevel?: IsolationLevel;
22
24
  readOnly?: boolean;
@@ -28,7 +30,7 @@ export declare abstract class AbstractSqlPlatform extends Platform {
28
30
  getReleaseSavepointSQL(savepointName: string): string;
29
31
  quoteValue(value: any): string;
30
32
  getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string | RawQueryFragment;
31
- getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string | RawQueryFragment;
33
+ getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean | string, value?: unknown): string | RawQueryFragment;
32
34
  /**
33
35
  * Quotes a key for use inside a JSON path expression (e.g. `$.key`).
34
36
  * Simple alphanumeric keys are left unquoted; others are wrapped in double quotes
@@ -1,4 +1,4 @@
1
- import { isRaw, JsonProperty, Platform, raw, Utils, } from '@mikro-orm/core';
1
+ import { isRaw, JsonProperty, Platform, raw, Utils, ValidationError, } from '@mikro-orm/core';
2
2
  import { SqlEntityRepository } from './SqlEntityRepository.js';
3
3
  import { SqlSchemaGenerator } from './schema/SqlSchemaGenerator.js';
4
4
  import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
@@ -39,6 +39,37 @@ export class AbstractSqlPlatform extends Platform {
39
39
  createNativeQueryBuilder() {
40
40
  return new NativeQueryBuilder(this);
41
41
  }
42
+ /** @inheritDoc */
43
+ getThroughRelationFormula(prop, columns) {
44
+ const through = prop.through;
45
+ const driver = this.config.getDriver();
46
+ const alias = `${prop.name}_through`;
47
+ const qb = driver.createQueryBuilder(through.entity, undefined, 'read', true, undefined, alias);
48
+ const throughMeta = driver.getMetadata().get(through.entity);
49
+ const ownerProp = throughMeta.properties[through.ownerProperty];
50
+ const ownerMeta = driver.getMetadata().get(ownerProp.target);
51
+ ownerProp.fieldNames.forEach((fieldName, idx) => {
52
+ const referencedColumn = ownerProp.referencedColumnNames[idx];
53
+ const referencedProp = ownerProp.referencedPKs
54
+ .map(name => ownerMeta.properties[name])
55
+ .find(p => p.fieldNames.includes(referencedColumn));
56
+ // the column mapping resolves the right alias even for TPT owners, only the column name may differ for composite FK properties
57
+ const ownerAlias = columns[referencedProp.name].slice(0, columns[referencedProp.name].lastIndexOf('.'));
58
+ qb.andWhere(raw('?? = ??', [`${alias}.${fieldName}`, `${ownerAlias}.${referencedColumn}`]));
59
+ });
60
+ if (through.where) {
61
+ qb.andWhere(through.where);
62
+ }
63
+ if (through.orderBy) {
64
+ qb.orderBy(through.orderBy);
65
+ }
66
+ qb.select(through.targetProperty ?? throughMeta.primaryKeys[0]).limit(1);
67
+ const sql = qb.getFormattedQuery();
68
+ if (Object.keys(qb.state.joins).length > 0) {
69
+ throw new ValidationError(`The 'where' and 'orderBy' options of the through relation ${prop.name} can only reference own columns of ${throughMeta.className}`);
70
+ }
71
+ return `(${sql})`;
72
+ }
42
73
  getBeginTransactionSQL(options) {
43
74
  if (options?.isolationLevel) {
44
75
  return [`set transaction isolation level ${options.isolationLevel}`, 'begin'];
@@ -75,6 +106,9 @@ export class AbstractSqlPlatform extends Platform {
75
106
  getSearchJsonPropertyKey(path, type, aliased, value) {
76
107
  const [a, ...b] = path;
77
108
  const jsonPath = this.quoteValue(`$.${b.map(this.quoteJsonKey).join('.')}`);
109
+ if (typeof aliased === 'string') {
110
+ return raw(`json_extract(${this.quoteIdentifier(`${aliased}.${a}`)}, ${jsonPath})`);
111
+ }
78
112
  if (aliased) {
79
113
  return raw(alias => `json_extract(${this.quoteIdentifier(`${alias}.${a}`)}, ${jsonPath})`);
80
114
  }
package/README.md CHANGED
@@ -24,6 +24,7 @@ npm install @mikro-orm/mysql # MySQL
24
24
  npm install @mikro-orm/mariadb # MariaDB
25
25
  npm install @mikro-orm/sqlite # SQLite
26
26
  npm install @mikro-orm/libsql # libSQL / Turso
27
+ npm install @mikro-orm/sql-js # sql.js (in-memory SQLite in WASM)
27
28
  npm install @mikro-orm/mongodb # MongoDB
28
29
  npm install @mikro-orm/mssql # MS SQL Server
29
30
  npm install @mikro-orm/oracledb # Oracle
@@ -60,14 +60,14 @@ export declare class SqlEntityManager<Driver extends AbstractSqlDriver = Abstrac
60
60
  * `signal` / `inflightQueryAbortStrategy` (set via `em.fork({ signal })`) is applied automatically.
61
61
  * For per-call cancellation use the options-bag overload below.
62
62
  */
63
- execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[], method?: 'all' | 'get' | 'run', loggerContext?: LoggingOptions): Promise<T>;
63
+ execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[] | Dictionary, method?: 'all' | 'get' | 'run', loggerContext?: LoggingOptions): Promise<T>;
64
64
  /**
65
65
  * Executes a raw SQL query with an options bag carrying `method`, `loggerContext`, `signal`
66
66
  * and `inflightQueryAbortStrategy`. Per-call `signal` / `inflightQueryAbortStrategy` override
67
67
  * the fork-level defaults set via `em.fork({ signal })`. The current transaction context is
68
68
  * applied automatically.
69
69
  */
70
- execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params: any[], options: EmExecuteOptions): Promise<T>;
70
+ execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params: any[] | Dictionary, options: EmExecuteOptions): Promise<T>;
71
71
  /**
72
72
  * @inheritDoc
73
73
  */
@@ -68,7 +68,7 @@ export class SqlEntityManager extends EntityManager {
68
68
  merged.signal = opts.signal ?? fork?.signal;
69
69
  merged.inflightQueryAbortStrategy = opts.inflightQueryAbortStrategy ?? fork?.inflightQueryAbortStrategy;
70
70
  }
71
- return this.getDriver().execute(query, params, opts.method ?? 'all', context.getTransactionContext(), merged);
71
+ return context.withSessionContext(context.getTransactionContext(), ctx => this.getDriver().execute(query, params, opts.method ?? 'all', ctx, merged));
72
72
  }
73
73
  /**
74
74
  * @inheritDoc
@@ -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)
@@ -14,6 +14,8 @@ export declare class BaseMySqlPlatform extends AbstractSqlPlatform {
14
14
  readonly "desc nulls first": 'is not null';
15
15
  readonly "desc nulls last": 'is null';
16
16
  };
17
+ /** mysql and mariadb treat null as the lowest value when no placement is requested. */
18
+ sortsNullsLowest(): boolean;
17
19
  supportsMultiColumnCountDistinct(): boolean;
18
20
  /** @internal */
19
21
  createNativeQueryBuilder(): MySqlNativeQueryBuilder;
@@ -18,6 +18,10 @@ export class BaseMySqlPlatform extends AbstractSqlPlatform {
18
18
  [QueryOrder.desc_nulls_first]: 'is not null',
19
19
  [QueryOrder.desc_nulls_last]: 'is null',
20
20
  };
21
+ /** mysql and mariadb treat null as the lowest value when no placement is requested. */
22
+ sortsNullsLowest() {
23
+ return true;
24
+ }
21
25
  supportsMultiColumnCountDistinct() {
22
26
  return true;
23
27
  }
@@ -16,6 +16,8 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
16
16
  getEnumArrayCheckConstraintExpression(column: string, items: string[]): string;
17
17
  supportsMaterializedViews(): boolean;
18
18
  supportsPartitionedTables(): boolean;
19
+ supportsRowLevelSecurity(): boolean;
20
+ getCurrentSettingCast(mappedType: Type<unknown>): string | null;
19
21
  supportsCustomPrimaryKeyNames(): boolean;
20
22
  getCurrentTimestampSQL(length: number): string;
21
23
  getDateTimeTypeDeclarationSQL(column: {
@@ -88,7 +90,7 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
88
90
  }): string;
89
91
  getBlobDeclarationSQL(): string;
90
92
  getJsonDeclarationSQL(): string;
91
- getSearchJsonPropertyKey(path: string[], type: string | undefined | Type, aliased: boolean, value?: unknown): string | RawQueryFragment;
93
+ getSearchJsonPropertyKey(path: string[], type: string | undefined | Type, aliased: boolean | string, value?: unknown): string | RawQueryFragment;
92
94
  getJsonIndexDefinition(index: IndexDef): string[];
93
95
  quoteIdentifier(id: string | {
94
96
  toString: () => string;
@@ -1,4 +1,4 @@
1
- import { ALIAS_REPLACEMENT, ARRAY_OPERATORS, raw, RawQueryFragment, Type, Utils, } from '@mikro-orm/core';
1
+ import { ALIAS_REPLACEMENT, ARRAY_OPERATORS, BigIntType, BooleanType, DateTimeType, DateType, EnumType, IntegerType, raw, RawQueryFragment, SmallIntType, StringType, TextType, TimeType, TinyIntType, Type, Utils, UuidType, } from '@mikro-orm/core';
2
2
  import { AbstractSqlPlatform } from '../../AbstractSqlPlatform.js';
3
3
  import { PostgreSqlNativeQueryBuilder } from './PostgreSqlNativeQueryBuilder.js';
4
4
  import { PostgreSqlSchemaHelper } from './PostgreSqlSchemaHelper.js';
@@ -33,6 +33,38 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
33
33
  supportsPartitionedTables() {
34
34
  return true;
35
35
  }
36
+ supportsRowLevelSecurity() {
37
+ return true;
38
+ }
39
+ getCurrentSettingCast(mappedType) {
40
+ if (mappedType instanceof UuidType) {
41
+ return '::uuid';
42
+ }
43
+ if (mappedType instanceof BigIntType) {
44
+ return '::bigint';
45
+ }
46
+ // MediumIntType extends IntegerType, so it is covered here too
47
+ if (mappedType instanceof IntegerType || mappedType instanceof SmallIntType || mappedType instanceof TinyIntType) {
48
+ return '::int';
49
+ }
50
+ if (mappedType instanceof BooleanType) {
51
+ return '::boolean';
52
+ }
53
+ if (mappedType instanceof DateTimeType) {
54
+ return '::timestamptz';
55
+ }
56
+ if (mappedType instanceof DateType) {
57
+ return '::date';
58
+ }
59
+ if (mappedType instanceof TimeType) {
60
+ return '::time';
61
+ }
62
+ // current_setting() returns text already, so string-compatible types need no cast (CharacterType extends StringType)
63
+ if (mappedType instanceof StringType || mappedType instanceof TextType || mappedType instanceof EnumType) {
64
+ return '';
65
+ }
66
+ return null;
67
+ }
36
68
  supportsCustomPrimaryKeyNames() {
37
69
  return true;
38
70
  }
@@ -285,7 +317,8 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
285
317
  getSearchJsonPropertyKey(path, type, aliased, value) {
286
318
  const first = path.shift();
287
319
  const last = path.pop();
288
- const root = this.quoteIdentifier(aliased ? `${ALIAS_REPLACEMENT}.${first}` : first);
320
+ const alias = typeof aliased === 'string' ? aliased : ALIAS_REPLACEMENT;
321
+ const root = this.quoteIdentifier(aliased ? `${alias}.${first}` : first);
289
322
  type = typeof type === 'string' ? this.getMappedType(type).runtimeType : String(type);
290
323
  const cast = (key) => raw(type in this.#jsonTypeCasts ? `(${key})::${this.#jsonTypeCasts[type]}` : key);
291
324
  let lastOperator = '->>';
@@ -1,4 +1,4 @@
1
- import { DeadlockException, ExceptionConverter, ForeignKeyConstraintViolationException, InvalidFieldNameException, NonUniqueFieldNameException, NotNullConstraintViolationException, SyntaxErrorException, TableExistsException, TableNotFoundException, UniqueConstraintViolationException, CheckConstraintViolationException, } from '@mikro-orm/core';
1
+ import { DeadlockException, ExceptionConverter, ForeignKeyConstraintViolationException, InvalidFieldNameException, NonUniqueFieldNameException, NotNullConstraintViolationException, RowLevelSecurityViolationException, SyntaxErrorException, TableExistsException, TableNotFoundException, UniqueConstraintViolationException, CheckConstraintViolationException, } from '@mikro-orm/core';
2
2
  export class PostgreSqlExceptionConverter extends ExceptionConverter {
3
3
  /**
4
4
  * @see http://www.postgresql.org/docs/9.4/static/errcodes-appendix.html
@@ -31,6 +31,13 @@ export class PostgreSqlExceptionConverter extends ExceptionConverter {
31
31
  return new UniqueConstraintViolationException(exception);
32
32
  case '23514':
33
33
  return new CheckConstraintViolationException(exception);
34
+ case '42501':
35
+ // 42501 is generic insufficient_privilege; only RLS write violations carry this message — the `routine`
36
+ // field covers servers with a non-english `lc_messages`, where the message check cannot match
37
+ if (exception.message.includes('row-level security policy') || exception.routine === 'ExecWithCheckOptions') {
38
+ return new RowLevelSecurityViolationException(exception);
39
+ }
40
+ break;
34
41
  case '42601':
35
42
  return new SyntaxErrorException(exception);
36
43
  case '42702':