@mikro-orm/sql 7.1.0-dev.0 → 7.1.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AbstractSqlDriver.d.ts +17 -0
- package/AbstractSqlDriver.js +263 -14
- package/PivotCollectionPersister.js +13 -2
- package/SqlEntityManager.d.ts +5 -1
- package/SqlEntityManager.js +36 -1
- package/dialects/mssql/MsSqlNativeQueryBuilder.js +4 -0
- package/dialects/mysql/BaseMySqlPlatform.d.ts +1 -0
- package/dialects/mysql/BaseMySqlPlatform.js +3 -0
- package/dialects/mysql/MySqlNativeQueryBuilder.js +11 -0
- package/dialects/mysql/MySqlSchemaHelper.d.ts +4 -1
- package/dialects/mysql/MySqlSchemaHelper.js +83 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +8 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +93 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +5 -1
- package/dialects/sqlite/SqliteSchemaHelper.js +99 -0
- package/package.json +2 -2
- package/query/NativeQueryBuilder.d.ts +6 -0
- package/query/NativeQueryBuilder.js +16 -1
- package/query/QueryBuilder.d.ts +63 -0
- package/query/QueryBuilder.js +168 -5
- package/schema/DatabaseSchema.js +14 -0
- package/schema/DatabaseTable.d.ts +7 -1
- package/schema/DatabaseTable.js +18 -0
- package/schema/SchemaComparator.d.ts +1 -0
- package/schema/SchemaComparator.js +54 -0
- package/schema/SchemaHelper.d.ts +11 -1
- package/schema/SchemaHelper.js +42 -0
- package/schema/SqlSchemaGenerator.js +7 -0
- package/typings.d.ts +13 -0
package/query/QueryBuilder.js
CHANGED
|
@@ -159,6 +159,41 @@ export class QueryBuilder {
|
|
|
159
159
|
insert(data) {
|
|
160
160
|
return this.init(QueryType.INSERT, data);
|
|
161
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Creates an INSERT ... SELECT query that copies rows from the source query.
|
|
164
|
+
*
|
|
165
|
+
* Column resolution (3 tiers):
|
|
166
|
+
* 1. No explicit select on source, no explicit columns → all cloneable columns derived from entity metadata
|
|
167
|
+
* 2. Explicit select on source, no explicit columns → columns derived from selected field names
|
|
168
|
+
* 3. Explicit `columns` option → user-provided column list
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* // Clone all fields (columns auto-derived from metadata)
|
|
173
|
+
* const source = em.createQueryBuilder(User).where({ id: 1 });
|
|
174
|
+
* await em.createQueryBuilder(User).insertFrom(source).execute();
|
|
175
|
+
*
|
|
176
|
+
* // Clone with overrides via raw() aliases
|
|
177
|
+
* const source = em.createQueryBuilder(User)
|
|
178
|
+
* .select(['name', raw("'new@email.com'").as('email')])
|
|
179
|
+
* .where({ id: 1 });
|
|
180
|
+
* await em.createQueryBuilder(User).insertFrom(source).execute();
|
|
181
|
+
*
|
|
182
|
+
* // Explicit columns for full control
|
|
183
|
+
* await em.createQueryBuilder(User)
|
|
184
|
+
* .insertFrom(source, { columns: ['name', 'email'] })
|
|
185
|
+
* .execute();
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
insertFrom(subQuery, options) {
|
|
189
|
+
this.ensureNotFinalized();
|
|
190
|
+
this.#state.type = QueryType.INSERT;
|
|
191
|
+
this.#state.insertSubQuery = subQuery;
|
|
192
|
+
if (options?.columns) {
|
|
193
|
+
this.#state.insertColumns = Utils.asArray(options.columns);
|
|
194
|
+
}
|
|
195
|
+
return this;
|
|
196
|
+
}
|
|
162
197
|
/**
|
|
163
198
|
* Creates an UPDATE query with the given data.
|
|
164
199
|
* Use `where()` to specify which rows to update.
|
|
@@ -686,6 +721,12 @@ export class QueryBuilder {
|
|
|
686
721
|
hasFlag(flag) {
|
|
687
722
|
return this.#state.flags.has(flag);
|
|
688
723
|
}
|
|
724
|
+
/** @internal */
|
|
725
|
+
setPartitionLimit(opts) {
|
|
726
|
+
this.ensureNotFinalized();
|
|
727
|
+
this.#state.partitionLimit = opts;
|
|
728
|
+
return this;
|
|
729
|
+
}
|
|
689
730
|
cache(config = true) {
|
|
690
731
|
this.ensureNotFinalized();
|
|
691
732
|
this.#state.cache = config;
|
|
@@ -789,12 +830,18 @@ export class QueryBuilder {
|
|
|
789
830
|
if (this.#state.lockMode) {
|
|
790
831
|
this.helper.getLockSQL(qb, this.#state.lockMode, this.#state.lockTables, this.#state.joins);
|
|
791
832
|
}
|
|
792
|
-
this.processReturningStatement(qb, this.mainAlias.meta, this.#state.data, this.#state.returning);
|
|
833
|
+
this.processReturningStatement(qb, this.mainAlias.meta, this.#state.insertSubQuery ? undefined : this.#state.data, this.#state.returning);
|
|
834
|
+
if (this.#state.partitionLimit) {
|
|
835
|
+
return (this.#query.qb = this.wrapPartitionLimitSubQuery(qb));
|
|
836
|
+
}
|
|
793
837
|
return (this.#query.qb = qb);
|
|
794
838
|
}
|
|
795
839
|
processReturningStatement(qb, meta, data, returning) {
|
|
796
840
|
const usesReturningStatement = this.platform.usesReturningStatement() || this.platform.usesOutputStatement();
|
|
797
|
-
if (!meta || !
|
|
841
|
+
if (!meta || !usesReturningStatement) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (!data && !this.#state.insertSubQuery) {
|
|
798
845
|
return;
|
|
799
846
|
}
|
|
800
847
|
// always respect explicit returning hint
|
|
@@ -805,13 +852,13 @@ export class QueryBuilder {
|
|
|
805
852
|
if (this.type === QueryType.INSERT) {
|
|
806
853
|
const returningProps = meta.hydrateProps
|
|
807
854
|
.filter(prop => prop.returning || (prop.persist !== false && ((prop.primary && prop.autoincrement) || prop.defaultRaw)))
|
|
808
|
-
.filter(prop => !(prop.name in data));
|
|
855
|
+
.filter(prop => !data || !(prop.name in data));
|
|
809
856
|
if (returningProps.length > 0) {
|
|
810
857
|
qb.returning(Utils.flatten(returningProps.map(prop => prop.fieldNames)));
|
|
811
858
|
}
|
|
812
859
|
return;
|
|
813
860
|
}
|
|
814
|
-
if (this.type === QueryType.UPDATE) {
|
|
861
|
+
if (this.type === QueryType.UPDATE && data) {
|
|
815
862
|
const returningProps = meta.hydrateProps.filter(prop => prop.fieldNames && isRaw(data[prop.fieldNames[0]]));
|
|
816
863
|
if (returningProps.length > 0) {
|
|
817
864
|
qb.returning(returningProps.flatMap((prop) => {
|
|
@@ -1603,7 +1650,14 @@ export class QueryBuilder {
|
|
|
1603
1650
|
break;
|
|
1604
1651
|
}
|
|
1605
1652
|
case QueryType.INSERT:
|
|
1606
|
-
|
|
1653
|
+
if (this.#state.insertSubQuery) {
|
|
1654
|
+
const columns = this.resolveInsertFromColumns();
|
|
1655
|
+
const compiled = this.#state.insertSubQuery.toQuery();
|
|
1656
|
+
qb.insertSelect(columns, raw(compiled.sql, compiled.params));
|
|
1657
|
+
}
|
|
1658
|
+
else {
|
|
1659
|
+
qb.insert(this.#state.data);
|
|
1660
|
+
}
|
|
1607
1661
|
break;
|
|
1608
1662
|
case QueryType.UPDATE:
|
|
1609
1663
|
qb.update(this.#state.data);
|
|
@@ -1619,6 +1673,78 @@ export class QueryBuilder {
|
|
|
1619
1673
|
}
|
|
1620
1674
|
return qb;
|
|
1621
1675
|
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Resolves the INSERT column list for `insertFrom()`.
|
|
1678
|
+
*
|
|
1679
|
+
* Tier 1: Explicit `insertColumns` from `options.columns` → map property names to field names
|
|
1680
|
+
* Tier 2: Source QB has explicit select fields → derive from those
|
|
1681
|
+
* Tier 3: Derive from target entity metadata (all cloneable columns), auto-populate source select
|
|
1682
|
+
*/
|
|
1683
|
+
resolveInsertFromColumns() {
|
|
1684
|
+
const meta = this.mainAlias.meta;
|
|
1685
|
+
const subQuery = this.#state.insertSubQuery;
|
|
1686
|
+
// Tier 1: explicit columns
|
|
1687
|
+
if (this.#state.insertColumns?.length) {
|
|
1688
|
+
return this.#state.insertColumns.flatMap(col => {
|
|
1689
|
+
const prop = meta?.properties[col];
|
|
1690
|
+
return prop?.fieldNames ?? [col];
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
// Tier 2: source QB has explicit select fields
|
|
1694
|
+
const sourceFields = subQuery.state.fields;
|
|
1695
|
+
if (sourceFields && subQuery.state.type === QueryType.SELECT) {
|
|
1696
|
+
return sourceFields
|
|
1697
|
+
.filter((field) => typeof field === 'string' || isRaw(field))
|
|
1698
|
+
.flatMap((field) => {
|
|
1699
|
+
if (typeof field === 'string') {
|
|
1700
|
+
// Strip alias prefix like 'a0.'
|
|
1701
|
+
const bare = field.replace(/^\w+\./, '');
|
|
1702
|
+
const prop = meta?.properties[bare];
|
|
1703
|
+
return prop?.fieldNames ?? [bare];
|
|
1704
|
+
}
|
|
1705
|
+
// RawQueryFragment with alias: raw('...').as('name')
|
|
1706
|
+
const alias = String(field.params[field.params.length - 1]);
|
|
1707
|
+
const prop = meta?.properties[alias];
|
|
1708
|
+
return prop?.fieldNames ?? [alias];
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
// Tier 3: derive from metadata — all cloneable columns
|
|
1712
|
+
const cloneableProps = this.getCloneableProps(meta);
|
|
1713
|
+
const selectFields = [];
|
|
1714
|
+
const columns = [];
|
|
1715
|
+
for (const prop of cloneableProps) {
|
|
1716
|
+
for (const fieldName of prop.fieldNames) {
|
|
1717
|
+
columns.push(fieldName);
|
|
1718
|
+
selectFields.push(fieldName);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
// Auto-populate source select with matching fields
|
|
1722
|
+
if (!sourceFields) {
|
|
1723
|
+
subQuery.select(selectFields);
|
|
1724
|
+
}
|
|
1725
|
+
return columns;
|
|
1726
|
+
}
|
|
1727
|
+
/** Returns properties that are safe to clone (persistable, non-PK, non-generated). */
|
|
1728
|
+
getCloneableProps(meta) {
|
|
1729
|
+
return meta.props.filter(prop => {
|
|
1730
|
+
if (prop.persist === false) {
|
|
1731
|
+
return false;
|
|
1732
|
+
}
|
|
1733
|
+
if (prop.primary) {
|
|
1734
|
+
return false;
|
|
1735
|
+
}
|
|
1736
|
+
if (!prop.fieldNames?.length) {
|
|
1737
|
+
return false;
|
|
1738
|
+
}
|
|
1739
|
+
if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
|
|
1740
|
+
return false;
|
|
1741
|
+
}
|
|
1742
|
+
if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
|
|
1743
|
+
return false;
|
|
1744
|
+
}
|
|
1745
|
+
return true;
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1622
1748
|
applyDiscriminatorCondition() {
|
|
1623
1749
|
const meta = this.mainAlias.meta;
|
|
1624
1750
|
if (meta.root.inheritanceType !== 'sti' || !meta.discriminatorValue) {
|
|
@@ -1782,6 +1908,9 @@ export class QueryBuilder {
|
|
|
1782
1908
|
(this.#state.limit > 0 || this.#state.offset > 0)) {
|
|
1783
1909
|
this.wrapPaginateSubQuery(meta);
|
|
1784
1910
|
}
|
|
1911
|
+
if (this.#state.partitionLimit) {
|
|
1912
|
+
this.preparePartitionLimit();
|
|
1913
|
+
}
|
|
1785
1914
|
if (meta &&
|
|
1786
1915
|
(this.#state.flags.has(QueryFlag.UPDATE_SUB_QUERY) || this.#state.flags.has(QueryFlag.DELETE_SUB_QUERY))) {
|
|
1787
1916
|
this.wrapModifySubQuery(meta);
|
|
@@ -2001,6 +2130,40 @@ export class QueryBuilder {
|
|
|
2001
2130
|
[Utils.getPrimaryKeyHash(meta.primaryKeys)]: { $in: raw(sql, params) },
|
|
2002
2131
|
});
|
|
2003
2132
|
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Wraps the inner query (which has ROW_NUMBER in SELECT) with an outer query
|
|
2135
|
+
* that filters by the __rn column to apply per-parent limiting.
|
|
2136
|
+
*/
|
|
2137
|
+
wrapPartitionLimitSubQuery(innerQb) {
|
|
2138
|
+
const { limit, offset = 0 } = this.#state.partitionLimit;
|
|
2139
|
+
const rnCol = this.platform.quoteIdentifier('__rn');
|
|
2140
|
+
innerQb.as(this.mainAlias.aliasName);
|
|
2141
|
+
const outerQb = this.platform.createNativeQueryBuilder();
|
|
2142
|
+
outerQb.select('*').from(innerQb);
|
|
2143
|
+
outerQb.where(`${rnCol} > ? and ${rnCol} <= ?`, [offset, offset + limit]);
|
|
2144
|
+
outerQb.orderBy(rnCol);
|
|
2145
|
+
return outerQb;
|
|
2146
|
+
}
|
|
2147
|
+
/**
|
|
2148
|
+
* Adds ROW_NUMBER() OVER (PARTITION BY ...) to the SELECT list and prepares
|
|
2149
|
+
* the query state for per-parent limiting. The actual wrapping into a subquery
|
|
2150
|
+
* with __rn filtering happens in getNativeQuery().
|
|
2151
|
+
*/
|
|
2152
|
+
preparePartitionLimit() {
|
|
2153
|
+
const { partitionBy } = this.#state.partitionLimit;
|
|
2154
|
+
// `partitionBy` is always a declared property name, so mapper returns a string here.
|
|
2155
|
+
const partitionCol = this.helper.mapper(partitionBy, this.type, undefined, null);
|
|
2156
|
+
const quotedPartition = partitionCol
|
|
2157
|
+
.split('.')
|
|
2158
|
+
.map(e => this.platform.quoteIdentifier(e))
|
|
2159
|
+
.join('.');
|
|
2160
|
+
const queryOrder = this.helper.getQueryOrder(this.type, this.#state.orderBy, this.#state.populateMap, this.#state.collation);
|
|
2161
|
+
const orderBySql = queryOrder.length > 0 ? Utils.unique(queryOrder).join(', ') : quotedPartition;
|
|
2162
|
+
const rnAlias = this.platform.quoteIdentifier('__rn');
|
|
2163
|
+
this.#state.fields.push(raw(`row_number() over (partition by ${quotedPartition} order by ${orderBySql}) as ${rnAlias}`));
|
|
2164
|
+
// Moved into the OVER clause; outer query re-applies via wrapPartitionLimitSubQuery
|
|
2165
|
+
this.#state.orderBy = [];
|
|
2166
|
+
}
|
|
2004
2167
|
/**
|
|
2005
2168
|
* Computes the set of populate paths from the _populate hints.
|
|
2006
2169
|
*/
|
package/schema/DatabaseSchema.js
CHANGED
|
@@ -220,6 +220,20 @@ export class DatabaseSchema {
|
|
|
220
220
|
columnName,
|
|
221
221
|
});
|
|
222
222
|
}
|
|
223
|
+
for (const trigger of meta.triggers) {
|
|
224
|
+
const body = isRaw(trigger.body)
|
|
225
|
+
? platform.formatQuery(trigger.body.sql, trigger.body.params)
|
|
226
|
+
: trigger.body;
|
|
227
|
+
table.addTrigger({
|
|
228
|
+
name: trigger.name,
|
|
229
|
+
timing: trigger.timing,
|
|
230
|
+
events: trigger.events,
|
|
231
|
+
forEach: trigger.forEach ?? 'row',
|
|
232
|
+
body: body ?? '',
|
|
233
|
+
when: trigger.when,
|
|
234
|
+
expression: trigger.expression,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
223
237
|
}
|
|
224
238
|
return schema;
|
|
225
239
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Configuration, type DeferMode, type Dictionary, type EntityMetadata, type EntityProperty, type IndexCallback, type NamingStrategy } from '@mikro-orm/core';
|
|
2
2
|
import type { SchemaHelper } from './SchemaHelper.js';
|
|
3
|
-
import type { CheckDef, Column, ForeignKey, IndexDef } from '../typings.js';
|
|
3
|
+
import type { CheckDef, Column, ForeignKey, IndexDef, SqlTriggerDef } from '../typings.js';
|
|
4
4
|
import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
|
|
5
5
|
/**
|
|
6
6
|
* @internal
|
|
@@ -22,11 +22,14 @@ export declare class DatabaseTable {
|
|
|
22
22
|
removeColumn(name: string): void;
|
|
23
23
|
getIndexes(): IndexDef[];
|
|
24
24
|
getChecks(): CheckDef[];
|
|
25
|
+
getTriggers(): SqlTriggerDef[];
|
|
25
26
|
/** @internal */
|
|
26
27
|
setIndexes(indexes: IndexDef[]): void;
|
|
27
28
|
/** @internal */
|
|
28
29
|
setChecks(checks: CheckDef[]): void;
|
|
29
30
|
/** @internal */
|
|
31
|
+
setTriggers(triggers: SqlTriggerDef[]): void;
|
|
32
|
+
/** @internal */
|
|
30
33
|
setForeignKeys(fks: Dictionary<ForeignKey>): void;
|
|
31
34
|
init(cols: Column[], indexes: IndexDef[] | undefined, checks: CheckDef[] | undefined, pks: string[], fks?: Dictionary<ForeignKey>, enums?: Dictionary<string[]>): void;
|
|
32
35
|
addColumn(column: Column): void;
|
|
@@ -47,6 +50,8 @@ export declare class DatabaseTable {
|
|
|
47
50
|
hasIndex(indexName: string): boolean;
|
|
48
51
|
getCheck(checkName: string): CheckDef | undefined;
|
|
49
52
|
hasCheck(checkName: string): boolean;
|
|
53
|
+
getTrigger(triggerName: string): SqlTriggerDef | undefined;
|
|
54
|
+
hasTrigger(triggerName: string): boolean;
|
|
50
55
|
getPrimaryKey(): IndexDef | undefined;
|
|
51
56
|
hasPrimaryKey(): boolean;
|
|
52
57
|
private getForeignKeyDeclaration;
|
|
@@ -78,5 +83,6 @@ export declare class DatabaseTable {
|
|
|
78
83
|
clustered?: boolean;
|
|
79
84
|
}, type: 'index' | 'unique' | 'primary'): void;
|
|
80
85
|
addCheck(check: CheckDef): void;
|
|
86
|
+
addTrigger(trigger: SqlTriggerDef): void;
|
|
81
87
|
toJSON(): Dictionary;
|
|
82
88
|
}
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -8,6 +8,7 @@ export class DatabaseTable {
|
|
|
8
8
|
#columns = {};
|
|
9
9
|
#indexes = [];
|
|
10
10
|
#checks = [];
|
|
11
|
+
#triggers = [];
|
|
11
12
|
#foreignKeys = {};
|
|
12
13
|
#platform;
|
|
13
14
|
nativeEnums = {}; // for postgres
|
|
@@ -35,6 +36,9 @@ export class DatabaseTable {
|
|
|
35
36
|
getChecks() {
|
|
36
37
|
return this.#checks;
|
|
37
38
|
}
|
|
39
|
+
getTriggers() {
|
|
40
|
+
return this.#triggers;
|
|
41
|
+
}
|
|
38
42
|
/** @internal */
|
|
39
43
|
setIndexes(indexes) {
|
|
40
44
|
this.#indexes = indexes;
|
|
@@ -44,6 +48,10 @@ export class DatabaseTable {
|
|
|
44
48
|
this.#checks = checks;
|
|
45
49
|
}
|
|
46
50
|
/** @internal */
|
|
51
|
+
setTriggers(triggers) {
|
|
52
|
+
this.#triggers = triggers;
|
|
53
|
+
}
|
|
54
|
+
/** @internal */
|
|
47
55
|
setForeignKeys(fks) {
|
|
48
56
|
this.#foreignKeys = fks;
|
|
49
57
|
}
|
|
@@ -598,6 +606,12 @@ export class DatabaseTable {
|
|
|
598
606
|
hasCheck(checkName) {
|
|
599
607
|
return !!this.getCheck(checkName);
|
|
600
608
|
}
|
|
609
|
+
getTrigger(triggerName) {
|
|
610
|
+
return this.#triggers.find(t => t.name === triggerName);
|
|
611
|
+
}
|
|
612
|
+
hasTrigger(triggerName) {
|
|
613
|
+
return !!this.getTrigger(triggerName);
|
|
614
|
+
}
|
|
601
615
|
getPrimaryKey() {
|
|
602
616
|
return this.#indexes.find(i => i.primary);
|
|
603
617
|
}
|
|
@@ -885,6 +899,9 @@ export class DatabaseTable {
|
|
|
885
899
|
addCheck(check) {
|
|
886
900
|
this.#checks.push(check);
|
|
887
901
|
}
|
|
902
|
+
addTrigger(trigger) {
|
|
903
|
+
this.#triggers.push(trigger);
|
|
904
|
+
}
|
|
888
905
|
toJSON() {
|
|
889
906
|
const columns = this.#columns;
|
|
890
907
|
const columnsMapped = Utils.keys(columns).reduce((o, col) => {
|
|
@@ -929,6 +946,7 @@ export class DatabaseTable {
|
|
|
929
946
|
columns: columnsMapped,
|
|
930
947
|
indexes: this.#indexes,
|
|
931
948
|
checks: this.#checks,
|
|
949
|
+
triggers: this.#triggers,
|
|
932
950
|
foreignKeys: this.#foreignKeys,
|
|
933
951
|
nativeEnums: this.nativeEnums,
|
|
934
952
|
comment: this.comment,
|
|
@@ -69,6 +69,7 @@ export declare class SchemaComparator {
|
|
|
69
69
|
* @see https://github.com/mikro-orm/mikro-orm/issues/7308
|
|
70
70
|
*/
|
|
71
71
|
private diffViewExpression;
|
|
72
|
+
private diffTrigger;
|
|
72
73
|
parseJsonDefault(defaultValue?: string | null): Dictionary | string | null;
|
|
73
74
|
hasSameDefaultValue(from: Column, to: Column): boolean;
|
|
74
75
|
private mapColumnToProperty;
|
|
@@ -176,14 +176,17 @@ export class SchemaComparator {
|
|
|
176
176
|
addedForeignKeys: {},
|
|
177
177
|
addedIndexes: {},
|
|
178
178
|
addedChecks: {},
|
|
179
|
+
addedTriggers: {},
|
|
179
180
|
changedColumns: {},
|
|
180
181
|
changedForeignKeys: {},
|
|
181
182
|
changedIndexes: {},
|
|
182
183
|
changedChecks: {},
|
|
184
|
+
changedTriggers: {},
|
|
183
185
|
removedColumns: {},
|
|
184
186
|
removedForeignKeys: {},
|
|
185
187
|
removedIndexes: {},
|
|
186
188
|
removedChecks: {},
|
|
189
|
+
removedTriggers: {},
|
|
187
190
|
renamedColumns: {},
|
|
188
191
|
renamedIndexes: {},
|
|
189
192
|
fromTable,
|
|
@@ -309,6 +312,33 @@ export class SchemaComparator {
|
|
|
309
312
|
tableDifferences.changedChecks[check.name] = toTableCheck;
|
|
310
313
|
changes++;
|
|
311
314
|
}
|
|
315
|
+
const fromTableTriggers = fromTable.getTriggers();
|
|
316
|
+
const toTableTriggers = toTable.getTriggers();
|
|
317
|
+
for (const trigger of toTableTriggers) {
|
|
318
|
+
if (fromTable.hasTrigger(trigger.name)) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
tableDifferences.addedTriggers[trigger.name] = trigger;
|
|
322
|
+
this.log(`trigger ${trigger.name} added to table ${tableDifferences.name}`, { trigger });
|
|
323
|
+
changes++;
|
|
324
|
+
}
|
|
325
|
+
for (const trigger of fromTableTriggers) {
|
|
326
|
+
if (!toTable.hasTrigger(trigger.name)) {
|
|
327
|
+
tableDifferences.removedTriggers[trigger.name] = trigger;
|
|
328
|
+
this.log(`trigger ${trigger.name} removed from table ${tableDifferences.name}`);
|
|
329
|
+
changes++;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const toTableTrigger = toTable.getTrigger(trigger.name);
|
|
333
|
+
if (this.diffTrigger(trigger, toTableTrigger)) {
|
|
334
|
+
this.log(`trigger ${trigger.name} changed in table ${tableDifferences.name}`, {
|
|
335
|
+
fromTableTrigger: trigger,
|
|
336
|
+
toTableTrigger,
|
|
337
|
+
});
|
|
338
|
+
tableDifferences.changedTriggers[trigger.name] = toTableTrigger;
|
|
339
|
+
changes++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
312
342
|
const fromForeignKeys = { ...fromTable.getForeignKeys() };
|
|
313
343
|
const toForeignKeys = { ...toTable.getForeignKeys() };
|
|
314
344
|
for (const fromConstraint of Object.values(fromForeignKeys)) {
|
|
@@ -714,6 +744,30 @@ export class SchemaComparator {
|
|
|
714
744
|
}
|
|
715
745
|
return true;
|
|
716
746
|
}
|
|
747
|
+
diffTrigger(from, to) {
|
|
748
|
+
// Raw DDL expression cannot be meaningfully compared to introspected
|
|
749
|
+
// trigger metadata, so skip diffing when the metadata side uses it.
|
|
750
|
+
if (to.expression) {
|
|
751
|
+
// Both sides have expression — compare the raw DDL directly
|
|
752
|
+
if (from.expression) {
|
|
753
|
+
return this.diffExpression(from.expression, to.expression);
|
|
754
|
+
}
|
|
755
|
+
// Only metadata side has expression — the raw DDL cannot be compared to
|
|
756
|
+
// introspected metadata. Changes to the expression value won't be detected;
|
|
757
|
+
// drop and recreate the trigger manually to apply expression changes.
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
if (from.timing !== to.timing || from.forEach !== to.forEach) {
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
if ([...from.events].sort().join(',') !== [...to.events].sort().join(',')) {
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
if ((from.when ?? '') !== (to.when ?? '')) {
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
return this.diffExpression(from.body, to.body);
|
|
770
|
+
}
|
|
717
771
|
parseJsonDefault(defaultValue) {
|
|
718
772
|
/* v8 ignore next */
|
|
719
773
|
if (!defaultValue) {
|
package/schema/SchemaHelper.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Connection, type Dictionary, type Options, type Transaction, type RawQueryFragment } from '@mikro-orm/core';
|
|
2
2
|
import type { AbstractSqlConnection } from '../AbstractSqlConnection.js';
|
|
3
3
|
import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
|
|
4
|
-
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference } from '../typings.js';
|
|
4
|
+
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, SqlTriggerDef } from '../typings.js';
|
|
5
5
|
import type { DatabaseSchema } from './DatabaseSchema.js';
|
|
6
6
|
import type { DatabaseTable } from './DatabaseTable.js';
|
|
7
7
|
/** Base class for database-specific schema helpers. Provides SQL generation for DDL operations. */
|
|
@@ -84,6 +84,16 @@ export declare abstract class SchemaHelper {
|
|
|
84
84
|
getReferencedTableName(referencedTableName: string, schema?: string): string;
|
|
85
85
|
createIndex(index: IndexDef, table: DatabaseTable, createPrimary?: boolean): string;
|
|
86
86
|
createCheck(table: DatabaseTable, check: CheckDef): string;
|
|
87
|
+
/**
|
|
88
|
+
* Generates SQL to create a database trigger on a table.
|
|
89
|
+
* Override in driver-specific helpers for custom DDL (e.g., PostgreSQL function wrapping).
|
|
90
|
+
*/
|
|
91
|
+
createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
92
|
+
/**
|
|
93
|
+
* Generates SQL to drop a database trigger from a table.
|
|
94
|
+
* Override in driver-specific helpers for custom DDL.
|
|
95
|
+
*/
|
|
96
|
+
dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
87
97
|
/** @internal */
|
|
88
98
|
getTableName(table: string, schema?: string): string;
|
|
89
99
|
getTablesGroupedBySchemas(tables: Table[]): Map<string | undefined, Table[]>;
|
package/schema/SchemaHelper.js
CHANGED
|
@@ -204,6 +204,12 @@ export class SchemaHelper {
|
|
|
204
204
|
for (const check of Object.values(diff.changedChecks)) {
|
|
205
205
|
ret.push(this.dropConstraint(diff.name, check.name));
|
|
206
206
|
}
|
|
207
|
+
for (const trigger of Object.values(diff.removedTriggers)) {
|
|
208
|
+
ret.push(this.dropTrigger(diff.toTable, trigger));
|
|
209
|
+
}
|
|
210
|
+
for (const trigger of Object.values(diff.changedTriggers)) {
|
|
211
|
+
ret.push(this.dropTrigger(diff.toTable, trigger));
|
|
212
|
+
}
|
|
207
213
|
/* v8 ignore next */
|
|
208
214
|
if (!safe && Object.values(diff.removedColumns).length > 0) {
|
|
209
215
|
ret.push(this.getDropColumnsSQL(tableName, Object.values(diff.removedColumns), schemaName));
|
|
@@ -263,6 +269,12 @@ export class SchemaHelper {
|
|
|
263
269
|
for (const check of Object.values(diff.changedChecks)) {
|
|
264
270
|
ret.push(this.createCheck(diff.toTable, check));
|
|
265
271
|
}
|
|
272
|
+
for (const trigger of Object.values(diff.addedTriggers)) {
|
|
273
|
+
ret.push(this.createTrigger(diff.toTable, trigger));
|
|
274
|
+
}
|
|
275
|
+
for (const trigger of Object.values(diff.changedTriggers)) {
|
|
276
|
+
ret.push(this.createTrigger(diff.toTable, trigger));
|
|
277
|
+
}
|
|
266
278
|
if ('changedComment' in diff) {
|
|
267
279
|
ret.push(this.alterTableComment(diff.toTable, diff.changedComment));
|
|
268
280
|
}
|
|
@@ -521,6 +533,9 @@ export class SchemaHelper {
|
|
|
521
533
|
for (const check of table.getChecks()) {
|
|
522
534
|
this.append(ret, this.createCheck(table, check));
|
|
523
535
|
}
|
|
536
|
+
for (const trigger of table.getTriggers()) {
|
|
537
|
+
this.append(ret, this.createTrigger(table, trigger));
|
|
538
|
+
}
|
|
524
539
|
}
|
|
525
540
|
return ret;
|
|
526
541
|
}
|
|
@@ -600,6 +615,33 @@ export class SchemaHelper {
|
|
|
600
615
|
createCheck(table, check) {
|
|
601
616
|
return `alter table ${table.getQuotedName()} add constraint ${this.quote(check.name)} check (${check.expression})`;
|
|
602
617
|
}
|
|
618
|
+
/**
|
|
619
|
+
* Generates SQL to create a database trigger on a table.
|
|
620
|
+
* Override in driver-specific helpers for custom DDL (e.g., PostgreSQL function wrapping).
|
|
621
|
+
*/
|
|
622
|
+
/* v8 ignore next 10 */
|
|
623
|
+
createTrigger(table, trigger) {
|
|
624
|
+
if (trigger.expression) {
|
|
625
|
+
return trigger.expression;
|
|
626
|
+
}
|
|
627
|
+
const timing = trigger.timing.toUpperCase();
|
|
628
|
+
const events = trigger.events.map(e => e.toUpperCase()).join(' OR ');
|
|
629
|
+
const forEach = trigger.forEach === 'statement' ? 'STATEMENT' : 'ROW';
|
|
630
|
+
const when = trigger.when ? ` when (${trigger.when})` : '';
|
|
631
|
+
return `create trigger ${this.quote(trigger.name)} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} begin ${trigger.body}; end`;
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Generates SQL to drop a database trigger from a table.
|
|
635
|
+
* Override in driver-specific helpers for custom DDL.
|
|
636
|
+
*/
|
|
637
|
+
dropTrigger(table, trigger) {
|
|
638
|
+
if (trigger.events.length > 1) {
|
|
639
|
+
return trigger.events
|
|
640
|
+
.map(event => `drop trigger if exists ${this.quote(`${trigger.name}_${event}`)}`)
|
|
641
|
+
.join(';\n');
|
|
642
|
+
}
|
|
643
|
+
return `drop trigger if exists ${this.quote(trigger.name)}`;
|
|
644
|
+
}
|
|
603
645
|
/** @internal */
|
|
604
646
|
getTableName(table, schema) {
|
|
605
647
|
if (schema && schema !== this.platform.getDefaultSchemaName()) {
|
|
@@ -300,11 +300,18 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
300
300
|
for (const check of newTable.getChecks()) {
|
|
301
301
|
this.append(sql, this.helper.createCheck(newTable, check));
|
|
302
302
|
}
|
|
303
|
+
for (const trigger of newTable.getTriggers()) {
|
|
304
|
+
this.append(sql, this.helper.createTrigger(newTable, trigger));
|
|
305
|
+
}
|
|
303
306
|
this.append(ret, sql, true);
|
|
304
307
|
}
|
|
305
308
|
}
|
|
306
309
|
if (options.dropTables && !options.safe) {
|
|
307
310
|
for (const table of Object.values(schemaDiff.removedTables)) {
|
|
311
|
+
// Drop triggers before the table so driver-specific cleanup runs (e.g. PostgreSQL function removal)
|
|
312
|
+
for (const trigger of table.getTriggers()) {
|
|
313
|
+
this.append(ret, this.helper.dropTrigger(table, trigger));
|
|
314
|
+
}
|
|
308
315
|
this.append(ret, this.helper.dropTableIfExists(table.name, table.schema));
|
|
309
316
|
}
|
|
310
317
|
if (Utils.hasObjectKeys(schemaDiff.removedTables)) {
|
package/typings.d.ts
CHANGED
|
@@ -108,6 +108,16 @@ export interface CheckDef<T = unknown> {
|
|
|
108
108
|
definition?: string;
|
|
109
109
|
columnName?: string;
|
|
110
110
|
}
|
|
111
|
+
/** Resolved trigger definition for schema operations (all callbacks resolved to strings). */
|
|
112
|
+
export interface SqlTriggerDef {
|
|
113
|
+
name: string;
|
|
114
|
+
timing: 'before' | 'after' | 'instead of';
|
|
115
|
+
events: ('insert' | 'update' | 'delete' | 'truncate')[];
|
|
116
|
+
forEach: 'row' | 'statement';
|
|
117
|
+
body: string;
|
|
118
|
+
when?: string;
|
|
119
|
+
expression?: string;
|
|
120
|
+
}
|
|
111
121
|
export interface ColumnDifference {
|
|
112
122
|
oldColumnName: string;
|
|
113
123
|
column: Column;
|
|
@@ -130,6 +140,9 @@ export interface TableDifference {
|
|
|
130
140
|
addedChecks: Dictionary<CheckDef>;
|
|
131
141
|
changedChecks: Dictionary<CheckDef>;
|
|
132
142
|
removedChecks: Dictionary<CheckDef>;
|
|
143
|
+
addedTriggers: Dictionary<SqlTriggerDef>;
|
|
144
|
+
changedTriggers: Dictionary<SqlTriggerDef>;
|
|
145
|
+
removedTriggers: Dictionary<SqlTriggerDef>;
|
|
133
146
|
addedForeignKeys: Dictionary<ForeignKey>;
|
|
134
147
|
changedForeignKeys: Dictionary<ForeignKey>;
|
|
135
148
|
removedForeignKeys: Dictionary<ForeignKey>;
|