@mikro-orm/sql 7.2.0-dev.0 → 7.2.0-dev.10
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 +18 -2
- package/AbstractSqlConnection.js +43 -17
- package/AbstractSqlDriver.d.ts +2 -2
- package/AbstractSqlDriver.js +64 -21
- package/AbstractSqlPlatform.d.ts +7 -0
- package/AbstractSqlPlatform.js +23 -2
- package/SqlEntityManager.d.ts +2 -2
- package/SqlEntityManager.js +1 -2
- package/dialects/mssql/MsSqlNativeQueryBuilder.js +1 -1
- package/dialects/mysql/BaseMySqlPlatform.js +1 -1
- package/dialects/mysql/MySqlSchemaHelper.d.ts +1 -0
- package/dialects/mysql/MySqlSchemaHelper.js +7 -3
- package/dialects/oracledb/OracleNativeQueryBuilder.js +1 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +4 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +32 -5
- package/dialects/sqlite/BaseSqliteConnection.d.ts +3 -0
- package/dialects/sqlite/BaseSqliteConnection.js +15 -5
- package/dialects/sqlite/SqliteSchemaHelper.js +2 -2
- package/package.json +4 -4
- package/query/NativeQueryBuilder.js +1 -1
- package/query/QueryBuilder.d.ts +37 -19
- package/query/QueryBuilder.js +47 -17
- package/query/QueryBuilderHelper.js +6 -1
- package/schema/DatabaseTable.d.ts +2 -0
- package/schema/DatabaseTable.js +63 -18
- package/schema/SchemaComparator.d.ts +1 -0
- package/schema/SchemaComparator.js +29 -6
- package/schema/SchemaHelper.d.ts +20 -1
- package/schema/SchemaHelper.js +52 -9
- package/schema/SqlSchemaGenerator.d.ts +4 -0
- package/schema/SqlSchemaGenerator.js +51 -22
- package/typings.d.ts +2 -2
|
@@ -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;
|
|
@@ -56,11 +63,20 @@ export declare abstract class AbstractSqlConnection extends Connection {
|
|
|
56
63
|
commit(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
|
|
57
64
|
/** Rolls back the transaction or rolls back to the savepoint. */
|
|
58
65
|
rollback(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Waits until the transaction's connection has no query in flight. Kysely runs `rollback` straight
|
|
68
|
+
* on that connection instead of going through its connection provider, so a rollback caused by an
|
|
69
|
+
* aborted query would otherwise be sent while the aborted query is still running. That not only
|
|
70
|
+
* queues the rollback behind it on the server, it also overwrites the query id Kysely compares
|
|
71
|
+
* against before firing the `'cancel query'`/`'kill session'` control statement — the control
|
|
72
|
+
* statement is then discarded as stale and the abort never reaches the database.
|
|
73
|
+
*/
|
|
74
|
+
private waitForIdleTransaction;
|
|
59
75
|
private prepareQuery;
|
|
60
76
|
/** 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[]
|
|
77
|
+
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
78
|
/** 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[]
|
|
79
|
+
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
80
|
/** @inheritDoc */
|
|
65
81
|
executeDump(dump: string): Promise<void>;
|
|
66
82
|
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,18 +158,8 @@ 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);
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
for (const query of this.platform.getBeginTransactionSQL(options)) {
|
|
151
|
-
this.logQuery(query, options.loggerContext);
|
|
152
|
-
}
|
|
161
|
+
for (const query of this.platform.getBeginTransactionSQL(options)) {
|
|
162
|
+
this.logQuery(query, options.loggerContext);
|
|
153
163
|
}
|
|
154
164
|
await options.eventBroadcaster?.dispatchEvent(EventType.afterTransactionStart, trx);
|
|
155
165
|
return trx;
|
|
@@ -173,6 +183,7 @@ export class AbstractSqlConnection extends Connection {
|
|
|
173
183
|
/** Rolls back the transaction or rolls back to the savepoint. */
|
|
174
184
|
async rollback(ctx, eventBroadcaster, loggerContext) {
|
|
175
185
|
await eventBroadcaster?.dispatchEvent(EventType.beforeTransactionRollback, ctx);
|
|
186
|
+
await this.waitForIdleTransaction(ctx);
|
|
176
187
|
if ('savepointName' in ctx) {
|
|
177
188
|
await ctx.rollbackToSavepoint(ctx.savepointName).execute();
|
|
178
189
|
this.logQuery(this.platform.getRollbackToSavepointSQL(ctx.savepointName), loggerContext);
|
|
@@ -183,17 +194,32 @@ export class AbstractSqlConnection extends Connection {
|
|
|
183
194
|
}
|
|
184
195
|
await eventBroadcaster?.dispatchEvent(EventType.afterTransactionRollback, ctx);
|
|
185
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Waits until the transaction's connection has no query in flight. Kysely runs `rollback` straight
|
|
199
|
+
* on that connection instead of going through its connection provider, so a rollback caused by an
|
|
200
|
+
* aborted query would otherwise be sent while the aborted query is still running. That not only
|
|
201
|
+
* queues the rollback behind it on the server, it also overwrites the query id Kysely compares
|
|
202
|
+
* against before firing the `'cancel query'`/`'kill session'` control statement — the control
|
|
203
|
+
* statement is then discarded as stale and the abort never reaches the database.
|
|
204
|
+
*/
|
|
205
|
+
async waitForIdleTransaction(ctx) {
|
|
206
|
+
await ctx.getExecutor().provideConnection(async () => undefined);
|
|
207
|
+
}
|
|
186
208
|
prepareQuery(query, params = []) {
|
|
187
209
|
if (query instanceof NativeQueryBuilder) {
|
|
188
210
|
query = query.toRaw();
|
|
189
211
|
}
|
|
212
|
+
if (typeof query === 'string' && !Array.isArray(params)) {
|
|
213
|
+
// plain object params hold named parameters, translate them via the `raw()` helper
|
|
214
|
+
query = raw(query, params);
|
|
215
|
+
}
|
|
190
216
|
if (isRaw(query)) {
|
|
191
217
|
params = query.params;
|
|
192
218
|
query = query.sql;
|
|
193
219
|
}
|
|
194
220
|
query = this.config.get('onQuery')(query, params);
|
|
195
221
|
const formatted = this.platform.formatQuery(query, params);
|
|
196
|
-
return { query, params, formatted };
|
|
222
|
+
return { query, params: params, formatted };
|
|
197
223
|
}
|
|
198
224
|
/** Executes a SQL query and returns the result based on the method: `'all'` for rows, `'get'` for single row, `'run'` for affected count. */
|
|
199
225
|
async execute(query, params = [], method = 'all', ctx, loggerContext) {
|
|
@@ -227,7 +253,7 @@ export class AbstractSqlConnection extends Connection {
|
|
|
227
253
|
.stream(compiled, chunkSize ?? 100, abort ? { signal: abort.signal } : undefined);
|
|
228
254
|
this.logQuery(sql, {
|
|
229
255
|
sql,
|
|
230
|
-
params,
|
|
256
|
+
params: q.params,
|
|
231
257
|
...cleanCtx,
|
|
232
258
|
affected: Utils.isPlainObject(res) ? res.affectedRows : undefined,
|
|
233
259
|
});
|
|
@@ -238,7 +264,7 @@ export class AbstractSqlConnection extends Connection {
|
|
|
238
264
|
}
|
|
239
265
|
}
|
|
240
266
|
catch (e) {
|
|
241
|
-
this.logQuery(sql, { sql, params, ...cleanCtx, level: 'error' });
|
|
267
|
+
this.logQuery(sql, { sql, params: q.params, ...cleanCtx, level: 'error' });
|
|
242
268
|
throw e;
|
|
243
269
|
}
|
|
244
270
|
}
|
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
|
|
@@ -168,7 +168,7 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
168
168
|
protected extractManyToMany<T>(meta: EntityMetadata<T>, data: EntityDictionary<T>): EntityData<T>;
|
|
169
169
|
protected processManyToMany<T extends object>(meta: EntityMetadata<T>, pks: Primary<T>[], collections: EntityData<T>, clear: boolean, options?: DriverMethodOptions): Promise<void>;
|
|
170
170
|
lockPessimistic<T extends object>(entity: T, options: LockOptions): Promise<void>;
|
|
171
|
-
protected buildPopulateWhere<T extends object>(meta: EntityMetadata<T>, joinedProps: PopulateOptions<T>[], options: Pick<FindOptions<any>, 'populateWhere'>): ObjectQuery<T>;
|
|
171
|
+
protected buildPopulateWhere<T extends object>(meta: EntityMetadata<T>, joinedProps: PopulateOptions<T>[], options: Pick<FindOptions<any>, 'populateWhere' | 'strategy'>): ObjectQuery<T>;
|
|
172
172
|
/**
|
|
173
173
|
* Builds a UNION ALL (or UNION) subquery from `unionWhere` branches and merges it
|
|
174
174
|
* into the main WHERE as `pk IN (branch_1 UNION ALL branch_2 ...)`.
|
package/AbstractSqlDriver.js
CHANGED
|
@@ -604,7 +604,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
604
604
|
let pk;
|
|
605
605
|
if (meta.primaryKeys.length > 1) {
|
|
606
606
|
// owner has composite pk
|
|
607
|
-
pk = Utils.
|
|
607
|
+
pk = Utils.getOrderedPrimaryKeys(data, meta);
|
|
608
608
|
}
|
|
609
609
|
else {
|
|
610
610
|
/* v8 ignore next */
|
|
@@ -887,10 +887,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
887
887
|
}
|
|
888
888
|
const res = await this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options));
|
|
889
889
|
let pk;
|
|
890
|
-
/* v8 ignore next */
|
|
891
890
|
if (pks.length > 1) {
|
|
892
891
|
// owner has composite pk
|
|
893
|
-
pk = data.map(d => Utils.
|
|
892
|
+
pk = data.map(d => Utils.getOrderedPrimaryKeys(d, meta));
|
|
894
893
|
}
|
|
895
894
|
else {
|
|
896
895
|
res.row ??= {};
|
|
@@ -949,8 +948,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
949
948
|
}
|
|
950
949
|
res = await this.rethrow(qb.execute('run', false));
|
|
951
950
|
}
|
|
952
|
-
|
|
953
|
-
const pk = pks.map(pk => Utils.extractPK(data[pk] || where, meta));
|
|
951
|
+
const pk = Utils.getOrderedPrimaryKeys({ ...where, ...data }, meta);
|
|
954
952
|
await this.processManyToMany(meta, pk, collections, true, options);
|
|
955
953
|
return res;
|
|
956
954
|
}
|
|
@@ -1072,7 +1070,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1072
1070
|
? `(${pks.map(() => '?').join(', ')})`
|
|
1073
1071
|
: `(${pks.map(pk => `${this.platform.quoteIdentifier(pk)} = ?`).join(' and ')})`;
|
|
1074
1072
|
const conds = where.map(cond => {
|
|
1075
|
-
|
|
1073
|
+
// with multiple PK columns the condition is looked up by property name, so it needs to stay an object
|
|
1074
|
+
if (pks.length === 1 && Utils.isPlainObject(cond) && Utils.getObjectKeysSize(cond) === 1) {
|
|
1076
1075
|
cond = Object.values(cond)[0];
|
|
1077
1076
|
}
|
|
1078
1077
|
if (pks.length > 1) {
|
|
@@ -1100,7 +1099,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1100
1099
|
sql += conds.join(' or ');
|
|
1101
1100
|
}
|
|
1102
1101
|
if (this.platform.usesReturningStatement() && returning.size > 0) {
|
|
1103
|
-
const returningFields = Utils.flatten([...returning].map(prop => meta.properties[prop].fieldNames));
|
|
1102
|
+
const returningFields = Utils.flatten([...returning].map(prop => (meta.properties[prop] ?? meta.root.properties[prop]).fieldNames));
|
|
1104
1103
|
/* v8 ignore next */
|
|
1105
1104
|
sql +=
|
|
1106
1105
|
returningFields.length > 0
|
|
@@ -1112,7 +1111,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1112
1111
|
}
|
|
1113
1112
|
const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options)));
|
|
1114
1113
|
for (let i = 0; i < collections.length; i++) {
|
|
1115
|
-
|
|
1114
|
+
const pk = Utils.getOrderedPrimaryKeys(where[i], meta);
|
|
1115
|
+
await this.processManyToMany(meta, pk, collections[i], false, options);
|
|
1116
1116
|
}
|
|
1117
1117
|
return res;
|
|
1118
1118
|
}
|
|
@@ -1316,7 +1316,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1316
1316
|
}
|
|
1317
1317
|
}
|
|
1318
1318
|
}
|
|
1319
|
-
return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name);
|
|
1319
|
+
return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name, ownerMeta);
|
|
1320
1320
|
}
|
|
1321
1321
|
/**
|
|
1322
1322
|
* Load from a polymorphic M:N pivot table.
|
|
@@ -1337,10 +1337,15 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1337
1337
|
async loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp) {
|
|
1338
1338
|
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1339
1339
|
const targetMeta = prop.targetMeta;
|
|
1340
|
-
//
|
|
1340
|
+
// `prop.discriminator` spans all owner FK columns but carries no target metadata, so a composite
|
|
1341
|
+
// owner PK neither expands to a tuple condition nor gets mapped back; the virtual M:1 relation
|
|
1342
|
+
// to this discriminator's owner describes the same columns as an actual relation
|
|
1343
|
+
const ownerMeta = this.metadata.get(pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue]);
|
|
1344
|
+
const ownerProp = pivotMeta.properties[`${prop.discriminator}_${ownerMeta.tableName}`];
|
|
1345
|
+
// Build condition: discriminator = 'post' AND {owner} IN (...)
|
|
1341
1346
|
const cond = {
|
|
1342
1347
|
[prop.discriminatorColumn]: prop.discriminatorValue,
|
|
1343
|
-
[
|
|
1348
|
+
[ownerProp.name]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
|
|
1344
1349
|
};
|
|
1345
1350
|
if (!Utils.isEmpty(where)) {
|
|
1346
1351
|
cond[inverseProp.name] = { ...where };
|
|
@@ -1351,9 +1356,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1351
1356
|
const childExclude = !Utils.isEmpty(options?.exclude)
|
|
1352
1357
|
? options.exclude.map(f => `${inverseProp.name}.${f}`)
|
|
1353
1358
|
: [];
|
|
1359
|
+
// the owner relation is virtual, so its FK columns have to be selected via the pivot props that
|
|
1360
|
+
// cover them; only the first owner of a shared pivot gets the flat prop named after the
|
|
1361
|
+
// discriminator, and it keeps that owner's columns, so later owners get per-column props instead
|
|
1362
|
+
const ownerFields = Utils.unique(prop.joinColumns.map(col => (pivotMeta.properties[col] ? col : prop.discriminator)));
|
|
1354
1363
|
const fields = pivotJoin
|
|
1355
|
-
? [inverseProp.name,
|
|
1356
|
-
: [inverseProp.name,
|
|
1364
|
+
? [inverseProp.name, ...ownerFields, prop.discriminatorColumn]
|
|
1365
|
+
: [inverseProp.name, ...ownerFields, prop.discriminatorColumn, ...childFields];
|
|
1357
1366
|
const res = await this.find(pivotMeta.class, cond, {
|
|
1358
1367
|
ctx,
|
|
1359
1368
|
...options,
|
|
@@ -1374,7 +1383,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1374
1383
|
_populateWhere: 'infer',
|
|
1375
1384
|
populateFilter: this.wrapPopulateFilter(options, inverseProp.name),
|
|
1376
1385
|
});
|
|
1377
|
-
return this.buildPivotResultMap(owners, res,
|
|
1386
|
+
return this.buildPivotResultMap(owners, res, ownerProp.name, inverseProp.name, ownerMeta);
|
|
1378
1387
|
}
|
|
1379
1388
|
/**
|
|
1380
1389
|
* Load from inverse side of polymorphic M:N (e.g., Tag -> Posts)
|
|
@@ -1423,7 +1432,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1423
1432
|
_populateWhere: 'infer',
|
|
1424
1433
|
populateFilter: this.wrapPopulateFilter(options, ownerRelationName),
|
|
1425
1434
|
});
|
|
1426
|
-
return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
|
|
1435
|
+
return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName, tagProp.targetMeta);
|
|
1427
1436
|
}
|
|
1428
1437
|
/**
|
|
1429
1438
|
* Load a union-target polymorphic M:N pivot (e.g. Post.attachments -> Image | Video).
|
|
@@ -1519,19 +1528,23 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1519
1528
|
}
|
|
1520
1529
|
}
|
|
1521
1530
|
const result = orphanedRows.size > 0 ? pivotRows.filter(r => !orphanedRows.has(r)) : pivotRows;
|
|
1522
|
-
return this.buildPivotResultMap(owners, result, ownerProp.name, prop.discriminator);
|
|
1531
|
+
return this.buildPivotResultMap(owners, result, ownerProp.name, prop.discriminator, ownerMeta);
|
|
1523
1532
|
}
|
|
1524
1533
|
/**
|
|
1525
1534
|
* Build a map from owner PKs to their related entities from pivot table results.
|
|
1526
1535
|
*/
|
|
1527
|
-
buildPivotResultMap(owners, results, keyProp, valueProp) {
|
|
1536
|
+
buildPivotResultMap(owners, results, keyProp, valueProp, ownerMeta) {
|
|
1528
1537
|
const map = {};
|
|
1529
1538
|
for (const owner of owners) {
|
|
1530
1539
|
const key = Utils.getPrimaryKeyHash(owner);
|
|
1531
1540
|
map[key] = [];
|
|
1532
1541
|
}
|
|
1533
1542
|
for (const item of results) {
|
|
1534
|
-
const
|
|
1543
|
+
const fk = item[keyProp];
|
|
1544
|
+
// the owner PKs are always flat, while the pivot FK follows the owner PK structure,
|
|
1545
|
+
// so a PK built from a relation to another composite PK entity needs flattening too
|
|
1546
|
+
const pks = ownerMeta && fk != null ? Utils.getOrderedPrimaryKeys(fk, ownerMeta) : Utils.asArray(fk);
|
|
1547
|
+
const key = Utils.getPrimaryKeyHash(pks);
|
|
1535
1548
|
const entity = item[valueProp];
|
|
1536
1549
|
if (map[key]) {
|
|
1537
1550
|
map[key].push(entity);
|
|
@@ -1657,8 +1670,23 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1657
1670
|
const [propName, ref] = hint.field.split(':', 2);
|
|
1658
1671
|
return { propName, ref, children: hint.children };
|
|
1659
1672
|
});
|
|
1673
|
+
// with `fixedOrder` the pivot PK is the order column, which is not guaranteed to be unique when the
|
|
1674
|
+
// pivot table is managed externally, so we disambiguate the rows by their FKs on top of the PK
|
|
1675
|
+
// (including virtual ones, as the owner FK of a polymorphic pivot is only mapped via non-persisted relations)
|
|
1676
|
+
const pivotRelations = meta.pivotTable && !meta.compositePK ? meta.relations.filter(p => p.kind === ReferenceKind.MANY_TO_ONE) : [];
|
|
1660
1677
|
for (const item of rawResults) {
|
|
1661
|
-
|
|
1678
|
+
// flat hash, so nested composite PK values keep their own separators and cannot collide
|
|
1679
|
+
let pk = Utils.getCompositeKeyHash(item, meta, false, undefined, true);
|
|
1680
|
+
if (pivotRelations.length > 0) {
|
|
1681
|
+
pk = Utils.getPrimaryKeyHash([
|
|
1682
|
+
pk,
|
|
1683
|
+
...pivotRelations.flatMap(p => {
|
|
1684
|
+
const value = item[p.name];
|
|
1685
|
+
// composite FKs are mapped to an array of values, which `extractPK` does not accept
|
|
1686
|
+
return (Array.isArray(value) ? Utils.flatten(value, true) : Utils.extractPK(value, p.targetMeta));
|
|
1687
|
+
}),
|
|
1688
|
+
]);
|
|
1689
|
+
}
|
|
1662
1690
|
if (map[pk]) {
|
|
1663
1691
|
for (const { propName } of hints) {
|
|
1664
1692
|
if (!item[propName]) {
|
|
@@ -2087,7 +2115,20 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
2087
2115
|
const ret = {};
|
|
2088
2116
|
for (const prop of meta.relations) {
|
|
2089
2117
|
if (prop.kind === ReferenceKind.MANY_TO_MANY && data[prop.name]) {
|
|
2090
|
-
|
|
2118
|
+
// union targets are validated to have a single PK column, so a pivot row is always keyed
|
|
2119
|
+
// by exactly `[discriminator, pk]` - anything else cannot address a target table
|
|
2120
|
+
const discriminators = QueryHelper.isUnionTargetPolymorphic(prop)
|
|
2121
|
+
? Object.keys(prop.discriminatorMap)
|
|
2122
|
+
: undefined;
|
|
2123
|
+
ret[prop.name] = data[prop.name].map((item) => {
|
|
2124
|
+
const values = Utils.asArray(item);
|
|
2125
|
+
if (discriminators && !(values.length === 2 && discriminators.includes('' + values[0]))) {
|
|
2126
|
+
throw new Error(`Cannot resolve the discriminator value of ${meta.className}.${prop.name} from '${values.join(', ')}', ` +
|
|
2127
|
+
`as the same primary key can exist in any of the target tables. ` +
|
|
2128
|
+
`Pass the target as a [discriminator, ...primaryKey] tuple, e.g. ${JSON.stringify([discriminators[0], ...values])}.`);
|
|
2129
|
+
}
|
|
2130
|
+
return values;
|
|
2131
|
+
});
|
|
2091
2132
|
delete data[prop.name];
|
|
2092
2133
|
}
|
|
2093
2134
|
}
|
|
@@ -2124,7 +2165,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
2124
2165
|
if (hint.children) {
|
|
2125
2166
|
const targetMeta = prop.targetMeta;
|
|
2126
2167
|
if (targetMeta) {
|
|
2127
|
-
|
|
2168
|
+
// only joined children contribute to the ON conditions, the rest is handled by the entity loader
|
|
2169
|
+
const children = this.joinedProps(targetMeta, hint.children, options);
|
|
2170
|
+
const inner = this.buildPopulateWhere(targetMeta, children, { strategy: options.strategy });
|
|
2128
2171
|
if (!Utils.isEmpty(inner) || RawQueryFragment.hasObjectFragments(inner)) {
|
|
2129
2172
|
where[prop.name] ??= {};
|
|
2130
2173
|
Object.assign(where[prop.name], inner);
|
package/AbstractSqlPlatform.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
|
|
|
6
6
|
/** Base class for SQL database platforms, providing SQL generation and quoting utilities. */
|
|
7
7
|
export declare abstract class AbstractSqlPlatform extends Platform {
|
|
8
8
|
#private;
|
|
9
|
+
private static readonly ORDER_BY_DIRECTIONS;
|
|
9
10
|
protected readonly schemaHelper?: SchemaHelper;
|
|
10
11
|
usesPivotTable(): boolean;
|
|
11
12
|
indexForeignKeys(): boolean;
|
|
@@ -46,6 +47,12 @@ export declare abstract class AbstractSqlPlatform extends Platform {
|
|
|
46
47
|
* @internal
|
|
47
48
|
*/
|
|
48
49
|
getOrderByExpression(column: string, direction: string, collation?: string): string[];
|
|
50
|
+
/**
|
|
51
|
+
* `toLowerCase()` folds every `QueryOrder` enum member (and the normalized `QueryOrderNumeric`
|
|
52
|
+
* values) onto the six allow-listed directions, so only unknown values are rejected.
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
validateOrderByDirection(direction: string): string;
|
|
49
56
|
/**
|
|
50
57
|
* Quotes a collation name for use in COLLATE clauses.
|
|
51
58
|
* @internal
|
package/AbstractSqlPlatform.js
CHANGED
|
@@ -5,6 +5,14 @@ import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
|
|
|
5
5
|
/** Base class for SQL database platforms, providing SQL generation and quoting utilities. */
|
|
6
6
|
export class AbstractSqlPlatform extends Platform {
|
|
7
7
|
static #JSON_PROPERTY_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
8
|
+
static ORDER_BY_DIRECTIONS = new Set([
|
|
9
|
+
'asc',
|
|
10
|
+
'desc',
|
|
11
|
+
'asc nulls first',
|
|
12
|
+
'asc nulls last',
|
|
13
|
+
'desc nulls first',
|
|
14
|
+
'desc nulls last',
|
|
15
|
+
]);
|
|
8
16
|
schemaHelper;
|
|
9
17
|
usesPivotTable() {
|
|
10
18
|
return true;
|
|
@@ -115,10 +123,23 @@ export class AbstractSqlPlatform extends Platform {
|
|
|
115
123
|
* @internal
|
|
116
124
|
*/
|
|
117
125
|
getOrderByExpression(column, direction, collation) {
|
|
126
|
+
const dir = this.validateOrderByDirection(direction);
|
|
118
127
|
if (collation) {
|
|
119
|
-
return [`${column} collate ${this.quoteCollation(collation)} ${
|
|
128
|
+
return [`${column} collate ${this.quoteCollation(collation)} ${dir}`];
|
|
120
129
|
}
|
|
121
|
-
return [`${column} ${
|
|
130
|
+
return [`${column} ${dir}`];
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* `toLowerCase()` folds every `QueryOrder` enum member (and the normalized `QueryOrderNumeric`
|
|
134
|
+
* values) onto the six allow-listed directions, so only unknown values are rejected.
|
|
135
|
+
* @internal
|
|
136
|
+
*/
|
|
137
|
+
validateOrderByDirection(direction) {
|
|
138
|
+
const dir = ('' + direction).toLowerCase().trim();
|
|
139
|
+
if (!AbstractSqlPlatform.ORDER_BY_DIRECTIONS.has(dir)) {
|
|
140
|
+
throw new Error(`Invalid order direction: '${direction}'`);
|
|
141
|
+
}
|
|
142
|
+
return dir;
|
|
122
143
|
}
|
|
123
144
|
/**
|
|
124
145
|
* Quotes a collation name for use in COLLATE clauses.
|
package/SqlEntityManager.d.ts
CHANGED
|
@@ -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
|
*/
|
package/SqlEntityManager.js
CHANGED
|
@@ -75,8 +75,7 @@ export class SqlEntityManager extends EntityManager {
|
|
|
75
75
|
*/
|
|
76
76
|
async countBy(entityName, groupBy, options = {}) {
|
|
77
77
|
const em = this.getContext(false);
|
|
78
|
-
options =
|
|
79
|
-
em.prepareOptions(options);
|
|
78
|
+
options = em.prepareOptions(options);
|
|
80
79
|
const meta = em.getMetadata().find(entityName);
|
|
81
80
|
const fields = Utils.asArray(groupBy);
|
|
82
81
|
const { where: rawWhere, ...countOptions } = options;
|
|
@@ -168,7 +168,7 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
|
|
|
168
168
|
const fields = this.options.groupBy.map(field => this.quote(field));
|
|
169
169
|
this.parts.push(`group by ${fields.join(', ')}`);
|
|
170
170
|
}
|
|
171
|
-
if (this.options.having) {
|
|
171
|
+
if (this.options.having?.sql.trim()) {
|
|
172
172
|
this.parts.push(`having ${this.options.having.sql}`);
|
|
173
173
|
this.params.push(...this.options.having.params);
|
|
174
174
|
}
|
|
@@ -114,7 +114,7 @@ export class BaseMySqlPlatform extends AbstractSqlPlatform {
|
|
|
114
114
|
}
|
|
115
115
|
getOrderByExpression(column, direction, collation) {
|
|
116
116
|
const ret = [];
|
|
117
|
-
const dir =
|
|
117
|
+
const dir = this.validateOrderByDirection(direction);
|
|
118
118
|
const col = collation ? `${column} collate ${this.quoteCollation(collation)}` : column;
|
|
119
119
|
if (dir in this.ORDER_BY_NULLS_TRANSLATE) {
|
|
120
120
|
ret.push(`${col} ${this.ORDER_BY_NULLS_TRANSLATE[dir]}`);
|
|
@@ -53,6 +53,7 @@ export declare class MySqlSchemaHelper extends SchemaHelper {
|
|
|
53
53
|
getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
|
|
54
54
|
getRenameColumnSQL(tableName: string, oldColumnName: string, to: Column): string;
|
|
55
55
|
getRenameIndexSQL(tableName: string, index: IndexDef, oldIndexName: string): string[];
|
|
56
|
+
protected hasInlineColumnComment(): boolean;
|
|
56
57
|
getChangeColumnCommentSQL(tableName: string, to: Column, schemaName?: string): string;
|
|
57
58
|
alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[];
|
|
58
59
|
private getColumnDeclarationSQL;
|
|
@@ -54,10 +54,10 @@ export class MySqlSchemaHelper extends SchemaHelper {
|
|
|
54
54
|
return sql;
|
|
55
55
|
}
|
|
56
56
|
getListTablesSQL() {
|
|
57
|
-
return `select table_name as table_name, nullif(table_schema, schema()) as schema_name, table_comment as table_comment, table_collation as table_collation from information_schema.tables where table_type = 'BASE TABLE' and table_schema = schema()`;
|
|
57
|
+
return `select table_name as table_name, nullif(table_schema, schema()) as schema_name, table_comment as table_comment, table_collation as table_collation from information_schema.tables where table_type = 'BASE TABLE' and table_schema = schema() order by table_name`;
|
|
58
58
|
}
|
|
59
59
|
getListViewsSQL() {
|
|
60
|
-
return `select table_name as view_name, nullif(table_schema, schema()) as schema_name, view_definition from information_schema.views where table_schema = schema()`;
|
|
60
|
+
return `select table_name as view_name, nullif(table_schema, schema()) as schema_name, view_definition from information_schema.views where table_schema = schema() order by table_name`;
|
|
61
61
|
}
|
|
62
62
|
async loadViews(schema, connection, schemaName, ctx) {
|
|
63
63
|
const views = await connection.execute(this.getListViewsSQL(), [], 'all', ctx);
|
|
@@ -322,7 +322,7 @@ export class MySqlSchemaHelper extends SchemaHelper {
|
|
|
322
322
|
const ret = [];
|
|
323
323
|
for (const event of trigger.events) {
|
|
324
324
|
const name = trigger.events.length > 1 ? `${trigger.name}_${event}` : trigger.name;
|
|
325
|
-
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ROW begin ${trigger.body}
|
|
325
|
+
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ROW begin ${this.normalizeTriggerBody(trigger.body)} end`);
|
|
326
326
|
}
|
|
327
327
|
return ret.join(';\n');
|
|
328
328
|
}
|
|
@@ -372,6 +372,7 @@ export class MySqlSchemaHelper extends SchemaHelper {
|
|
|
372
372
|
from information_schema.routines r
|
|
373
373
|
where r.routine_schema = database()
|
|
374
374
|
and r.routine_type in ('PROCEDURE', 'FUNCTION')
|
|
375
|
+
order by r.routine_name
|
|
375
376
|
`;
|
|
376
377
|
const [rows, params] = await Promise.all([
|
|
377
378
|
connection.execute(sql),
|
|
@@ -539,6 +540,9 @@ export class MySqlSchemaHelper extends SchemaHelper {
|
|
|
539
540
|
const keyName = this.quote(index.keyName);
|
|
540
541
|
return [`alter table ${tableName} rename index ${oldIndexName} to ${keyName}`];
|
|
541
542
|
}
|
|
543
|
+
hasInlineColumnComment() {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
542
546
|
getChangeColumnCommentSQL(tableName, to, schemaName) {
|
|
543
547
|
tableName = this.quote(tableName);
|
|
544
548
|
const columnName = this.quote(to.name);
|
|
@@ -234,7 +234,7 @@ export class OracleNativeQueryBuilder extends NativeQueryBuilder {
|
|
|
234
234
|
const fields = this.options.groupBy.map(field => this.quote(field));
|
|
235
235
|
this.parts.push(`group by ${fields.join(', ')}`);
|
|
236
236
|
}
|
|
237
|
-
if (this.options.having) {
|
|
237
|
+
if (this.options.having?.sql.trim()) {
|
|
238
238
|
this.parts.push(`having ${this.options.having.sql}`);
|
|
239
239
|
this.params.push(...this.options.having.params);
|
|
240
240
|
}
|
|
@@ -69,6 +69,8 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
69
69
|
createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
70
70
|
/** Generates SQL to drop a PostgreSQL trigger and its associated function. */
|
|
71
71
|
dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
72
|
+
/** Flattens `;\n` inside the dollar-quoted blocks of a raw DDL expression, which are not statement boundaries. */
|
|
73
|
+
private flattenDollarQuotedBodies;
|
|
72
74
|
createRoutine(routine: SqlRoutineDef): string;
|
|
73
75
|
dropRoutine(routine: SqlRoutineDef): string;
|
|
74
76
|
getAllRoutines(connection: AbstractSqlConnection, schemas?: string[]): Promise<SqlRoutineDef[]>;
|
|
@@ -131,6 +133,8 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
131
133
|
* Build the column list for a PostgreSQL index.
|
|
132
134
|
*/
|
|
133
135
|
protected getIndexColumns(index: IndexDef): string;
|
|
136
|
+
/** Non-default index access methods (gin, gist, brin, hash, ...), normalized to lower case. */
|
|
137
|
+
getIndexAccessMethod(index: IndexDef): string;
|
|
134
138
|
/**
|
|
135
139
|
* PostgreSQL-specific index options like fill factor.
|
|
136
140
|
*/
|