@mikro-orm/sql 7.2.0-dev.2 → 7.2.0-dev.20
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.
- package/AbstractSqlConnection.d.ts +30 -3
- package/AbstractSqlConnection.js +75 -15
- package/AbstractSqlDriver.d.ts +1 -8
- package/AbstractSqlDriver.js +124 -55
- package/AbstractSqlPlatform.d.ts +4 -2
- package/AbstractSqlPlatform.js +35 -1
- package/SqlEntityManager.d.ts +2 -2
- package/SqlEntityManager.js +5 -4
- package/dialects/mssql/MsSqlNativeQueryBuilder.js +1 -1
- package/dialects/mysql/BaseMySqlPlatform.d.ts +2 -0
- package/dialects/mysql/BaseMySqlPlatform.js +4 -0
- package/dialects/mysql/MySqlSchemaHelper.d.ts +1 -0
- package/dialects/mysql/MySqlSchemaHelper.js +4 -1
- package/dialects/oracledb/OracleNativeQueryBuilder.js +1 -1
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +3 -1
- package/dialects/postgresql/BasePostgreSqlPlatform.js +35 -2
- package/dialects/postgresql/PostgreSqlExceptionConverter.js +8 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +22 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +181 -4
- package/dialects/sqlite/BaseSqliteConnection.d.ts +3 -0
- package/dialects/sqlite/BaseSqliteConnection.js +15 -5
- package/dialects/sqlite/SqlitePlatform.d.ts +2 -0
- package/dialects/sqlite/SqlitePlatform.js +4 -0
- package/dialects/sqlite/SqliteSchemaHelper.js +2 -2
- package/package.json +4 -4
- package/plugin/transformer.d.ts +7 -1
- package/plugin/transformer.js +60 -1
- package/query/CriteriaNodeFactory.js +4 -0
- package/query/NativeQueryBuilder.js +1 -1
- package/query/ObjectCriteriaNode.d.ts +1 -0
- package/query/ObjectCriteriaNode.js +30 -5
- package/query/QueryBuilder.d.ts +40 -7
- package/query/QueryBuilder.js +181 -37
- package/query/QueryBuilderHelper.d.ts +5 -0
- package/query/QueryBuilderHelper.js +39 -9
- package/schema/DatabaseSchema.d.ts +4 -0
- package/schema/DatabaseSchema.js +107 -1
- package/schema/DatabaseTable.d.ts +15 -1
- package/schema/DatabaseTable.js +113 -19
- package/schema/SchemaComparator.d.ts +3 -0
- package/schema/SchemaComparator.js +123 -10
- package/schema/SchemaHelper.d.ts +26 -1
- package/schema/SchemaHelper.js +67 -9
- package/schema/SqlSchemaGenerator.d.ts +4 -0
- package/schema/SqlSchemaGenerator.js +59 -22
- package/typings.d.ts +20 -2
|
@@ -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. */
|
|
@@ -36,6 +36,13 @@ export declare abstract class AbstractSqlConnection extends Connection {
|
|
|
36
36
|
getClient<T = any>(): Kysely<T>;
|
|
37
37
|
/** Ensures the Kysely client is initialized, creating it asynchronously if needed. */
|
|
38
38
|
initClient(): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Guards `getNativeClient()` overrides — drivers only capture their client while building their
|
|
41
|
+
* own dialect, which `driverOptions` carrying a ready-made Kysely instance or dialect skips.
|
|
42
|
+
*
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
protected requireNativeClient<T>(client: T | undefined | null): T;
|
|
39
46
|
/** Executes a callback within a transaction, committing on success and rolling back on error. */
|
|
40
47
|
transactional<T>(cb: (trx: Transaction<ControlledTransaction<any, any>>) => Promise<T>, options?: {
|
|
41
48
|
isolationLevel?: IsolationLevel;
|
|
@@ -43,6 +50,7 @@ export declare abstract class AbstractSqlConnection extends Connection {
|
|
|
43
50
|
ctx?: ControlledTransaction<any>;
|
|
44
51
|
eventBroadcaster?: TransactionEventBroadcaster;
|
|
45
52
|
loggerContext?: LogContext;
|
|
53
|
+
sessionContext?: SessionContext;
|
|
46
54
|
}): Promise<T>;
|
|
47
55
|
/** Begins a new transaction or creates a savepoint if a transaction context already exists. */
|
|
48
56
|
begin(options?: {
|
|
@@ -51,16 +59,35 @@ export declare abstract class AbstractSqlConnection extends Connection {
|
|
|
51
59
|
ctx?: ControlledTransaction<any, any>;
|
|
52
60
|
eventBroadcaster?: TransactionEventBroadcaster;
|
|
53
61
|
loggerContext?: LogContext;
|
|
62
|
+
sessionContext?: SessionContext;
|
|
54
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;
|
|
55
73
|
/** Commits the transaction or releases the savepoint. */
|
|
56
74
|
commit(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
|
|
57
75
|
/** Rolls back the transaction or rolls back to the savepoint. */
|
|
58
76
|
rollback(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Waits until the transaction's connection has no query in flight. Kysely runs `rollback` straight
|
|
79
|
+
* on that connection instead of going through its connection provider, so a rollback caused by an
|
|
80
|
+
* aborted query would otherwise be sent while the aborted query is still running. That not only
|
|
81
|
+
* queues the rollback behind it on the server, it also overwrites the query id Kysely compares
|
|
82
|
+
* against before firing the `'cancel query'`/`'kill session'` control statement — the control
|
|
83
|
+
* statement is then discarded as stale and the abort never reaches the database.
|
|
84
|
+
*/
|
|
85
|
+
private waitForIdleTransaction;
|
|
59
86
|
private prepareQuery;
|
|
60
87
|
/** Executes a SQL query and returns the result based on the method: `'all'` for rows, `'get'` for single row, `'run'` for affected count. */
|
|
61
|
-
execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[]
|
|
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>;
|
|
62
89
|
/** 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[]
|
|
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>;
|
|
64
91
|
/** @inheritDoc */
|
|
65
92
|
executeDump(dump: string): Promise<void>;
|
|
66
93
|
protected getSql(query: string, formatted: string, context?: LogContext): string;
|
package/AbstractSqlConnection.js
CHANGED
|
@@ -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
|
|
@@ -101,6 +101,18 @@ export class AbstractSqlConnection extends Connection {
|
|
|
101
101
|
await this.createKysely();
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Guards `getNativeClient()` overrides — drivers only capture their client while building their
|
|
106
|
+
* own dialect, which `driverOptions` carrying a ready-made Kysely instance or dialect skips.
|
|
107
|
+
*
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
requireNativeClient(client) {
|
|
111
|
+
if (client == null) {
|
|
112
|
+
throw new Error('The native client is not available, as it is owned by the Kysely instance or dialect passed via `driverOptions`. Access it through the object you provided there instead.');
|
|
113
|
+
}
|
|
114
|
+
return client;
|
|
115
|
+
}
|
|
104
116
|
/** Executes a callback within a transaction, committing on success and rolling back on error. */
|
|
105
117
|
async transactional(cb, options = {}) {
|
|
106
118
|
const trx = await this.begin(options);
|
|
@@ -110,7 +122,15 @@ export class AbstractSqlConnection extends Connection {
|
|
|
110
122
|
return ret;
|
|
111
123
|
}
|
|
112
124
|
catch (error) {
|
|
113
|
-
|
|
125
|
+
// A failing rollback must not mask why the transaction failed in the first place — the
|
|
126
|
+
// `'kill session'` abort strategy tears the connection down, so the rollback that follows can
|
|
127
|
+
// only ever report the dead connection.
|
|
128
|
+
try {
|
|
129
|
+
await this.rollback(trx, options.eventBroadcaster, options.loggerContext);
|
|
130
|
+
}
|
|
131
|
+
catch (rollbackError) {
|
|
132
|
+
this.logger.warn('query', `Failed to roll back transaction: ${rollbackError.message}`);
|
|
133
|
+
}
|
|
114
134
|
throw error;
|
|
115
135
|
}
|
|
116
136
|
}
|
|
@@ -138,22 +158,46 @@ export class AbstractSqlConnection extends Connection {
|
|
|
138
158
|
trxBuilder = trxBuilder.setAccessMode('read only');
|
|
139
159
|
}
|
|
140
160
|
const trx = await trxBuilder.execute();
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
ctx.index ??= 0;
|
|
144
|
-
const savepointName = `trx${ctx.index + 1}`;
|
|
145
|
-
Reflect.defineProperty(trx, 'index', { value: ctx.index + 1 });
|
|
146
|
-
Reflect.defineProperty(trx, 'savepointName', { value: savepointName });
|
|
147
|
-
this.logQuery(this.platform.getSavepointSQL(savepointName), options.loggerContext);
|
|
161
|
+
for (const query of this.platform.getBeginTransactionSQL(options)) {
|
|
162
|
+
this.logQuery(query, options.loggerContext);
|
|
148
163
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.
|
|
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;
|
|
152
173
|
}
|
|
153
174
|
}
|
|
154
175
|
await options.eventBroadcaster?.dispatchEvent(EventType.afterTransactionStart, trx);
|
|
155
176
|
return trx;
|
|
156
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
|
+
}
|
|
157
201
|
/** Commits the transaction or releases the savepoint. */
|
|
158
202
|
async commit(ctx, eventBroadcaster, loggerContext) {
|
|
159
203
|
if (ctx.isRolledBack) {
|
|
@@ -173,6 +217,7 @@ export class AbstractSqlConnection extends Connection {
|
|
|
173
217
|
/** Rolls back the transaction or rolls back to the savepoint. */
|
|
174
218
|
async rollback(ctx, eventBroadcaster, loggerContext) {
|
|
175
219
|
await eventBroadcaster?.dispatchEvent(EventType.beforeTransactionRollback, ctx);
|
|
220
|
+
await this.waitForIdleTransaction(ctx);
|
|
176
221
|
if ('savepointName' in ctx) {
|
|
177
222
|
await ctx.rollbackToSavepoint(ctx.savepointName).execute();
|
|
178
223
|
this.logQuery(this.platform.getRollbackToSavepointSQL(ctx.savepointName), loggerContext);
|
|
@@ -183,17 +228,32 @@ export class AbstractSqlConnection extends Connection {
|
|
|
183
228
|
}
|
|
184
229
|
await eventBroadcaster?.dispatchEvent(EventType.afterTransactionRollback, ctx);
|
|
185
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Waits until the transaction's connection has no query in flight. Kysely runs `rollback` straight
|
|
233
|
+
* on that connection instead of going through its connection provider, so a rollback caused by an
|
|
234
|
+
* aborted query would otherwise be sent while the aborted query is still running. That not only
|
|
235
|
+
* queues the rollback behind it on the server, it also overwrites the query id Kysely compares
|
|
236
|
+
* against before firing the `'cancel query'`/`'kill session'` control statement — the control
|
|
237
|
+
* statement is then discarded as stale and the abort never reaches the database.
|
|
238
|
+
*/
|
|
239
|
+
async waitForIdleTransaction(ctx) {
|
|
240
|
+
await ctx.getExecutor().provideConnection(async () => undefined);
|
|
241
|
+
}
|
|
186
242
|
prepareQuery(query, params = []) {
|
|
187
243
|
if (query instanceof NativeQueryBuilder) {
|
|
188
244
|
query = query.toRaw();
|
|
189
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
|
+
}
|
|
190
250
|
if (isRaw(query)) {
|
|
191
251
|
params = query.params;
|
|
192
252
|
query = query.sql;
|
|
193
253
|
}
|
|
194
254
|
query = this.config.get('onQuery')(query, params);
|
|
195
255
|
const formatted = this.platform.formatQuery(query, params);
|
|
196
|
-
return { query, params, formatted };
|
|
256
|
+
return { query, params: params, formatted };
|
|
197
257
|
}
|
|
198
258
|
/** Executes a SQL query and returns the result based on the method: `'all'` for rows, `'get'` for single row, `'run'` for affected count. */
|
|
199
259
|
async execute(query, params = [], method = 'all', ctx, loggerContext) {
|
|
@@ -227,7 +287,7 @@ export class AbstractSqlConnection extends Connection {
|
|
|
227
287
|
.stream(compiled, chunkSize ?? 100, abort ? { signal: abort.signal } : undefined);
|
|
228
288
|
this.logQuery(sql, {
|
|
229
289
|
sql,
|
|
230
|
-
params,
|
|
290
|
+
params: q.params,
|
|
231
291
|
...cleanCtx,
|
|
232
292
|
affected: Utils.isPlainObject(res) ? res.affectedRows : undefined,
|
|
233
293
|
});
|
|
@@ -238,7 +298,7 @@ export class AbstractSqlConnection extends Connection {
|
|
|
238
298
|
}
|
|
239
299
|
}
|
|
240
300
|
catch (e) {
|
|
241
|
-
this.logQuery(sql, { sql, params, ...cleanCtx, level: 'error' });
|
|
301
|
+
this.logQuery(sql, { sql, params: q.params, ...cleanCtx, level: 'error' });
|
|
242
302
|
throw e;
|
|
243
303
|
}
|
|
244
304
|
}
|
package/AbstractSqlDriver.d.ts
CHANGED
|
@@ -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
|
package/AbstractSqlDriver.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -604,7 +617,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
604
617
|
let pk;
|
|
605
618
|
if (meta.primaryKeys.length > 1) {
|
|
606
619
|
// owner has composite pk
|
|
607
|
-
pk = Utils.
|
|
620
|
+
pk = Utils.getOrderedPrimaryKeys(data, meta);
|
|
608
621
|
}
|
|
609
622
|
else {
|
|
610
623
|
/* v8 ignore next */
|
|
@@ -887,10 +900,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
887
900
|
}
|
|
888
901
|
const res = await this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options));
|
|
889
902
|
let pk;
|
|
890
|
-
/* v8 ignore next */
|
|
891
903
|
if (pks.length > 1) {
|
|
892
904
|
// owner has composite pk
|
|
893
|
-
pk = data.map(d => Utils.
|
|
905
|
+
pk = data.map(d => Utils.getOrderedPrimaryKeys(d, meta));
|
|
894
906
|
}
|
|
895
907
|
else {
|
|
896
908
|
res.row ??= {};
|
|
@@ -916,7 +928,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
916
928
|
if (!options.upsert && options.unionWhere?.length) {
|
|
917
929
|
where = (await this.applyUnionWhere(meta, where, options, true));
|
|
918
930
|
}
|
|
919
|
-
if (
|
|
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
|
|
920
936
|
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
921
937
|
qb.setAbortOptions(pickAbortOptions(options));
|
|
922
938
|
if (options.upsert) {
|
|
@@ -942,15 +958,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
942
958
|
qb.update(data).where(where);
|
|
943
959
|
// reload generated columns and version fields
|
|
944
960
|
const returning = [];
|
|
945
|
-
meta
|
|
961
|
+
this.getTableProps(meta)
|
|
946
962
|
.filter(prop => (prop.generated && !prop.primary) || prop.version)
|
|
947
963
|
.forEach(prop => returning.push(prop.name));
|
|
948
964
|
qb.returning(returning);
|
|
949
965
|
}
|
|
950
966
|
res = await this.rethrow(qb.execute('run', false));
|
|
951
967
|
}
|
|
952
|
-
|
|
953
|
-
const pk = pks.map(pk => Utils.extractPK(data[pk] || where, meta));
|
|
968
|
+
const pk = Utils.getOrderedPrimaryKeys({ ...where, ...data }, meta);
|
|
954
969
|
await this.processManyToMany(meta, pk, collections, true, options);
|
|
955
970
|
return res;
|
|
956
971
|
}
|
|
@@ -959,13 +974,38 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
959
974
|
options.convertCustomTypes ??= true;
|
|
960
975
|
const meta = this.metadata.get(entityName);
|
|
961
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
|
+
}
|
|
962
993
|
const uniqueFields = options.onConflictFields ??
|
|
963
994
|
(Utils.isPlainObject(where[0])
|
|
964
995
|
? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key))
|
|
965
996
|
: meta.primaryKeys);
|
|
966
997
|
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
967
998
|
qb.setAbortOptions(pickAbortOptions(options));
|
|
968
|
-
|
|
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
|
+
}
|
|
969
1009
|
qb.insert(data)
|
|
970
1010
|
.onConflict(uniqueFields)
|
|
971
1011
|
.returning(returning);
|
|
@@ -979,7 +1019,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
979
1019
|
if (options.onConflictWhere) {
|
|
980
1020
|
qb.where(options.onConflictWhere);
|
|
981
1021
|
}
|
|
982
|
-
|
|
1022
|
+
const res = await this.rethrow(qb.execute('run', false));
|
|
1023
|
+
return meta.inheritanceType === 'tpt' ? { ...res, row: undefined, rows: [] } : res;
|
|
983
1024
|
}
|
|
984
1025
|
const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
|
|
985
1026
|
const keys = new Set();
|
|
@@ -994,7 +1035,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
994
1035
|
}
|
|
995
1036
|
}
|
|
996
1037
|
// reload generated columns and version fields
|
|
997
|
-
meta.
|
|
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));
|
|
998
1042
|
const pkCond = Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames))
|
|
999
1043
|
.map(pk => `${this.platform.quoteIdentifier(pk)} = ?`)
|
|
1000
1044
|
.join(' and ');
|
|
@@ -1052,7 +1096,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1052
1096
|
return sql;
|
|
1053
1097
|
});
|
|
1054
1098
|
}
|
|
1055
|
-
if (meta.
|
|
1099
|
+
if (meta.ownsVersionProperty()) {
|
|
1056
1100
|
const versionProperty = meta.properties[meta.versionProperty];
|
|
1057
1101
|
const quotedFieldName = this.platform.quoteIdentifier(versionProperty.fieldNames[0]);
|
|
1058
1102
|
sql += `${quotedFieldName} = `;
|
|
@@ -1065,14 +1109,15 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1065
1109
|
sql += `, `;
|
|
1066
1110
|
}
|
|
1067
1111
|
sql = sql.substring(0, sql.length - 2) + ' where ';
|
|
1068
|
-
const pkProps = meta.primaryKeys.concat(...meta.
|
|
1112
|
+
const pkProps = meta.primaryKeys.concat(...meta.getOwnConcurrencyCheckKeys());
|
|
1069
1113
|
const pks = Utils.flatten(pkProps.map(pk => meta.properties[pk].fieldNames));
|
|
1070
1114
|
const useTupleIn = pks.length <= 1 || this.platform.allowsComparingTuples();
|
|
1071
1115
|
const condTemplate = useTupleIn
|
|
1072
1116
|
? `(${pks.map(() => '?').join(', ')})`
|
|
1073
1117
|
: `(${pks.map(pk => `${this.platform.quoteIdentifier(pk)} = ?`).join(' and ')})`;
|
|
1074
1118
|
const conds = where.map(cond => {
|
|
1075
|
-
|
|
1119
|
+
// with multiple PK columns the condition is looked up by property name, so it needs to stay an object
|
|
1120
|
+
if (pks.length === 1 && Utils.isPlainObject(cond) && Utils.getObjectKeysSize(cond) === 1) {
|
|
1076
1121
|
cond = Object.values(cond)[0];
|
|
1077
1122
|
}
|
|
1078
1123
|
if (pks.length > 1) {
|
|
@@ -1100,7 +1145,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1100
1145
|
sql += conds.join(' or ');
|
|
1101
1146
|
}
|
|
1102
1147
|
if (this.platform.usesReturningStatement() && returning.size > 0) {
|
|
1103
|
-
const returningFields = Utils.flatten([...returning].map(prop => meta.properties[prop].fieldNames));
|
|
1148
|
+
const returningFields = Utils.flatten([...returning].map(prop => (meta.properties[prop] ?? meta.root.properties[prop]).fieldNames));
|
|
1104
1149
|
/* v8 ignore next */
|
|
1105
1150
|
sql +=
|
|
1106
1151
|
returningFields.length > 0
|
|
@@ -1112,7 +1157,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1112
1157
|
}
|
|
1113
1158
|
const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options)));
|
|
1114
1159
|
for (let i = 0; i < collections.length; i++) {
|
|
1115
|
-
|
|
1160
|
+
const pk = Utils.getOrderedPrimaryKeys(where[i], meta);
|
|
1161
|
+
await this.processManyToMany(meta, pk, collections[i], false, options);
|
|
1116
1162
|
}
|
|
1117
1163
|
return res;
|
|
1118
1164
|
}
|
|
@@ -1300,7 +1346,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1300
1346
|
],
|
|
1301
1347
|
populateWhere: undefined,
|
|
1302
1348
|
_populateWhere: 'infer',
|
|
1303
|
-
populateFilter: this.wrapPopulateFilter(options,
|
|
1349
|
+
populateFilter: this.wrapPopulateFilter(options, pivotProp1.name),
|
|
1304
1350
|
};
|
|
1305
1351
|
if (pivotFindOptions._partitionLimit) {
|
|
1306
1352
|
pivotFindOptions._partitionLimit.partitionBy = pivotProp2.name;
|
|
@@ -1316,7 +1362,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1316
1362
|
}
|
|
1317
1363
|
}
|
|
1318
1364
|
}
|
|
1319
|
-
return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name);
|
|
1365
|
+
return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name, ownerMeta);
|
|
1320
1366
|
}
|
|
1321
1367
|
/**
|
|
1322
1368
|
* Load from a polymorphic M:N pivot table.
|
|
@@ -1337,10 +1383,15 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1337
1383
|
async loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp) {
|
|
1338
1384
|
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1339
1385
|
const targetMeta = prop.targetMeta;
|
|
1340
|
-
//
|
|
1386
|
+
// `prop.discriminator` spans all owner FK columns but carries no target metadata, so a composite
|
|
1387
|
+
// owner PK neither expands to a tuple condition nor gets mapped back; the virtual M:1 relation
|
|
1388
|
+
// to this discriminator's owner describes the same columns as an actual relation
|
|
1389
|
+
const ownerMeta = this.metadata.get(pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue]);
|
|
1390
|
+
const ownerProp = pivotMeta.properties[`${prop.discriminator}_${ownerMeta.tableName}`];
|
|
1391
|
+
// Build condition: discriminator = 'post' AND {owner} IN (...)
|
|
1341
1392
|
const cond = {
|
|
1342
1393
|
[prop.discriminatorColumn]: prop.discriminatorValue,
|
|
1343
|
-
[
|
|
1394
|
+
[ownerProp.name]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
|
|
1344
1395
|
};
|
|
1345
1396
|
if (!Utils.isEmpty(where)) {
|
|
1346
1397
|
cond[inverseProp.name] = { ...where };
|
|
@@ -1351,9 +1402,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1351
1402
|
const childExclude = !Utils.isEmpty(options?.exclude)
|
|
1352
1403
|
? options.exclude.map(f => `${inverseProp.name}.${f}`)
|
|
1353
1404
|
: [];
|
|
1405
|
+
// the owner relation is virtual, so its FK columns have to be selected via the pivot props that
|
|
1406
|
+
// cover them; only the first owner of a shared pivot gets the flat prop named after the
|
|
1407
|
+
// discriminator, and it keeps that owner's columns, so later owners get per-column props instead
|
|
1408
|
+
const ownerFields = Utils.unique(prop.joinColumns.map(col => (pivotMeta.properties[col] ? col : prop.discriminator)));
|
|
1354
1409
|
const fields = pivotJoin
|
|
1355
|
-
? [inverseProp.name,
|
|
1356
|
-
: [inverseProp.name,
|
|
1410
|
+
? [inverseProp.name, ...ownerFields, prop.discriminatorColumn]
|
|
1411
|
+
: [inverseProp.name, ...ownerFields, prop.discriminatorColumn, ...childFields];
|
|
1357
1412
|
const res = await this.find(pivotMeta.class, cond, {
|
|
1358
1413
|
ctx,
|
|
1359
1414
|
...options,
|
|
@@ -1374,7 +1429,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1374
1429
|
_populateWhere: 'infer',
|
|
1375
1430
|
populateFilter: this.wrapPopulateFilter(options, inverseProp.name),
|
|
1376
1431
|
});
|
|
1377
|
-
return this.buildPivotResultMap(owners, res,
|
|
1432
|
+
return this.buildPivotResultMap(owners, res, ownerProp.name, inverseProp.name, ownerMeta);
|
|
1378
1433
|
}
|
|
1379
1434
|
/**
|
|
1380
1435
|
* Load from inverse side of polymorphic M:N (e.g., Tag -> Posts)
|
|
@@ -1423,7 +1478,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1423
1478
|
_populateWhere: 'infer',
|
|
1424
1479
|
populateFilter: this.wrapPopulateFilter(options, ownerRelationName),
|
|
1425
1480
|
});
|
|
1426
|
-
return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
|
|
1481
|
+
return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName, tagProp.targetMeta);
|
|
1427
1482
|
}
|
|
1428
1483
|
/**
|
|
1429
1484
|
* Load a union-target polymorphic M:N pivot (e.g. Post.attachments -> Image | Video).
|
|
@@ -1519,19 +1574,23 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1519
1574
|
}
|
|
1520
1575
|
}
|
|
1521
1576
|
const result = orphanedRows.size > 0 ? pivotRows.filter(r => !orphanedRows.has(r)) : pivotRows;
|
|
1522
|
-
return this.buildPivotResultMap(owners, result, ownerProp.name, prop.discriminator);
|
|
1577
|
+
return this.buildPivotResultMap(owners, result, ownerProp.name, prop.discriminator, ownerMeta);
|
|
1523
1578
|
}
|
|
1524
1579
|
/**
|
|
1525
1580
|
* Build a map from owner PKs to their related entities from pivot table results.
|
|
1526
1581
|
*/
|
|
1527
|
-
buildPivotResultMap(owners, results, keyProp, valueProp) {
|
|
1582
|
+
buildPivotResultMap(owners, results, keyProp, valueProp, ownerMeta) {
|
|
1528
1583
|
const map = {};
|
|
1529
1584
|
for (const owner of owners) {
|
|
1530
1585
|
const key = Utils.getPrimaryKeyHash(owner);
|
|
1531
1586
|
map[key] = [];
|
|
1532
1587
|
}
|
|
1533
1588
|
for (const item of results) {
|
|
1534
|
-
const
|
|
1589
|
+
const fk = item[keyProp];
|
|
1590
|
+
// the owner PKs are always flat, while the pivot FK follows the owner PK structure,
|
|
1591
|
+
// so a PK built from a relation to another composite PK entity needs flattening too
|
|
1592
|
+
const pks = ownerMeta && fk != null ? Utils.getOrderedPrimaryKeys(fk, ownerMeta) : Utils.asArray(fk);
|
|
1593
|
+
const key = Utils.getPrimaryKeyHash(pks);
|
|
1535
1594
|
const entity = item[valueProp];
|
|
1536
1595
|
if (map[key]) {
|
|
1537
1596
|
map[key].push(entity);
|
|
@@ -1636,6 +1695,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1636
1695
|
if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
|
|
1637
1696
|
return false;
|
|
1638
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
|
+
}
|
|
1639
1703
|
if (strategy !== LoadStrategy.JOINED) {
|
|
1640
1704
|
// force joined strategy for explicit 1:1 owner populate hint as it would require a join anyway
|
|
1641
1705
|
return prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner;
|
|
@@ -1657,8 +1721,23 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1657
1721
|
const [propName, ref] = hint.field.split(':', 2);
|
|
1658
1722
|
return { propName, ref, children: hint.children };
|
|
1659
1723
|
});
|
|
1724
|
+
// with `fixedOrder` the pivot PK is the order column, which is not guaranteed to be unique when the
|
|
1725
|
+
// pivot table is managed externally, so we disambiguate the rows by their FKs on top of the PK
|
|
1726
|
+
// (including virtual ones, as the owner FK of a polymorphic pivot is only mapped via non-persisted relations)
|
|
1727
|
+
const pivotRelations = meta.pivotTable && !meta.compositePK ? meta.relations.filter(p => p.kind === ReferenceKind.MANY_TO_ONE) : [];
|
|
1660
1728
|
for (const item of rawResults) {
|
|
1661
|
-
|
|
1729
|
+
// flat hash, so nested composite PK values keep their own separators and cannot collide
|
|
1730
|
+
let pk = Utils.getCompositeKeyHash(item, meta, false, undefined, true);
|
|
1731
|
+
if (pivotRelations.length > 0) {
|
|
1732
|
+
pk = Utils.getPrimaryKeyHash([
|
|
1733
|
+
pk,
|
|
1734
|
+
...pivotRelations.flatMap(p => {
|
|
1735
|
+
const value = item[p.name];
|
|
1736
|
+
// composite FKs are mapped to an array of values, which `extractPK` does not accept
|
|
1737
|
+
return (Array.isArray(value) ? Utils.flatten(value, true) : Utils.extractPK(value, p.targetMeta));
|
|
1738
|
+
}),
|
|
1739
|
+
]);
|
|
1740
|
+
}
|
|
1662
1741
|
if (map[pk]) {
|
|
1663
1742
|
for (const { propName } of hints) {
|
|
1664
1743
|
if (!item[propName]) {
|
|
@@ -1751,7 +1830,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1751
1830
|
// INNER JOINs get nested inside the polymorphic LEFT JOIN by processNestedJoins, which
|
|
1752
1831
|
// keeps the resulting query valid for rows pointing to other polymorphic targets.
|
|
1753
1832
|
if (targetMeta.inheritanceType === 'tpt' && targetMeta.tptParent) {
|
|
1754
|
-
|
|
1833
|
+
qb.addTPTParentJoins(targetMeta, tableAlias, targetPath);
|
|
1755
1834
|
}
|
|
1756
1835
|
// For polymorphic targets that are TPT base classes, also LEFT JOIN
|
|
1757
1836
|
// all descendant tables so child-specific fields can be selected.
|
|
@@ -1799,10 +1878,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1799
1878
|
: JoinType.leftJoin;
|
|
1800
1879
|
const schema = prop.targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : prop.targetMeta.schema;
|
|
1801
1880
|
qb.join(field, tableAlias, {}, joinType, path, schema);
|
|
1802
|
-
// For relations to TPT child entities, INNER JOIN parent tables (GH #7469)
|
|
1803
|
-
if (meta2.inheritanceType === 'tpt' && meta2.tptParent) {
|
|
1804
|
-
this.addTPTParentJoinsForRelation(qb, meta2, tableAlias, path);
|
|
1805
|
-
}
|
|
1806
1881
|
// For relations to TPT base classes, add LEFT JOINs for all child tables (polymorphic loading)
|
|
1807
1882
|
if (meta2.inheritanceType === 'tpt' && meta2.tptChildren?.length && !ref) {
|
|
1808
1883
|
// Use the registry metadata to ensure allTPTDescendants is available
|
|
@@ -1849,25 +1924,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1849
1924
|
}
|
|
1850
1925
|
return fields;
|
|
1851
1926
|
}
|
|
1852
|
-
/**
|
|
1853
|
-
* Walks the TPT inheritance chain of `leafMeta` and INNER JOINs each parent table.
|
|
1854
|
-
* Registers the parent aliases in `qb.state.tptAlias` so column resolution finds them
|
|
1855
|
-
* when filter conditions reference parent-table columns.
|
|
1856
|
-
* @internal
|
|
1857
|
-
*/
|
|
1858
|
-
addTPTParentJoinsForRelation(qb, leafMeta, leafAlias, basePath) {
|
|
1859
|
-
let childAlias = leafAlias;
|
|
1860
|
-
let childMeta = leafMeta;
|
|
1861
|
-
while (childMeta.tptParent) {
|
|
1862
|
-
const parentMeta = childMeta.tptParent;
|
|
1863
|
-
const parentAlias = qb.getNextAlias(parentMeta.className);
|
|
1864
|
-
qb.createAlias(parentMeta.class, parentAlias);
|
|
1865
|
-
qb.state.tptAlias[`${leafAlias}:${parentMeta.className}`] = parentAlias;
|
|
1866
|
-
qb.addPropertyJoin(childMeta.tptParentProp, childAlias, parentAlias, JoinType.innerJoin, `${basePath}.[tpt]${childMeta.className}`);
|
|
1867
|
-
childAlias = parentAlias;
|
|
1868
|
-
childMeta = parentMeta;
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
1927
|
/**
|
|
1872
1928
|
* Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
|
|
1873
1929
|
* @internal
|
|
@@ -2087,7 +2143,20 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
2087
2143
|
const ret = {};
|
|
2088
2144
|
for (const prop of meta.relations) {
|
|
2089
2145
|
if (prop.kind === ReferenceKind.MANY_TO_MANY && data[prop.name]) {
|
|
2090
|
-
|
|
2146
|
+
// union targets are validated to have a single PK column, so a pivot row is always keyed
|
|
2147
|
+
// by exactly `[discriminator, pk]` - anything else cannot address a target table
|
|
2148
|
+
const discriminators = QueryHelper.isUnionTargetPolymorphic(prop)
|
|
2149
|
+
? Object.keys(prop.discriminatorMap)
|
|
2150
|
+
: undefined;
|
|
2151
|
+
ret[prop.name] = data[prop.name].map((item) => {
|
|
2152
|
+
const values = Utils.asArray(item);
|
|
2153
|
+
if (discriminators && !(values.length === 2 && discriminators.includes('' + values[0]))) {
|
|
2154
|
+
throw new Error(`Cannot resolve the discriminator value of ${meta.className}.${prop.name} from '${values.join(', ')}', ` +
|
|
2155
|
+
`as the same primary key can exist in any of the target tables. ` +
|
|
2156
|
+
`Pass the target as a [discriminator, ...primaryKey] tuple, e.g. ${JSON.stringify([discriminators[0], ...values])}.`);
|
|
2157
|
+
}
|
|
2158
|
+
return values;
|
|
2159
|
+
});
|
|
2091
2160
|
delete data[prop.name];
|
|
2092
2161
|
}
|
|
2093
2162
|
}
|