@mikro-orm/sql 7.2.0-dev.3 → 7.2.0-dev.5
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 +61 -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 +2 -2
- package/dialects/sqlite/BaseSqliteConnection.d.ts +3 -0
- package/dialects/sqlite/BaseSqliteConnection.js +15 -5
- package/dialects/sqlite/SqliteSchemaHelper.js +1 -1
- package/package.json +3 -3
- package/query/NativeQueryBuilder.js +1 -1
- package/query/QueryBuilder.d.ts +11 -0
- package/query/QueryBuilder.js +44 -16
- package/query/QueryBuilderHelper.js +6 -1
- package/schema/DatabaseTable.d.ts +2 -0
- package/schema/DatabaseTable.js +57 -16
- package/schema/SchemaComparator.js +12 -2
- package/schema/SchemaHelper.d.ts +13 -1
- package/schema/SchemaHelper.js +37 -7
- package/schema/SqlSchemaGenerator.d.ts +4 -0
- package/schema/SqlSchemaGenerator.js +34 -6
- package/typings.d.ts +2 -2
|
@@ -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,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
|
}
|
|
@@ -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
|
}
|
|
@@ -492,7 +492,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
492
492
|
// SchemaHelper.createCheck).
|
|
493
493
|
const m = /^check \(\((.*)\)\)$/is.exec(check.expression);
|
|
494
494
|
const single = m ? null : /^check \((.*)\)$/is.exec(check.expression);
|
|
495
|
-
const def = m ? m[1].replace(/\((
|
|
495
|
+
const def = m ? m[1].replace(/\(([^()]*)\)::\w+/g, '$1') : single ? single[1] : check.expression;
|
|
496
496
|
ret[key].push({
|
|
497
497
|
name: check.name,
|
|
498
498
|
columnName: check.column_name,
|
|
@@ -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
|
}
|
|
@@ -7,4 +7,7 @@ export declare class BaseSqliteConnection extends AbstractSqlConnection {
|
|
|
7
7
|
skipOnConnect?: boolean;
|
|
8
8
|
}): Promise<void>;
|
|
9
9
|
protected attachDatabases(): Promise<void>;
|
|
10
|
+
/** Per-connection state, lost whenever the underlying connection is recreated, so it has to be replayed. */
|
|
11
|
+
protected getConnectionSetupSql(): Promise<string[]>;
|
|
12
|
+
private getAttachDatabasesSql;
|
|
10
13
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CompiledQuery } from 'kysely';
|
|
2
2
|
import { AbstractSqlConnection } from '../../AbstractSqlConnection.js';
|
|
3
|
+
const FOREIGN_KEYS_PRAGMA = 'pragma foreign_keys = on';
|
|
3
4
|
export class BaseSqliteConnection extends AbstractSqlConnection {
|
|
4
5
|
createKyselyDialect(options) {
|
|
5
6
|
throw new Error('No SQLite dialect configured. Pass a Kysely dialect via the `driverOptions` config option, ' +
|
|
@@ -7,19 +8,28 @@ export class BaseSqliteConnection extends AbstractSqlConnection {
|
|
|
7
8
|
}
|
|
8
9
|
async connect(options) {
|
|
9
10
|
await super.connect(options);
|
|
10
|
-
await this.getClient().executeQuery(CompiledQuery.raw(
|
|
11
|
+
await this.getClient().executeQuery(CompiledQuery.raw(FOREIGN_KEYS_PRAGMA));
|
|
11
12
|
await this.attachDatabases();
|
|
12
13
|
}
|
|
13
14
|
async attachDatabases() {
|
|
15
|
+
for (const sql of await this.getAttachDatabasesSql()) {
|
|
16
|
+
await this.execute(sql);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Per-connection state, lost whenever the underlying connection is recreated, so it has to be replayed. */
|
|
20
|
+
async getConnectionSetupSql() {
|
|
21
|
+
return [FOREIGN_KEYS_PRAGMA, ...(await this.getAttachDatabasesSql())];
|
|
22
|
+
}
|
|
23
|
+
async getAttachDatabasesSql() {
|
|
14
24
|
const attachDatabases = this.config.get('attachDatabases');
|
|
15
25
|
if (!attachDatabases?.length) {
|
|
16
|
-
return;
|
|
26
|
+
return [];
|
|
17
27
|
}
|
|
18
28
|
const { fs } = await import('@mikro-orm/core/fs-utils');
|
|
19
29
|
const baseDir = this.config.get('baseDir');
|
|
20
|
-
|
|
30
|
+
return attachDatabases.map(db => {
|
|
21
31
|
const path = fs.absolutePath(db.path, baseDir);
|
|
22
|
-
|
|
23
|
-
}
|
|
32
|
+
return `attach database '${path}' as ${this.platform.quoteIdentifier(db.name)}`;
|
|
33
|
+
});
|
|
24
34
|
}
|
|
25
35
|
}
|
|
@@ -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.2.0-dev.
|
|
3
|
+
"version": "7.2.0-dev.5",
|
|
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.11"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.2.0-dev.
|
|
56
|
+
"@mikro-orm/core": "7.2.0-dev.5"
|
|
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
|
}
|
package/query/QueryBuilder.d.ts
CHANGED
|
@@ -506,6 +506,17 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
506
506
|
* @internal
|
|
507
507
|
*/
|
|
508
508
|
applyJoinedFilters(em: EntityManager, filterOptions: FilterOptions | undefined): Promise<void>;
|
|
509
|
+
/**
|
|
510
|
+
* The `on` clause of `condJoin` — its explicit join condition or a filter condition merged into
|
|
511
|
+
* it — can reference the alias of any join in its subtree, both auto-joins created while
|
|
512
|
+
* processing the condition and pre-existing joined paths, all of which render after `condJoin`
|
|
513
|
+
* and would be forward alias references (issues #7681, #8090, #8099). When that happens, fold the
|
|
514
|
+
* subtree into `condJoin`, so it renders as a single parenthesized join group and every alias
|
|
515
|
+
* shares the scope of the outer `on` clause.
|
|
516
|
+
*/
|
|
517
|
+
private nestReferencedJoins;
|
|
518
|
+
private getJoinSubtree;
|
|
519
|
+
private condReferencesAlias;
|
|
509
520
|
withSubQuery(subQuery: RawQueryFragment | NativeQueryBuilder, alias: string): this;
|
|
510
521
|
/**
|
|
511
522
|
* Adds a WHERE clause to the query using an object condition.
|
package/query/QueryBuilder.js
CHANGED
|
@@ -433,6 +433,7 @@ export class QueryBuilder {
|
|
|
433
433
|
else {
|
|
434
434
|
join.cond = { ...cond };
|
|
435
435
|
}
|
|
436
|
+
this.nestReferencedJoins(join);
|
|
436
437
|
// For polymorphic LEFT JOIN filters, add a WHERE condition to enforce the filter
|
|
437
438
|
// only for rows matching this target's discriminator value. This ensures rows pointing
|
|
438
439
|
// to other polymorphic targets are not excluded.
|
|
@@ -456,6 +457,45 @@ export class QueryBuilder {
|
|
|
456
457
|
}
|
|
457
458
|
}
|
|
458
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* The `on` clause of `condJoin` — its explicit join condition or a filter condition merged into
|
|
462
|
+
* it — can reference the alias of any join in its subtree, both auto-joins created while
|
|
463
|
+
* processing the condition and pre-existing joined paths, all of which render after `condJoin`
|
|
464
|
+
* and would be forward alias references (issues #7681, #8090, #8099). When that happens, fold the
|
|
465
|
+
* subtree into `condJoin`, so it renders as a single parenthesized join group and every alias
|
|
466
|
+
* shares the scope of the outer `on` clause.
|
|
467
|
+
*/
|
|
468
|
+
nestReferencedJoins(condJoin) {
|
|
469
|
+
// m:n pivot joins might not have the target join entry created
|
|
470
|
+
if (!condJoin) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const subtree = this.getJoinSubtree(condJoin);
|
|
474
|
+
if (!subtree.some(j => this.condReferencesAlias(condJoin.cond, j.alias))) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
for (const j of subtree) {
|
|
478
|
+
const parent = j.ownerAlias === condJoin.alias ? condJoin : subtree.find(p => p.alias === j.ownerAlias);
|
|
479
|
+
if (!parent.nested?.has(j)) {
|
|
480
|
+
const nested = (parent.nested ??= new Set());
|
|
481
|
+
j.type = j.type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
|
|
482
|
+
nested.add(j);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
getJoinSubtree(join) {
|
|
487
|
+
const children = Object.values(this.#state.joins).filter(j => j !== join && j.ownerAlias === join.alias);
|
|
488
|
+
return children.flatMap(j => [j, ...this.getJoinSubtree(j)]);
|
|
489
|
+
}
|
|
490
|
+
condReferencesAlias(cond, alias) {
|
|
491
|
+
if (Array.isArray(cond)) {
|
|
492
|
+
return cond.some(c => this.condReferencesAlias(c, alias));
|
|
493
|
+
}
|
|
494
|
+
if (Utils.isPlainObject(cond)) {
|
|
495
|
+
return Object.entries(cond).some(([key, value]) => key.startsWith(`${alias}.`) || this.condReferencesAlias(value, alias));
|
|
496
|
+
}
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
459
499
|
withSubQuery(subQuery, alias) {
|
|
460
500
|
this.ensureNotFinalized();
|
|
461
501
|
if (isRaw(subQuery)) {
|
|
@@ -1104,7 +1144,9 @@ export class QueryBuilder {
|
|
|
1104
1144
|
}
|
|
1105
1145
|
if (stack.length > 0) {
|
|
1106
1146
|
const merged = this.driver.mergeJoinedResult(stack, this.mainAlias.meta, joinedProps);
|
|
1107
|
-
|
|
1147
|
+
for (const row of merged) {
|
|
1148
|
+
yield this.mapResult(row, options.mapResults);
|
|
1149
|
+
}
|
|
1108
1150
|
}
|
|
1109
1151
|
}
|
|
1110
1152
|
/**
|
|
@@ -1412,7 +1454,6 @@ export class QueryBuilder {
|
|
|
1412
1454
|
aliased: [QueryType.SELECT, QueryType.COUNT].includes(this.type),
|
|
1413
1455
|
});
|
|
1414
1456
|
const criteriaNode = CriteriaNodeFactory.createNode(this.metadata, prop.targetMeta.class, cond);
|
|
1415
|
-
const joinCountBefore = Object.keys(this.#state.joins).length;
|
|
1416
1457
|
cond = criteriaNode.process(this, { ignoreBranching: true, alias });
|
|
1417
1458
|
let aliasedName = `${fromAlias}.${prop.name}#${alias}`;
|
|
1418
1459
|
path ??= `${Object.values(this.#state.joins).find(j => j.alias === fromAlias)?.path ?? Utils.className(entityName)}.${prop.name}`;
|
|
@@ -1442,20 +1483,7 @@ export class QueryBuilder {
|
|
|
1442
1483
|
this.#state.joins[aliasedName] = this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, cond, schema);
|
|
1443
1484
|
this.#state.joins[aliasedName].path ??= path;
|
|
1444
1485
|
}
|
|
1445
|
-
|
|
1446
|
-
// forward reference (the auto-join's ON refers to alias, while alias's ON refers back to it);
|
|
1447
|
-
// fold them into the new join so both aliases share scope in the outer ON clause (issue #7681)
|
|
1448
|
-
const condJoin = this.#state.joins[aliasedName];
|
|
1449
|
-
const joinKeys = Object.keys(this.#state.joins);
|
|
1450
|
-
for (let i = joinCountBefore; i < joinKeys.length; i++) {
|
|
1451
|
-
const j = this.#state.joins[joinKeys[i]];
|
|
1452
|
-
if (j === condJoin || j.ownerAlias !== alias) {
|
|
1453
|
-
continue;
|
|
1454
|
-
}
|
|
1455
|
-
const nested = (condJoin.nested ??= new Set());
|
|
1456
|
-
j.type = j.type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
|
|
1457
|
-
nested.add(j);
|
|
1458
|
-
}
|
|
1486
|
+
this.nestReferencedJoins(this.#state.joins[aliasedName]);
|
|
1459
1487
|
return { prop, key: aliasedName };
|
|
1460
1488
|
}
|
|
1461
1489
|
prepareFields(fields, type = 'where', schema) {
|
|
@@ -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
|
}
|
|
@@ -785,6 +786,10 @@ export class QueryBuilderHelper {
|
|
|
785
786
|
appendGroupCondition(type, operator, subCondition) {
|
|
786
787
|
const parts = [];
|
|
787
788
|
const params = [];
|
|
789
|
+
// an empty disjunction is false, same as `$in: []`, while an empty conjunction is vacuously true
|
|
790
|
+
if (operator === '$or' && subCondition.length === 0) {
|
|
791
|
+
return { sql: '1 = 0', params };
|
|
792
|
+
}
|
|
788
793
|
// single sub-condition can be ignored to reduce nesting of parens
|
|
789
794
|
if (subCondition.length === 1 || operator === '$and') {
|
|
790
795
|
for (const sub of subCondition) {
|
|
@@ -49,6 +49,8 @@ export declare class DatabaseTable {
|
|
|
49
49
|
getEntityDeclaration(namingStrategy: NamingStrategy, schemaHelper: SchemaHelper, scalarPropertiesForRelations: 'always' | 'never' | 'smart'): EntityMetadata;
|
|
50
50
|
private foreignKeysToProps;
|
|
51
51
|
private findFkIndex;
|
|
52
|
+
/** Advanced options require an entity-level declaration, as the property-level `index`/`unique` cannot carry them. */
|
|
53
|
+
private hasAdvancedIndexOptions;
|
|
52
54
|
private getIndexProperties;
|
|
53
55
|
private getSafeBaseNameForFkProp;
|
|
54
56
|
/**
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -219,6 +219,8 @@ export class DatabaseTable {
|
|
|
219
219
|
skippedColumnNames.includes(index.columnNames[0]) || // Non-composite indexes for skipped columns are to be mapped as entity decorators.
|
|
220
220
|
index.deferMode ||
|
|
221
221
|
index.expression ||
|
|
222
|
+
index.where ||
|
|
223
|
+
this.hasAdvancedIndexOptions(index) ||
|
|
222
224
|
!(index.columnNames[0] in columnFks)) && // Trivial non-composite indexes for scalar props are to be mapped to the column.
|
|
223
225
|
// ignore indexes that don't have all column names (this can happen in sqlite where there is no way to infer this for expressions)
|
|
224
226
|
!(index.columnNames.some(col => !col) && !index.expression));
|
|
@@ -254,14 +256,7 @@ export class DatabaseTable {
|
|
|
254
256
|
}
|
|
255
257
|
}
|
|
256
258
|
// An index is trivial if it has no special options that require entity-level declaration
|
|
257
|
-
const
|
|
258
|
-
index.include?.length ||
|
|
259
|
-
index.fillFactor ||
|
|
260
|
-
index.type ||
|
|
261
|
-
index.invisible ||
|
|
262
|
-
index.disabled ||
|
|
263
|
-
index.clustered;
|
|
264
|
-
const isTrivial = !index.deferMode && !index.expression && !index.where && !hasAdvancedOptions;
|
|
259
|
+
const isTrivial = !index.deferMode && !index.expression && !index.where && !this.hasAdvancedIndexOptions(index);
|
|
265
260
|
if (isTrivial) {
|
|
266
261
|
// Index is for FK. Map to the FK prop and move on.
|
|
267
262
|
const fkForIndex = fkIndexes.get(index);
|
|
@@ -299,6 +294,19 @@ export class DatabaseTable {
|
|
|
299
294
|
}
|
|
300
295
|
schema.addIndex(ret);
|
|
301
296
|
}
|
|
297
|
+
for (const check of this.getChecks()) {
|
|
298
|
+
// skip checks that were consumed by enum conversion — the enum property recreates an
|
|
299
|
+
// equivalent check under the conventional name during discovery (only on platforms that
|
|
300
|
+
// emulate enums via check constraints; mysql/mariadb enums are native and recreate nothing)
|
|
301
|
+
const enumItems = check.columnName ? this.getColumn(check.columnName)?.enumItems : undefined;
|
|
302
|
+
if (this.#platform.usesEnumCheckConstraints() &&
|
|
303
|
+
enumItems?.length &&
|
|
304
|
+
(check.expression === this.#platform.getEnumCheckConstraintExpression(check.columnName, enumItems) ||
|
|
305
|
+
check.name === this.#platform.getIndexName(this.name, [check.columnName], 'check'))) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
schema.meta.checks.push({ name: check.name, expression: check.expression });
|
|
309
|
+
}
|
|
302
310
|
const addedStandaloneFkPropsBasedOnColumn = new Set();
|
|
303
311
|
const nonSkippedColumns = this.getColumns().filter(column => !skippedColumnNames.includes(column.name));
|
|
304
312
|
for (const column of nonSkippedColumns) {
|
|
@@ -507,7 +515,9 @@ export class DatabaseTable {
|
|
|
507
515
|
findFkIndex(currentFk) {
|
|
508
516
|
const fkColumnsLength = currentFk.columnNames.length;
|
|
509
517
|
const possibleIndexes = this.#indexes.filter(index => {
|
|
510
|
-
return (index.
|
|
518
|
+
return (!index.where &&
|
|
519
|
+
!this.hasAdvancedIndexOptions(index) &&
|
|
520
|
+
index.columnNames.length === fkColumnsLength &&
|
|
511
521
|
!currentFk.columnNames.some((columnName, i) => index.columnNames[i] !== columnName));
|
|
512
522
|
});
|
|
513
523
|
possibleIndexes.sort((a, b) => {
|
|
@@ -521,13 +531,31 @@ export class DatabaseTable {
|
|
|
521
531
|
});
|
|
522
532
|
return possibleIndexes.at(0);
|
|
523
533
|
}
|
|
534
|
+
/** Advanced options require an entity-level declaration, as the property-level `index`/`unique` cannot carry them. */
|
|
535
|
+
hasAdvancedIndexOptions(index) {
|
|
536
|
+
return !!(index.columns?.length ||
|
|
537
|
+
index.include?.length ||
|
|
538
|
+
index.fillFactor ||
|
|
539
|
+
index.type ||
|
|
540
|
+
index.invisible ||
|
|
541
|
+
index.disabled ||
|
|
542
|
+
index.clustered);
|
|
543
|
+
}
|
|
524
544
|
getIndexProperties(index, columnFks, fksOnColumnProps, fksOnStandaloneProps, namingStrategy) {
|
|
525
|
-
const propBaseNames = new
|
|
545
|
+
const propBaseNames = new Map();
|
|
526
546
|
const columnNames = index.columnNames;
|
|
527
547
|
const l = columnNames.length;
|
|
528
548
|
if (columnNames.some(col => !col)) {
|
|
529
549
|
return;
|
|
530
550
|
}
|
|
551
|
+
const addPropBaseName = (baseName, position) => {
|
|
552
|
+
const positions = propBaseNames.get(baseName);
|
|
553
|
+
if (positions) {
|
|
554
|
+
positions.last = position;
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
propBaseNames.set(baseName, { first: position, last: position });
|
|
558
|
+
};
|
|
531
559
|
for (let i = 0; i < l; ++i) {
|
|
532
560
|
const columnName = columnNames[i];
|
|
533
561
|
// The column is not involved with FKs.
|
|
@@ -538,14 +566,14 @@ export class DatabaseTable {
|
|
|
538
566
|
}
|
|
539
567
|
// It has a prop named after it.
|
|
540
568
|
// Add it and move on.
|
|
541
|
-
|
|
569
|
+
addPropBaseName(columnName, i);
|
|
542
570
|
continue;
|
|
543
571
|
}
|
|
544
572
|
// If the prop named after the column has a FK and the FK's columns are a subset of this index,
|
|
545
573
|
// include this prop and move on.
|
|
546
574
|
const columnPropFk = fksOnColumnProps.get(columnName);
|
|
547
575
|
if (columnPropFk && !columnPropFk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
|
|
548
|
-
|
|
576
|
+
addPropBaseName(columnName, i);
|
|
549
577
|
continue;
|
|
550
578
|
}
|
|
551
579
|
// If there is at least one standalone FK featuring this column,
|
|
@@ -557,7 +585,7 @@ export class DatabaseTable {
|
|
|
557
585
|
continue;
|
|
558
586
|
}
|
|
559
587
|
if (!fk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
|
|
560
|
-
|
|
588
|
+
addPropBaseName(propName, i);
|
|
561
589
|
propAdded = true;
|
|
562
590
|
}
|
|
563
591
|
}
|
|
@@ -568,7 +596,10 @@ export class DatabaseTable {
|
|
|
568
596
|
// Break the whole prop creation.
|
|
569
597
|
return;
|
|
570
598
|
}
|
|
571
|
-
|
|
599
|
+
// Props sharing their first column would otherwise follow FK discovery order, so break ties on the last one.
|
|
600
|
+
return Array.from(propBaseNames)
|
|
601
|
+
.sort(([, a], [, b]) => a.first - b.first || a.last - b.last)
|
|
602
|
+
.map(([baseName]) => this.getPropertyName(namingStrategy, baseName, fksOnColumnProps.get(baseName)));
|
|
572
603
|
}
|
|
573
604
|
getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName) {
|
|
574
605
|
if (columnName &&
|
|
@@ -694,9 +725,19 @@ export class DatabaseTable {
|
|
|
694
725
|
const prop = this.getPropertyName(namingStrategy, column.name, fk);
|
|
695
726
|
const persist = !(column.name in columnFks && typeof fk === 'undefined');
|
|
696
727
|
const index = compositeFkIndexes[prop] ||
|
|
697
|
-
this.#indexes.find(idx => idx.columnNames[0] === column.name &&
|
|
728
|
+
this.#indexes.find(idx => idx.columnNames[0] === column.name &&
|
|
729
|
+
!idx.composite &&
|
|
730
|
+
!idx.unique &&
|
|
731
|
+
!idx.primary &&
|
|
732
|
+
!idx.where &&
|
|
733
|
+
!this.hasAdvancedIndexOptions(idx));
|
|
698
734
|
const unique = compositeFkUniques[prop] ||
|
|
699
|
-
this.#indexes.find(idx => idx.columnNames[0] === column.name &&
|
|
735
|
+
this.#indexes.find(idx => idx.columnNames[0] === column.name &&
|
|
736
|
+
!idx.composite &&
|
|
737
|
+
idx.unique &&
|
|
738
|
+
!idx.primary &&
|
|
739
|
+
!idx.where &&
|
|
740
|
+
!this.hasAdvancedIndexOptions(idx));
|
|
700
741
|
const kind = this.getReferenceKind(fk, unique);
|
|
701
742
|
const runtimeType = this.getPropertyTypeForColumn(namingStrategy, column, fk);
|
|
702
743
|
const type = fk
|
|
@@ -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
|
|
@@ -118,6 +124,8 @@ export declare abstract class SchemaHelper {
|
|
|
118
124
|
getAddColumnsSQL(table: DatabaseTable, columns: Column[]): string[];
|
|
119
125
|
getDropColumnsSQL(tableName: string, columns: Column[], schemaName?: string): string;
|
|
120
126
|
hasNonDefaultPrimaryKeyName(table: DatabaseTable): boolean;
|
|
127
|
+
/** Returns the `constraint <name> ` prefix for a primary key definition, empty when the server assigns the default name on its own. */
|
|
128
|
+
protected getPrimaryKeyConstraintPrefix(table: DatabaseTable, index: IndexDef): string;
|
|
121
129
|
castColumn(name: string, type: string): string;
|
|
122
130
|
alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[];
|
|
123
131
|
/** Returns the bare `collate <name>` clause for column DDL. Overridden by PostgreSQL to quote the identifier. */
|
|
@@ -137,6 +145,8 @@ export declare abstract class SchemaHelper {
|
|
|
137
145
|
}[];
|
|
138
146
|
}[], safe: boolean): string[];
|
|
139
147
|
getChangeColumnCommentSQL(tableName: string, to: Column, schemaName?: string): string;
|
|
148
|
+
/** Whether the column comment is part of the column declaration, as opposed to a separate statement. */
|
|
149
|
+
protected hasInlineColumnComment(): boolean;
|
|
140
150
|
getNamespaces(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string[]>;
|
|
141
151
|
protected mapIndexes(indexes: IndexDef[]): Promise<IndexDef[]>;
|
|
142
152
|
mapForeignKeys(fks: any[], tableName: string, schemaName?: string): Dictionary;
|
|
@@ -176,6 +186,8 @@ export declare abstract class SchemaHelper {
|
|
|
176
186
|
createRoutine(_routine: SqlRoutineDef): string;
|
|
177
187
|
dropRoutine(_routine: SqlRoutineDef): string;
|
|
178
188
|
getAllRoutines(_connection: AbstractSqlConnection, _schemas?: string[]): Promise<SqlRoutineDef[]>;
|
|
189
|
+
/** Flattens internal `;\n` so the statement splitter doesn't tear the DDL, and ensures exactly one trailing `;` for the enclosing `begin ... end` block. */
|
|
190
|
+
protected normalizeTriggerBody(body: string): string;
|
|
179
191
|
/** 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
192
|
protected wrapRoutineBody(body: string): string;
|
|
181
193
|
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));
|
|
@@ -504,6 +521,10 @@ export class SchemaHelper {
|
|
|
504
521
|
const defaultName = this.platform.getDefaultPrimaryName(table.name, pkIndex.columnNames);
|
|
505
522
|
return pkIndex?.keyName !== defaultName;
|
|
506
523
|
}
|
|
524
|
+
/** Returns the `constraint <name> ` prefix for a primary key definition, empty when the server assigns the default name on its own. */
|
|
525
|
+
getPrimaryKeyConstraintPrefix(table, index) {
|
|
526
|
+
return this.hasNonDefaultPrimaryKeyName(table) ? `constraint ${this.quote(index.keyName)} ` : '';
|
|
527
|
+
}
|
|
507
528
|
/* v8 ignore next */
|
|
508
529
|
castColumn(name, type) {
|
|
509
530
|
return '';
|
|
@@ -608,6 +629,10 @@ export class SchemaHelper {
|
|
|
608
629
|
getChangeColumnCommentSQL(tableName, to, schemaName) {
|
|
609
630
|
return '';
|
|
610
631
|
}
|
|
632
|
+
/** Whether the column comment is part of the column declaration, as opposed to a separate statement. */
|
|
633
|
+
hasInlineColumnComment() {
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
611
636
|
async getNamespaces(connection, ctx) {
|
|
612
637
|
return [];
|
|
613
638
|
}
|
|
@@ -745,7 +770,7 @@ export class SchemaHelper {
|
|
|
745
770
|
const primaryKey = table.getPrimaryKey();
|
|
746
771
|
const createPrimary = !table.getColumns().some(c => c.autoincrement && c.primary) || this.hasNonDefaultPrimaryKeyName(table);
|
|
747
772
|
if (createPrimary && primaryKey) {
|
|
748
|
-
const name = this.
|
|
773
|
+
const name = this.getPrimaryKeyConstraintPrefix(table, primaryKey);
|
|
749
774
|
sql += `, ${name}primary key (${primaryKey.columnNames.map(c => this.quote(c)).join(', ')})`;
|
|
750
775
|
}
|
|
751
776
|
sql += ')';
|
|
@@ -828,7 +853,7 @@ export class SchemaHelper {
|
|
|
828
853
|
const columns = index.columnNames.map(c => this.quote(c)).join(', ');
|
|
829
854
|
const defer = index.deferMode ? ` deferrable initially ${index.deferMode}` : '';
|
|
830
855
|
if (index.primary) {
|
|
831
|
-
const keyName = this.
|
|
856
|
+
const keyName = this.getPrimaryKeyConstraintPrefix(table, index);
|
|
832
857
|
return `alter table ${table.getQuotedName()} add ${keyName}primary key (${columns})${defer}`;
|
|
833
858
|
}
|
|
834
859
|
if (index.type === 'fulltext') {
|
|
@@ -861,7 +886,7 @@ export class SchemaHelper {
|
|
|
861
886
|
const events = trigger.events.map(e => e.toUpperCase()).join(' OR ');
|
|
862
887
|
const forEach = trigger.forEach === 'statement' ? 'STATEMENT' : 'ROW';
|
|
863
888
|
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}
|
|
889
|
+
return `create trigger ${this.quote(trigger.name)} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} begin ${this.normalizeTriggerBody(trigger.body)} end`;
|
|
865
890
|
}
|
|
866
891
|
/**
|
|
867
892
|
* Generates SQL to drop a database trigger from a table.
|
|
@@ -885,6 +910,11 @@ export class SchemaHelper {
|
|
|
885
910
|
async getAllRoutines(_connection, _schemas = []) {
|
|
886
911
|
return [];
|
|
887
912
|
}
|
|
913
|
+
/** Flattens internal `;\n` so the statement splitter doesn't tear the DDL, and ensures exactly one trailing `;` for the enclosing `begin ... end` block. */
|
|
914
|
+
normalizeTriggerBody(body) {
|
|
915
|
+
const trimmed = stripStatementNewlines(body).trim();
|
|
916
|
+
return /;\s*$/.test(trimmed) ? trimmed : `${trimmed};`;
|
|
917
|
+
}
|
|
888
918
|
/** 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
919
|
wrapRoutineBody(body) {
|
|
890
920
|
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);
|
package/typings.d.ts
CHANGED
|
@@ -368,9 +368,9 @@ type MaybeGenerated<TValue, TOptions, TProcessOnCreate extends boolean> = TOptio
|
|
|
368
368
|
} ? TValue | null : TOptions extends {
|
|
369
369
|
autoincrement: true;
|
|
370
370
|
} ? Generated<TValue> : TOptions extends {
|
|
371
|
-
default:
|
|
371
|
+
default: unknown;
|
|
372
372
|
} ? Generated<TValue> : TOptions extends {
|
|
373
|
-
defaultRaw:
|
|
373
|
+
defaultRaw: unknown;
|
|
374
374
|
} ? Generated<TValue> : TProcessOnCreate extends false ? TValue : TOptions extends {
|
|
375
375
|
onCreate: Function;
|
|
376
376
|
} ? Generated<TValue> : TValue;
|