@mikro-orm/sql 7.1.9-dev.8 → 7.1.9
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 +9 -0
- package/AbstractSqlConnection.js +23 -13
- package/AbstractSqlDriver.js +60 -20
- package/dialects/mssql/MsSqlNativeQueryBuilder.js +1 -1
- 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/PostgreSqlSchemaHelper.js +1 -1
- package/dialects/sqlite/SqliteSchemaHelper.js +1 -1
- package/package.json +3 -3
- package/query/NativeQueryBuilder.js +1 -1
- package/query/QueryBuilderHelper.js +2 -1
- package/schema/SchemaComparator.js +12 -2
- package/schema/SchemaHelper.d.ts +11 -1
- package/schema/SchemaHelper.js +31 -5
- package/schema/SqlSchemaGenerator.d.ts +4 -0
- package/schema/SqlSchemaGenerator.js +34 -6
|
@@ -56,6 +56,15 @@ export declare abstract class AbstractSqlConnection extends Connection {
|
|
|
56
56
|
commit(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
|
|
57
57
|
/** Rolls back the transaction or rolls back to the savepoint. */
|
|
58
58
|
rollback(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Waits until the transaction's connection has no query in flight. Kysely runs `rollback` straight
|
|
61
|
+
* on that connection instead of going through its connection provider, so a rollback caused by an
|
|
62
|
+
* aborted query would otherwise be sent while the aborted query is still running. That not only
|
|
63
|
+
* queues the rollback behind it on the server, it also overwrites the query id Kysely compares
|
|
64
|
+
* against before firing the `'cancel query'`/`'kill session'` control statement — the control
|
|
65
|
+
* statement is then discarded as stale and the abort never reaches the database.
|
|
66
|
+
*/
|
|
67
|
+
private waitForIdleTransaction;
|
|
59
68
|
private prepareQuery;
|
|
60
69
|
/** Executes a SQL query and returns the result based on the method: `'all'` for rows, `'get'` for single row, `'run'` for affected count. */
|
|
61
70
|
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>;
|
package/AbstractSqlConnection.js
CHANGED
|
@@ -110,7 +110,15 @@ export class AbstractSqlConnection extends Connection {
|
|
|
110
110
|
return ret;
|
|
111
111
|
}
|
|
112
112
|
catch (error) {
|
|
113
|
-
|
|
113
|
+
// A failing rollback must not mask why the transaction failed in the first place — the
|
|
114
|
+
// `'kill session'` abort strategy tears the connection down, so the rollback that follows can
|
|
115
|
+
// only ever report the dead connection.
|
|
116
|
+
try {
|
|
117
|
+
await this.rollback(trx, options.eventBroadcaster, options.loggerContext);
|
|
118
|
+
}
|
|
119
|
+
catch (rollbackError) {
|
|
120
|
+
this.logger.warn('query', `Failed to roll back transaction: ${rollbackError.message}`);
|
|
121
|
+
}
|
|
114
122
|
throw error;
|
|
115
123
|
}
|
|
116
124
|
}
|
|
@@ -138,18 +146,8 @@ export class AbstractSqlConnection extends Connection {
|
|
|
138
146
|
trxBuilder = trxBuilder.setAccessMode('read only');
|
|
139
147
|
}
|
|
140
148
|
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
|
-
}
|
|
149
|
+
for (const query of this.platform.getBeginTransactionSQL(options)) {
|
|
150
|
+
this.logQuery(query, options.loggerContext);
|
|
153
151
|
}
|
|
154
152
|
await options.eventBroadcaster?.dispatchEvent(EventType.afterTransactionStart, trx);
|
|
155
153
|
return trx;
|
|
@@ -173,6 +171,7 @@ export class AbstractSqlConnection extends Connection {
|
|
|
173
171
|
/** Rolls back the transaction or rolls back to the savepoint. */
|
|
174
172
|
async rollback(ctx, eventBroadcaster, loggerContext) {
|
|
175
173
|
await eventBroadcaster?.dispatchEvent(EventType.beforeTransactionRollback, ctx);
|
|
174
|
+
await this.waitForIdleTransaction(ctx);
|
|
176
175
|
if ('savepointName' in ctx) {
|
|
177
176
|
await ctx.rollbackToSavepoint(ctx.savepointName).execute();
|
|
178
177
|
this.logQuery(this.platform.getRollbackToSavepointSQL(ctx.savepointName), loggerContext);
|
|
@@ -183,6 +182,17 @@ export class AbstractSqlConnection extends Connection {
|
|
|
183
182
|
}
|
|
184
183
|
await eventBroadcaster?.dispatchEvent(EventType.afterTransactionRollback, ctx);
|
|
185
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Waits until the transaction's connection has no query in flight. Kysely runs `rollback` straight
|
|
187
|
+
* on that connection instead of going through its connection provider, so a rollback caused by an
|
|
188
|
+
* aborted query would otherwise be sent while the aborted query is still running. That not only
|
|
189
|
+
* queues the rollback behind it on the server, it also overwrites the query id Kysely compares
|
|
190
|
+
* against before firing the `'cancel query'`/`'kill session'` control statement — the control
|
|
191
|
+
* statement is then discarded as stale and the abort never reaches the database.
|
|
192
|
+
*/
|
|
193
|
+
async waitForIdleTransaction(ctx) {
|
|
194
|
+
await ctx.getExecutor().provideConnection(async () => undefined);
|
|
195
|
+
}
|
|
186
196
|
prepareQuery(query, params = []) {
|
|
187
197
|
if (query instanceof NativeQueryBuilder) {
|
|
188
198
|
query = query.toRaw();
|
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,22 @@ 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
|
+
let pk = Utils.getCompositeKeyHash(item, meta);
|
|
1679
|
+
if (pivotRelations.length > 0) {
|
|
1680
|
+
pk = Utils.getPrimaryKeyHash([
|
|
1681
|
+
pk,
|
|
1682
|
+
...pivotRelations.flatMap(p => {
|
|
1683
|
+
const value = item[p.name];
|
|
1684
|
+
// composite FKs are mapped to an array of values, which `extractPK` does not accept
|
|
1685
|
+
return (Array.isArray(value) ? value : Utils.extractPK(value, p.targetMeta));
|
|
1686
|
+
}),
|
|
1687
|
+
]);
|
|
1688
|
+
}
|
|
1662
1689
|
if (map[pk]) {
|
|
1663
1690
|
for (const { propName } of hints) {
|
|
1664
1691
|
if (!item[propName]) {
|
|
@@ -2087,7 +2114,20 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
2087
2114
|
const ret = {};
|
|
2088
2115
|
for (const prop of meta.relations) {
|
|
2089
2116
|
if (prop.kind === ReferenceKind.MANY_TO_MANY && data[prop.name]) {
|
|
2090
|
-
|
|
2117
|
+
// union targets are validated to have a single PK column, so a pivot row is always keyed
|
|
2118
|
+
// by exactly `[discriminator, pk]` - anything else cannot address a target table
|
|
2119
|
+
const discriminators = QueryHelper.isUnionTargetPolymorphic(prop)
|
|
2120
|
+
? Object.keys(prop.discriminatorMap)
|
|
2121
|
+
: undefined;
|
|
2122
|
+
ret[prop.name] = data[prop.name].map((item) => {
|
|
2123
|
+
const values = Utils.asArray(item);
|
|
2124
|
+
if (discriminators && !(values.length === 2 && discriminators.includes('' + values[0]))) {
|
|
2125
|
+
throw new Error(`Cannot resolve the discriminator value of ${meta.className}.${prop.name} from '${values.join(', ')}', ` +
|
|
2126
|
+
`as the same primary key can exist in any of the target tables. ` +
|
|
2127
|
+
`Pass the target as a [discriminator, ...primaryKey] tuple, e.g. ${JSON.stringify([discriminators[0], ...values])}.`);
|
|
2128
|
+
}
|
|
2129
|
+
return values;
|
|
2130
|
+
});
|
|
2091
2131
|
delete data[prop.name];
|
|
2092
2132
|
}
|
|
2093
2133
|
}
|
|
@@ -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
|
}
|
|
@@ -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;
|
|
@@ -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
|
}
|
|
@@ -540,6 +540,9 @@ export class MySqlSchemaHelper extends SchemaHelper {
|
|
|
540
540
|
const keyName = this.quote(index.keyName);
|
|
541
541
|
return [`alter table ${tableName} rename index ${oldIndexName} to ${keyName}`];
|
|
542
542
|
}
|
|
543
|
+
hasInlineColumnComment() {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
543
546
|
getChangeColumnCommentSQL(tableName, to, schemaName) {
|
|
544
547
|
tableName = this.quote(tableName);
|
|
545
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
|
}
|
|
@@ -513,7 +513,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
513
513
|
const when = trigger.when ? `\n when (${trigger.when})` : '';
|
|
514
514
|
const fnName = this.getSchemaQualifiedTriggerFnName(table, trigger);
|
|
515
515
|
const triggerName = this.platform.quoteIdentifier(trigger.name);
|
|
516
|
-
const fnSql = `create or replace function ${fnName}() returns trigger as $$ begin ${trigger.body}
|
|
516
|
+
const fnSql = `create or replace function ${fnName}() returns trigger as $$ begin ${this.normalizeTriggerBody(trigger.body)} end; $$ language plpgsql`;
|
|
517
517
|
const triggerSql = `create trigger ${triggerName} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} execute function ${fnName}()`;
|
|
518
518
|
return `${fnSql};\n${triggerSql}`;
|
|
519
519
|
}
|
|
@@ -561,7 +561,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
561
561
|
for (const event of trigger.events) {
|
|
562
562
|
const name = trigger.events.length > 1 ? `${trigger.name}_${event}` : trigger.name;
|
|
563
563
|
const when = trigger.when ? `\n when ${trigger.when}` : '';
|
|
564
|
-
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ${forEach}${when} begin ${trigger.body}
|
|
564
|
+
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ${forEach}${when} begin ${this.normalizeTriggerBody(trigger.body)} end`);
|
|
565
565
|
}
|
|
566
566
|
return ret.join(';\n');
|
|
567
567
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.1.9
|
|
3
|
+
"version": "7.1.9",
|
|
4
4
|
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data-mapper",
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"kysely": "0.29.4"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@mikro-orm/core": "^7.1.
|
|
53
|
+
"@mikro-orm/core": "^7.1.9"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.1.9
|
|
56
|
+
"@mikro-orm/core": "7.1.9"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
|
@@ -308,7 +308,7 @@ export class NativeQueryBuilder {
|
|
|
308
308
|
const fields = this.options.groupBy.map(field => this.quote(field));
|
|
309
309
|
this.parts.push(`group by ${fields.join(', ')}`);
|
|
310
310
|
}
|
|
311
|
-
if (this.options.having) {
|
|
311
|
+
if (this.options.having?.sql.trim()) {
|
|
312
312
|
this.parts.push(`having ${this.options.having.sql}`);
|
|
313
313
|
this.params.push(...this.options.having.params);
|
|
314
314
|
}
|
|
@@ -414,7 +414,8 @@ export class QueryBuilderHelper {
|
|
|
414
414
|
}
|
|
415
415
|
if (k === '$not') {
|
|
416
416
|
const res = this._appendQueryCondition(type, cond[k]);
|
|
417
|
-
|
|
417
|
+
// negating a vacuously true condition (e.g. an empty `$and`) matches nothing
|
|
418
|
+
parts.push(res.sql ? `not (${res.sql})` : '1 = 0');
|
|
418
419
|
res.params.forEach(p => params.push(p));
|
|
419
420
|
continue;
|
|
420
421
|
}
|
|
@@ -897,9 +897,13 @@ export class SchemaComparator {
|
|
|
897
897
|
// lookbehind: only strip a real charset introducer, never an underscore inside a literal like 'a_b'
|
|
898
898
|
?.replace(/(?<![\w'])_\w+'(.*?)'/g, '$1')
|
|
899
899
|
.replace(/!=/g, '<>')
|
|
900
|
-
|
|
900
|
+
// `\b` keeps this from firing inside identifiers like `min(...)`
|
|
901
|
+
.replace(/\bin\s*\((.*?)\)/gi, '= any (array[$1])')
|
|
901
902
|
// MySQL normalizes count(*) to count(0)
|
|
902
903
|
.replace(/\bcount\s*\(\s*0\s*\)/gi, 'count(*)')
|
|
904
|
+
// multi word type names in casts, the generic `::\w+` below only covers single word ones
|
|
905
|
+
// the precision is kept, so `timestamptz(3)` and `timestamp(3) with time zone` leave the same residue
|
|
906
|
+
.replace(/::\s*(?:character\s+varying|bit\s+varying|double\s+precision|(?:timestamp|time)\b(\s*\(\d+\))?(?:\s+with(?:out)?\s+time\s+zone)?)/gi, '$1')
|
|
903
907
|
// Remove quotes first so we can process identifiers
|
|
904
908
|
.replace(/['"`]/g, '')
|
|
905
909
|
// MySQL adds table/alias prefixes to columns (e.g., a.name or table_name.column vs just column)
|
|
@@ -907,12 +911,18 @@ export class SchemaComparator {
|
|
|
907
911
|
.replace(/\b\w+\.(\w+)/g, '$1')
|
|
908
912
|
// Normalize JOIN syntax: inner join -> join (equivalent in SQL)
|
|
909
913
|
.replace(/\binner\s+join\b/gi, 'join')
|
|
914
|
+
// PostgreSQL names an unaliased bare function call after the function itself,
|
|
915
|
+
// so `max(created_at)` comes back as `max(created_at) AS max`
|
|
916
|
+
// the lookahead skips table function column alias lists like `unnest(a) AS unnest(c)`, which are meaningful
|
|
917
|
+
.replace(/\b(\w+)\s*\(((?:[^()]|\([^()]*\))*)\)\s+as\s+\1\b(?!\s*\()/gi, '$1($2)')
|
|
910
918
|
// Remove redundant column aliases like `title AS title` -> `title`
|
|
911
919
|
.replace(/\b(\w+)\s+as\s+\1\b/gi, '$1')
|
|
912
920
|
// Remove AS keyword (optional in SQL, MySQL may add/remove it)
|
|
913
921
|
.replace(/\bas\b/gi, '')
|
|
914
922
|
// Remove remaining special chars, parentheses, type casts, asterisks, and normalize whitespace
|
|
915
|
-
|
|
923
|
+
// tabs and CRs included — the schema generator trims every line before executing the DDL,
|
|
924
|
+
// so indentation and CRLF endings can never come back from introspection
|
|
925
|
+
.replace(/[()\n\r\t[\]*]|::\w+| +/g, '')
|
|
916
926
|
.replace(/anyarray\[(.*)]/gi, '$1')
|
|
917
927
|
.toLowerCase()
|
|
918
928
|
// PostgreSQL adds default aliases to aggregate functions (e.g., count(*) AS count)
|
package/schema/SchemaHelper.d.ts
CHANGED
|
@@ -4,7 +4,13 @@ import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
|
|
|
4
4
|
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, SqlTriggerDef, SqlRoutineDef } from '../typings.js';
|
|
5
5
|
import type { DatabaseSchema } from './DatabaseSchema.js';
|
|
6
6
|
import type { DatabaseTable } from './DatabaseTable.js';
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Flattens `;\n` boundaries and drops blank lines so the schema-generator's statement splitter
|
|
9
|
+
* doesn't break the routine or trigger DDL apart — it treats both as statement/group separators.
|
|
10
|
+
* Blank lines go first, otherwise a `;` followed by one would keep its newline. Like
|
|
11
|
+
* `normalizeViewDefinition`, this is not string-literal aware, so a blank line inside a multi-line
|
|
12
|
+
* literal is dropped too. Other whitespace is preserved.
|
|
13
|
+
*/
|
|
8
14
|
export declare function stripStatementNewlines(body: string): string;
|
|
9
15
|
/**
|
|
10
16
|
* Strips SQL line comments and blank lines from a view definition. Comments are dropped by the
|
|
@@ -137,6 +143,8 @@ export declare abstract class SchemaHelper {
|
|
|
137
143
|
}[];
|
|
138
144
|
}[], safe: boolean): string[];
|
|
139
145
|
getChangeColumnCommentSQL(tableName: string, to: Column, schemaName?: string): string;
|
|
146
|
+
/** Whether the column comment is part of the column declaration, as opposed to a separate statement. */
|
|
147
|
+
protected hasInlineColumnComment(): boolean;
|
|
140
148
|
getNamespaces(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string[]>;
|
|
141
149
|
protected mapIndexes(indexes: IndexDef[]): Promise<IndexDef[]>;
|
|
142
150
|
mapForeignKeys(fks: any[], tableName: string, schemaName?: string): Dictionary;
|
|
@@ -176,6 +184,8 @@ export declare abstract class SchemaHelper {
|
|
|
176
184
|
createRoutine(_routine: SqlRoutineDef): string;
|
|
177
185
|
dropRoutine(_routine: SqlRoutineDef): string;
|
|
178
186
|
getAllRoutines(_connection: AbstractSqlConnection, _schemas?: string[]): Promise<SqlRoutineDef[]>;
|
|
187
|
+
/** Flattens internal `;\n` so the statement splitter doesn't tear the DDL, and ensures exactly one trailing `;` for the enclosing `begin ... end` block. */
|
|
188
|
+
protected normalizeTriggerBody(body: string): string;
|
|
179
189
|
/** Wraps the body in `BEGIN ... END` if not already, and flattens internal `;\n` so the schema-generator's statement splitter doesn't tear the DDL. */
|
|
180
190
|
protected wrapRoutineBody(body: string): string;
|
|
181
191
|
protected stripRoutineBody(body: string): string;
|
package/schema/SchemaHelper.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { isRaw, Utils, } from '@mikro-orm/core';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Flattens `;\n` boundaries and drops blank lines so the schema-generator's statement splitter
|
|
4
|
+
* doesn't break the routine or trigger DDL apart — it treats both as statement/group separators.
|
|
5
|
+
* Blank lines go first, otherwise a `;` followed by one would keep its newline. Like
|
|
6
|
+
* `normalizeViewDefinition`, this is not string-literal aware, so a blank line inside a multi-line
|
|
7
|
+
* literal is dropped too. Other whitespace is preserved.
|
|
8
|
+
*/
|
|
3
9
|
export function stripStatementNewlines(body) {
|
|
4
|
-
return body
|
|
10
|
+
return body
|
|
11
|
+
.split('\n')
|
|
12
|
+
.filter(line => line.trim() !== '')
|
|
13
|
+
.join('\n')
|
|
14
|
+
.replace(/;[\t ]*\r?\n/g, '; ');
|
|
5
15
|
}
|
|
6
16
|
/**
|
|
7
17
|
* Strips SQL line comments and blank lines from a view definition. Comments are dropped by the
|
|
@@ -436,7 +446,8 @@ export class SchemaHelper {
|
|
|
436
446
|
this.append(ret, this.alterTableColumn(column, diff.fromTable, changedProperties));
|
|
437
447
|
}
|
|
438
448
|
for (const { column, changedProperties } of Object.values(diff.changedColumns).filter(diff => diff.changedProperties.has('comment'))) {
|
|
439
|
-
if (
|
|
449
|
+
if (this.hasInlineColumnComment() &&
|
|
450
|
+
['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems', 'collation'].some(t => changedProperties.has(t))) {
|
|
440
451
|
continue; // will be handled via column update
|
|
441
452
|
}
|
|
442
453
|
ret.push(this.getChangeColumnCommentSQL(tableName, column, schemaName));
|
|
@@ -489,7 +500,13 @@ export class SchemaHelper {
|
|
|
489
500
|
return `add ${this.createTableColumn(column, table)}`;
|
|
490
501
|
})
|
|
491
502
|
.join(', ');
|
|
492
|
-
|
|
503
|
+
const ret = [`alter table ${table.getQuotedName()} ${adds}`];
|
|
504
|
+
if (!this.hasInlineColumnComment()) {
|
|
505
|
+
for (const column of columns.filter(column => column.comment)) {
|
|
506
|
+
ret.push(this.getChangeColumnCommentSQL(table.name, column, table.schema));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return ret;
|
|
493
510
|
}
|
|
494
511
|
getDropColumnsSQL(tableName, columns, schemaName) {
|
|
495
512
|
const name = this.quote(this.getTableName(tableName, schemaName));
|
|
@@ -608,6 +625,10 @@ export class SchemaHelper {
|
|
|
608
625
|
getChangeColumnCommentSQL(tableName, to, schemaName) {
|
|
609
626
|
return '';
|
|
610
627
|
}
|
|
628
|
+
/** Whether the column comment is part of the column declaration, as opposed to a separate statement. */
|
|
629
|
+
hasInlineColumnComment() {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
611
632
|
async getNamespaces(connection, ctx) {
|
|
612
633
|
return [];
|
|
613
634
|
}
|
|
@@ -861,7 +882,7 @@ export class SchemaHelper {
|
|
|
861
882
|
const events = trigger.events.map(e => e.toUpperCase()).join(' OR ');
|
|
862
883
|
const forEach = trigger.forEach === 'statement' ? 'STATEMENT' : 'ROW';
|
|
863
884
|
const when = trigger.when ? ` when (${trigger.when})` : '';
|
|
864
|
-
return `create trigger ${this.quote(trigger.name)} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} begin ${trigger.body}
|
|
885
|
+
return `create trigger ${this.quote(trigger.name)} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} begin ${this.normalizeTriggerBody(trigger.body)} end`;
|
|
865
886
|
}
|
|
866
887
|
/**
|
|
867
888
|
* Generates SQL to drop a database trigger from a table.
|
|
@@ -885,6 +906,11 @@ export class SchemaHelper {
|
|
|
885
906
|
async getAllRoutines(_connection, _schemas = []) {
|
|
886
907
|
return [];
|
|
887
908
|
}
|
|
909
|
+
/** Flattens internal `;\n` so the statement splitter doesn't tear the DDL, and ensures exactly one trailing `;` for the enclosing `begin ... end` block. */
|
|
910
|
+
normalizeTriggerBody(body) {
|
|
911
|
+
const trimmed = stripStatementNewlines(body).trim();
|
|
912
|
+
return /;\s*$/.test(trimmed) ? trimmed : `${trimmed};`;
|
|
913
|
+
}
|
|
888
914
|
/** Wraps the body in `BEGIN ... END` if not already, and flattens internal `;\n` so the schema-generator's statement splitter doesn't tear the DDL. */
|
|
889
915
|
wrapRoutineBody(body) {
|
|
890
916
|
const trimmed = stripStatementNewlines(body).trim();
|
|
@@ -54,6 +54,10 @@ export declare class SqlSchemaGenerator extends AbstractSchemaGenerator<Abstract
|
|
|
54
54
|
wrap?: boolean;
|
|
55
55
|
ctx?: Transaction;
|
|
56
56
|
}): Promise<void>;
|
|
57
|
+
/** Splits the SQL on the separator, keeping the separators that fall inside a string literal. */
|
|
58
|
+
private splitOutsideLiterals;
|
|
59
|
+
/** Whether the statement has to be the first one in a query batch, e.g. `create trigger` on MSSQL. */
|
|
60
|
+
protected startsBatch(_statement: string): boolean;
|
|
57
61
|
dropTableIfExists(name: string, schema?: string): Promise<void>;
|
|
58
62
|
private wrapSchema;
|
|
59
63
|
private append;
|
|
@@ -439,18 +439,23 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
439
439
|
}
|
|
440
440
|
async execute(sql, options = {}) {
|
|
441
441
|
options.wrap ??= false;
|
|
442
|
-
const lines = this.wrapSchema(sql, options)
|
|
442
|
+
const lines = this.splitOutsideLiterals(this.wrapSchema(sql, options), '\n');
|
|
443
443
|
const groups = [];
|
|
444
444
|
let i = 0;
|
|
445
445
|
for (const line of lines) {
|
|
446
|
-
|
|
446
|
+
const stmt = line.trim();
|
|
447
|
+
if (stmt === '') {
|
|
447
448
|
if (groups[i]?.length > 0) {
|
|
448
449
|
i++;
|
|
449
450
|
}
|
|
450
451
|
continue;
|
|
451
452
|
}
|
|
453
|
+
// same boundary an empty line creates, for statements that have to start their own batch
|
|
454
|
+
if (groups[i]?.length > 0 && this.startsBatch(stmt)) {
|
|
455
|
+
i++;
|
|
456
|
+
}
|
|
452
457
|
groups[i] ??= [];
|
|
453
|
-
groups[i].push(
|
|
458
|
+
groups[i].push(stmt);
|
|
454
459
|
}
|
|
455
460
|
if (groups.length === 0) {
|
|
456
461
|
return;
|
|
@@ -463,14 +468,37 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
463
468
|
return;
|
|
464
469
|
}
|
|
465
470
|
const statements = groups.flatMap(group => {
|
|
466
|
-
return group
|
|
467
|
-
.join('\n')
|
|
468
|
-
.split(';\n')
|
|
471
|
+
return this.splitOutsideLiterals(group.join('\n'), ';\n')
|
|
469
472
|
.map(s => s.trim())
|
|
470
473
|
.filter(s => s);
|
|
471
474
|
});
|
|
472
475
|
await Utils.runSerial(statements, stmt => this.driver.execute(stmt));
|
|
473
476
|
}
|
|
477
|
+
/** Splits the SQL on the separator, keeping the separators that fall inside a string literal. */
|
|
478
|
+
splitOutsideLiterals(sql, separator) {
|
|
479
|
+
const [idOpen, idClose] = this.platform.quoteIdentifier('');
|
|
480
|
+
// mysql escapes quotes as `\'`, the other dialects double them, which pairs up on its own
|
|
481
|
+
const esc = this.platform.quoteValue(`'`).includes(`\\'`) ? '\\\\.|' : '';
|
|
482
|
+
// complete literals, quoted identifiers and `--` comments, so that an apostrophe inside an
|
|
483
|
+
// identifier or a comment is not mistaken for one opening a literal
|
|
484
|
+
const tokens = new RegExp(`'(?:${esc}[^'])*'|\\${idOpen}[^\\${idClose}]*\\${idClose}|--[^\n]*`, 'g');
|
|
485
|
+
const parts = [];
|
|
486
|
+
for (const chunk of sql.split(separator)) {
|
|
487
|
+
const prev = parts.at(-1);
|
|
488
|
+
// whatever quote is left once the complete tokens are gone opened a literal we are still inside of
|
|
489
|
+
if (prev?.replace(tokens, '').includes(`'`)) {
|
|
490
|
+
parts[parts.length - 1] = prev + separator + chunk;
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
parts.push(chunk);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return parts;
|
|
497
|
+
}
|
|
498
|
+
/** Whether the statement has to be the first one in a query batch, e.g. `create trigger` on MSSQL. */
|
|
499
|
+
startsBatch(_statement) {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
474
502
|
async dropTableIfExists(name, schema) {
|
|
475
503
|
const sql = this.helper.dropTableIfExists(name, schema);
|
|
476
504
|
return this.execute(sql);
|