@mikro-orm/sql 7.1.0-dev.1 → 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/AbstractSqlDriver.d.ts
CHANGED
|
@@ -52,6 +52,12 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
52
52
|
private mapJoinedProp;
|
|
53
53
|
count<T extends object>(entityName: EntityName<T>, where: any, options?: CountOptions<T>): Promise<number>;
|
|
54
54
|
nativeInsert<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
|
|
55
|
+
nativeClone<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, overrides?: EntityData<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
|
|
56
|
+
private nativeCloneSimple;
|
|
57
|
+
private nativeCloneTPT;
|
|
58
|
+
private mapCloneOverrides;
|
|
59
|
+
private buildCloneFields;
|
|
60
|
+
private getCloneableProps;
|
|
55
61
|
nativeInsertMany<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>, transform?: (sql: string) => string): Promise<QueryResult<T>>;
|
|
56
62
|
nativeUpdate<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T> & UpsertOptions<T>): Promise<QueryResult<T>>;
|
|
57
63
|
nativeUpdateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>[], data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T> & UpsertManyOptions<T>, transform?: (sql: string, params: any[]) => string): Promise<QueryResult<T>>;
|
|
@@ -77,11 +83,22 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
77
83
|
* Uses single query with join via virtual relation on pivot.
|
|
78
84
|
*/
|
|
79
85
|
protected loadPolymorphicPivotInverseSide<T extends object, O extends object>(prop: EntityProperty, owners: Primary<O>[][], where: FilterQuery<any>, orderBy?: OrderDefinition<T>, ctx?: Transaction, options?: FindOptions<T, any, any, any>): Promise<Dictionary<T[]>>;
|
|
86
|
+
/**
|
|
87
|
+
* Load a union-target polymorphic M:N pivot (e.g. Post.attachments -> Image | Video).
|
|
88
|
+
* Each pivot row's discriminator column selects which target table to hydrate.
|
|
89
|
+
*/
|
|
90
|
+
protected loadFromUnionTargetPolymorphicPivotTable<T extends object, O extends object>(prop: EntityProperty, owners: Primary<O>[][], where?: FilterQuery<any>, orderBy?: OrderDefinition<T>, ctx?: Transaction, options?: FindOptions<T, any, any, any>, pivotJoin?: boolean): Promise<Dictionary<T[]>>;
|
|
80
91
|
/**
|
|
81
92
|
* Build a map from owner PKs to their related entities from pivot table results.
|
|
82
93
|
*/
|
|
83
94
|
private buildPivotResultMap;
|
|
84
95
|
private wrapPopulateFilter;
|
|
96
|
+
/**
|
|
97
|
+
* The pivot query builder doesn't convert custom types — manually convert owner PKs to the DB
|
|
98
|
+
* representation. Returns `needsConversion` + `pkProp` so the caller can convert result FKs back
|
|
99
|
+
* to JS format for consistent key hashing in `buildPivotResultMap`.
|
|
100
|
+
*/
|
|
101
|
+
private convertOwnerPksForPivotQuery;
|
|
85
102
|
private getPivotOrderBy;
|
|
86
103
|
execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[], method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
|
|
87
104
|
stream<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T, any, any, any>): AsyncIterableIterator<T>;
|
package/AbstractSqlDriver.js
CHANGED
|
@@ -34,6 +34,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
34
34
|
return { alias, name: meta.tableName, schema: effectiveSchema, qualifiedName, toString: () => alias };
|
|
35
35
|
}
|
|
36
36
|
validateSqlOptions(options) {
|
|
37
|
+
if (options.using && !options.indexHint) {
|
|
38
|
+
const names = Utils.asArray(options.using);
|
|
39
|
+
const hint = this.platform.formatIndexHint(names);
|
|
40
|
+
if (hint) {
|
|
41
|
+
options.indexHint = hint;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
37
44
|
if (options.collation != null && typeof options.collation !== 'string') {
|
|
38
45
|
throw new Error('Collation option for SQL drivers must be a string (collation name). Use a CollationOptions object only with MongoDB.');
|
|
39
46
|
}
|
|
@@ -88,6 +95,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
88
95
|
if (options.em) {
|
|
89
96
|
await qb.applyJoinedFilters(options.em, options.filters);
|
|
90
97
|
}
|
|
98
|
+
if (options._partitionLimit) {
|
|
99
|
+
qb.setPartitionLimit(options._partitionLimit);
|
|
100
|
+
}
|
|
91
101
|
return qb;
|
|
92
102
|
}
|
|
93
103
|
async find(entityName, where, options = {}) {
|
|
@@ -574,6 +584,109 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
574
584
|
await this.processManyToMany(meta, pk, collections, false, options);
|
|
575
585
|
return res;
|
|
576
586
|
}
|
|
587
|
+
async nativeClone(entityName, where, overrides, options = {}) {
|
|
588
|
+
options.convertCustomTypes ??= true;
|
|
589
|
+
const meta = this.metadata.get(entityName);
|
|
590
|
+
if (meta.inheritanceType === 'tpt' || meta.tptParent) {
|
|
591
|
+
return this.nativeCloneTPT(meta, where, overrides, options);
|
|
592
|
+
}
|
|
593
|
+
return this.nativeCloneSimple(meta, where, overrides, options);
|
|
594
|
+
}
|
|
595
|
+
async nativeCloneSimple(meta, where, overrides, options = {}) {
|
|
596
|
+
const props = this.getCloneableProps(meta);
|
|
597
|
+
const mappedOverrides = this.mapCloneOverrides(overrides, meta, options);
|
|
598
|
+
const { selectFields, insertColumns } = this.buildCloneFields(props, mappedOverrides, meta);
|
|
599
|
+
const selectQb = this.createQueryBuilder(meta.class, options.ctx, 'read', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
600
|
+
selectQb.select(selectFields).where(where);
|
|
601
|
+
const insertQb = this.createQueryBuilder(meta.class, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
602
|
+
return this.rethrow(insertQb
|
|
603
|
+
.insertFrom(selectQb, { columns: insertColumns })
|
|
604
|
+
.execute('run', false));
|
|
605
|
+
}
|
|
606
|
+
async nativeCloneTPT(leafMeta, where, overrides, options = {}) {
|
|
607
|
+
const hierarchy = [];
|
|
608
|
+
let current = leafMeta;
|
|
609
|
+
while (current) {
|
|
610
|
+
hierarchy.unshift(current);
|
|
611
|
+
current = current.tptParent;
|
|
612
|
+
}
|
|
613
|
+
const rootMeta = hierarchy[0];
|
|
614
|
+
let newPk;
|
|
615
|
+
let rootResult;
|
|
616
|
+
for (const tableMeta of hierarchy) {
|
|
617
|
+
const props = this.getCloneableProps(tableMeta, true);
|
|
618
|
+
const mappedOverrides = this.mapCloneOverrides(overrides, tableMeta, options);
|
|
619
|
+
const { selectFields, insertColumns } = this.buildCloneFields(props, mappedOverrides, tableMeta);
|
|
620
|
+
// For child tables, prepend the new PK value
|
|
621
|
+
if (tableMeta !== rootMeta && newPk != null) {
|
|
622
|
+
for (const pkName of tableMeta.primaryKeys) {
|
|
623
|
+
const prop = tableMeta.properties[pkName];
|
|
624
|
+
for (const fieldName of prop.fieldNames) {
|
|
625
|
+
insertColumns.unshift(fieldName);
|
|
626
|
+
selectFields.unshift(raw('? as ??', [newPk, fieldName]));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const sourceWhere = tableMeta === rootMeta ? where : (Utils.extractPK(where, tableMeta) ?? where);
|
|
631
|
+
const selectQb = this.createQueryBuilder(tableMeta.class, options.ctx, 'read', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(tableMeta, options));
|
|
632
|
+
selectQb.select(selectFields).where(sourceWhere);
|
|
633
|
+
const insertQb = this.createQueryBuilder(tableMeta.class, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(tableMeta, options));
|
|
634
|
+
const res = await this.rethrow(insertQb
|
|
635
|
+
.insertFrom(selectQb, { columns: insertColumns })
|
|
636
|
+
.execute('run', false));
|
|
637
|
+
if (tableMeta === rootMeta) {
|
|
638
|
+
rootResult = res;
|
|
639
|
+
newPk = res.insertId ?? res.row?.[rootMeta.primaryKeys[0]];
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return rootResult;
|
|
643
|
+
}
|
|
644
|
+
mapCloneOverrides(overrides, meta, options) {
|
|
645
|
+
if (!overrides) {
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
return super.mapDataToFieldNames(overrides, true, meta.properties, options.convertCustomTypes);
|
|
649
|
+
}
|
|
650
|
+
buildCloneFields(props, mappedOverrides, meta) {
|
|
651
|
+
const selectFields = [];
|
|
652
|
+
const insertColumns = [];
|
|
653
|
+
for (const prop of props) {
|
|
654
|
+
for (const fieldName of prop.fieldNames) {
|
|
655
|
+
insertColumns.push(fieldName);
|
|
656
|
+
if (mappedOverrides && fieldName in mappedOverrides) {
|
|
657
|
+
selectFields.push(raw('? as ??', [mappedOverrides[fieldName], fieldName]));
|
|
658
|
+
}
|
|
659
|
+
else if (meta.versionProperty === prop.name) {
|
|
660
|
+
const initial = prop.runtimeType === 'Date' ? new Date() : 1;
|
|
661
|
+
selectFields.push(raw('? as ??', [initial, fieldName]));
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
selectFields.push(fieldName);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return { selectFields, insertColumns };
|
|
669
|
+
}
|
|
670
|
+
getCloneableProps(meta, ownProps) {
|
|
671
|
+
return (ownProps ? (meta.ownProps ?? meta.props) : meta.props).filter(prop => {
|
|
672
|
+
if (prop.persist === false) {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
if (prop.primary) {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
if (!prop.fieldNames?.length) {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
return true;
|
|
688
|
+
});
|
|
689
|
+
}
|
|
577
690
|
async nativeInsertMany(entityName, data, options = {}, transform) {
|
|
578
691
|
options.processCollections ??= true;
|
|
579
692
|
options.convertCustomTypes ??= true;
|
|
@@ -996,8 +1109,29 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
996
1109
|
const pks = wrapped.getPrimaryKeys(true);
|
|
997
1110
|
const snap = coll.getSnapshot();
|
|
998
1111
|
const includes = (arr, item) => !!arr.find(i => this.comparePrimaryKeyArrays(i, item));
|
|
999
|
-
|
|
1000
|
-
|
|
1112
|
+
// For union-target polymorphic M:N, prepend the per-row discriminator value so the pivot
|
|
1113
|
+
// persister can write it alongside the FK id. Memoized per sync-run because a collection can
|
|
1114
|
+
// hold hundreds of items of the same few types, and findDiscriminatorValue walks the prototype
|
|
1115
|
+
// chain + re-scans Object.entries each call.
|
|
1116
|
+
const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(coll.property);
|
|
1117
|
+
const classToDisc = new Map();
|
|
1118
|
+
const toDiff = (item) => {
|
|
1119
|
+
const keys = helper(item).getPrimaryKeys(true);
|
|
1120
|
+
if (!isUnionTargetMN) {
|
|
1121
|
+
return keys;
|
|
1122
|
+
}
|
|
1123
|
+
let disc = classToDisc.get(item.constructor);
|
|
1124
|
+
if (!classToDisc.has(item.constructor)) {
|
|
1125
|
+
disc = QueryHelper.findDiscriminatorValue(coll.property.discriminatorMap, item.constructor);
|
|
1126
|
+
if (disc === undefined) {
|
|
1127
|
+
throw new Error(`Cannot resolve discriminator value for ${item.constructor.name} in ${coll.property.name}; the class is not part of the union target list.`);
|
|
1128
|
+
}
|
|
1129
|
+
classToDisc.set(item.constructor, disc);
|
|
1130
|
+
}
|
|
1131
|
+
return [disc, ...keys];
|
|
1132
|
+
};
|
|
1133
|
+
const snapshot = snap ? snap.map(toDiff) : [];
|
|
1134
|
+
const current = coll.getItems(false).map(toDiff);
|
|
1001
1135
|
const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
|
|
1002
1136
|
const insertDiff = current.filter(item => !includes(snapshot, item));
|
|
1003
1137
|
const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
|
|
@@ -1069,21 +1203,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1069
1203
|
return {};
|
|
1070
1204
|
}
|
|
1071
1205
|
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1206
|
+
if (prop.discriminatorColumn && QueryHelper.isUnionTargetPolymorphic(prop)) {
|
|
1207
|
+
return this.loadFromUnionTargetPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
|
|
1208
|
+
}
|
|
1072
1209
|
if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
|
|
1073
1210
|
return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
|
|
1074
1211
|
}
|
|
1075
1212
|
const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
|
|
1076
1213
|
const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
|
|
1077
1214
|
const ownerMeta = pivotProp2.targetMeta;
|
|
1078
|
-
|
|
1079
|
-
// convert owner PKs to DB format for the query and convert result FKs back to
|
|
1080
|
-
// JS format for consistent key hashing in buildPivotResultMap.
|
|
1081
|
-
const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
|
|
1082
|
-
const needsConversion = pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
|
|
1083
|
-
let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
|
|
1084
|
-
if (needsConversion) {
|
|
1085
|
-
ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
|
|
1086
|
-
}
|
|
1215
|
+
const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
|
|
1087
1216
|
const cond = {
|
|
1088
1217
|
[pivotProp2.name]: { $in: ownerPks },
|
|
1089
1218
|
};
|
|
@@ -1098,7 +1227,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1098
1227
|
const fields = pivotJoin
|
|
1099
1228
|
? [pivotProp1.name, pivotProp2.name]
|
|
1100
1229
|
: [pivotProp1.name, pivotProp2.name, ...childFields];
|
|
1101
|
-
const
|
|
1230
|
+
const pivotFindOptions = {
|
|
1102
1231
|
ctx,
|
|
1103
1232
|
...options,
|
|
1104
1233
|
fields,
|
|
@@ -1114,10 +1243,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1114
1243
|
},
|
|
1115
1244
|
],
|
|
1116
1245
|
populateWhere: undefined,
|
|
1117
|
-
// @ts-ignore
|
|
1118
1246
|
_populateWhere: 'infer',
|
|
1119
1247
|
populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
|
|
1120
|
-
}
|
|
1248
|
+
};
|
|
1249
|
+
if (pivotFindOptions._partitionLimit) {
|
|
1250
|
+
pivotFindOptions._partitionLimit.partitionBy = pivotProp2.name;
|
|
1251
|
+
}
|
|
1252
|
+
const res = await this.find(pivotMeta.class, where, pivotFindOptions);
|
|
1121
1253
|
// Convert result FK values back to JS format so key hashing
|
|
1122
1254
|
// in buildPivotResultMap is consistent with the owner keys.
|
|
1123
1255
|
if (needsConversion) {
|
|
@@ -1237,6 +1369,102 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1237
1369
|
});
|
|
1238
1370
|
return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
|
|
1239
1371
|
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Load a union-target polymorphic M:N pivot (e.g. Post.attachments -> Image | Video).
|
|
1374
|
+
* Each pivot row's discriminator column selects which target table to hydrate.
|
|
1375
|
+
*/
|
|
1376
|
+
async loadFromUnionTargetPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options = {}, pivotJoin) {
|
|
1377
|
+
// :ref hints cannot be honored for union-target — EntityLoader.getReference needs a concrete
|
|
1378
|
+
// class per item, but the ref-mode map only carries flat PK values and would hydrate every
|
|
1379
|
+
// row as the first polymorph target. Fail loudly instead of silently corrupting the collection.
|
|
1380
|
+
if (pivotJoin) {
|
|
1381
|
+
throw new Error(`The ':ref' populate hint is not supported for union-target polymorphic M:N on ${prop.name}. Use a regular populate hint instead.`);
|
|
1382
|
+
}
|
|
1383
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1384
|
+
const targets = prop.polymorphTargets;
|
|
1385
|
+
const ownerProp = pivotMeta.relations.find(r => r.persist !== false && !r.polymorphic);
|
|
1386
|
+
const discriminatorColumn = prop.discriminatorColumn;
|
|
1387
|
+
const ownerMeta = ownerProp.targetMeta;
|
|
1388
|
+
const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
|
|
1389
|
+
const pivotRows = (await this.find(pivotMeta.class, { [ownerProp.name]: { $in: ownerPks } }, {
|
|
1390
|
+
ctx,
|
|
1391
|
+
orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
|
|
1392
|
+
fields: [ownerProp.name, discriminatorColumn, prop.discriminator],
|
|
1393
|
+
populateWhere: undefined,
|
|
1394
|
+
// @ts-ignore
|
|
1395
|
+
_populateWhere: 'infer',
|
|
1396
|
+
}));
|
|
1397
|
+
/* v8 ignore next 7 - custom-type PK conversion, tested via loadFromPivotTable path */
|
|
1398
|
+
if (needsConversion) {
|
|
1399
|
+
for (const item of pivotRows) {
|
|
1400
|
+
const fk = item[ownerProp.name];
|
|
1401
|
+
if (fk != null) {
|
|
1402
|
+
item[ownerProp.name] = pkProp.customType.convertToJSValue(fk, this.platform);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
const classMeta = new Map(targets.map(t => [t.class, t]));
|
|
1407
|
+
const rowsByTarget = new Map();
|
|
1408
|
+
for (const row of pivotRows) {
|
|
1409
|
+
const discValue = row[discriminatorColumn];
|
|
1410
|
+
const targetClass = prop.discriminatorMap[discValue];
|
|
1411
|
+
const targetMeta = classMeta.get(targetClass);
|
|
1412
|
+
/* v8 ignore next 3 - defensive: unknown discriminator value */
|
|
1413
|
+
if (!targetMeta) {
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
const list = rowsByTarget.get(targetMeta) ?? [];
|
|
1417
|
+
list.push(row);
|
|
1418
|
+
rowsByTarget.set(targetMeta, list);
|
|
1419
|
+
}
|
|
1420
|
+
// Strip the outer find's orderBy/fields/exclude before bulk-loading targets by PK — those apply
|
|
1421
|
+
// to the owner query, not each polymorph target (Image and Video wouldn't share an orderBy field).
|
|
1422
|
+
// populateFilter is a filter on the populated collection; since union-target splits the pivot
|
|
1423
|
+
// and target queries, we merge it into the target-level `where` instead of wrapping it on the
|
|
1424
|
+
// pivot query (where joins to target tables aren't available).
|
|
1425
|
+
// Hoisted above the loop since `options` doesn't change per target.
|
|
1426
|
+
const { orderBy: _o, fields: _f, exclude: _e, populateFilter, ...childOptions } = options;
|
|
1427
|
+
const populate = options.populate ?? [];
|
|
1428
|
+
const orphanedRows = new Set();
|
|
1429
|
+
for (const [targetMeta, rows] of rowsByTarget) {
|
|
1430
|
+
const targetIds = rows.map(r => r[prop.discriminator]);
|
|
1431
|
+
// Union-target pivot stores one scalar FK per row; composite-PK targets are rejected at
|
|
1432
|
+
// metadata validation time, so a single primary key column is guaranteed here.
|
|
1433
|
+
const pkCol = targetMeta.primaryKeys[0];
|
|
1434
|
+
let cond = { [pkCol]: { $in: targetIds } };
|
|
1435
|
+
if (!Utils.isEmpty(where)) {
|
|
1436
|
+
cond = { $and: [cond, where] };
|
|
1437
|
+
}
|
|
1438
|
+
if (!Utils.isEmpty(populateFilter)) {
|
|
1439
|
+
cond = { $and: [cond, populateFilter] };
|
|
1440
|
+
}
|
|
1441
|
+
const results = (await this.find(targetMeta.class, cond, {
|
|
1442
|
+
ctx,
|
|
1443
|
+
...childOptions,
|
|
1444
|
+
populate: populate,
|
|
1445
|
+
}));
|
|
1446
|
+
const byPk = new Map();
|
|
1447
|
+
for (const row of results) {
|
|
1448
|
+
Object.defineProperty(row, 'constructor', {
|
|
1449
|
+
value: targetMeta.class,
|
|
1450
|
+
enumerable: false,
|
|
1451
|
+
configurable: true,
|
|
1452
|
+
});
|
|
1453
|
+
byPk.set(Utils.getPrimaryKeyHash([row[pkCol]]), row);
|
|
1454
|
+
}
|
|
1455
|
+
for (const row of rows) {
|
|
1456
|
+
const pkHash = Utils.getPrimaryKeyHash([row[prop.discriminator]]);
|
|
1457
|
+
const entity = byPk.get(pkHash);
|
|
1458
|
+
if (entity == null) {
|
|
1459
|
+
orphanedRows.add(row);
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
row[prop.discriminator] = entity;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
const result = orphanedRows.size > 0 ? pivotRows.filter(r => !orphanedRows.has(r)) : pivotRows;
|
|
1466
|
+
return this.buildPivotResultMap(owners, result, ownerProp.name, prop.discriminator);
|
|
1467
|
+
}
|
|
1240
1468
|
/**
|
|
1241
1469
|
* Build a map from owner PKs to their related entities from pivot table results.
|
|
1242
1470
|
*/
|
|
@@ -1261,6 +1489,21 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1261
1489
|
}
|
|
1262
1490
|
return undefined;
|
|
1263
1491
|
}
|
|
1492
|
+
/**
|
|
1493
|
+
* The pivot query builder doesn't convert custom types — manually convert owner PKs to the DB
|
|
1494
|
+
* representation. Returns `needsConversion` + `pkProp` so the caller can convert result FKs back
|
|
1495
|
+
* to JS format for consistent key hashing in `buildPivotResultMap`.
|
|
1496
|
+
*/
|
|
1497
|
+
convertOwnerPksForPivotQuery(owners, ownerMeta) {
|
|
1498
|
+
const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
|
|
1499
|
+
const needsConversion = !!pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
|
|
1500
|
+
let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
|
|
1501
|
+
/* v8 ignore next 4 - custom-type PK conversion, tested via loadFromPivotTable path */
|
|
1502
|
+
if (needsConversion) {
|
|
1503
|
+
ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
|
|
1504
|
+
}
|
|
1505
|
+
return { ownerPks, needsConversion, pkProp };
|
|
1506
|
+
}
|
|
1264
1507
|
getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
|
|
1265
1508
|
if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
|
|
1266
1509
|
return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
|
|
@@ -1327,6 +1570,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1327
1570
|
if (ref && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
|
|
1328
1571
|
return true;
|
|
1329
1572
|
}
|
|
1573
|
+
// Union-target polymorphic M:N cannot be loaded via a single JOIN because rows span multiple
|
|
1574
|
+
// target tables; fall through to SELECT_IN which dispatches through `loadFromPivotTable`.
|
|
1575
|
+
// Polymorphic M:1 (to-one with target_type discriminator) is handled via LEFT JOINs elsewhere.
|
|
1576
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY && QueryHelper.isUnionTargetPolymorphic(prop) && prop.owner) {
|
|
1577
|
+
return false;
|
|
1578
|
+
}
|
|
1330
1579
|
// skip redundant joins for 1:1 owner population hints when using `mapToPk`
|
|
1331
1580
|
if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
|
|
1332
1581
|
return false;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { QueryHelper, } from '@mikro-orm/core';
|
|
1
2
|
class InsertStatement {
|
|
2
3
|
order;
|
|
3
4
|
#keys;
|
|
@@ -106,6 +107,16 @@ export class PivotCollectionPersister {
|
|
|
106
107
|
buildPivotKeysAndData(prop, fks, pks, deleteAll = false) {
|
|
107
108
|
let data;
|
|
108
109
|
let keys;
|
|
110
|
+
// Union-target polymorphic M:N prepends the per-row discriminator to `fks` in syncCollections;
|
|
111
|
+
// Rails-style polymorphic M:N uses a static discriminatorValue on the prop. Normalize to a single
|
|
112
|
+
// "current row's discriminator" value so the prepend block below handles both cases uniformly.
|
|
113
|
+
let rowDiscriminator;
|
|
114
|
+
if (QueryHelper.isUnionTargetPolymorphic(prop) && !deleteAll && fks.length > 0) {
|
|
115
|
+
[rowDiscriminator, ...fks] = fks;
|
|
116
|
+
}
|
|
117
|
+
else if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
|
|
118
|
+
rowDiscriminator = prop.discriminatorValue;
|
|
119
|
+
}
|
|
109
120
|
if (deleteAll) {
|
|
110
121
|
data = pks;
|
|
111
122
|
keys = prop.joinColumns;
|
|
@@ -116,8 +127,8 @@ export class PivotCollectionPersister {
|
|
|
116
127
|
? [...prop.inverseJoinColumns, ...prop.joinColumns]
|
|
117
128
|
: [...prop.joinColumns, ...prop.inverseJoinColumns];
|
|
118
129
|
}
|
|
119
|
-
if (
|
|
120
|
-
data = [
|
|
130
|
+
if (rowDiscriminator !== undefined) {
|
|
131
|
+
data = [rowDiscriminator, ...data];
|
|
121
132
|
keys = [prop.discriminatorColumn, ...keys];
|
|
122
133
|
}
|
|
123
134
|
return { data, keys };
|
package/SqlEntityManager.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type EntitySchemaWithMeta, EntityManager, type AnyEntity, type ConnectionType, type EntityData, type EntityName, type EntityRepository, type
|
|
1
|
+
import { type EntitySchemaWithMeta, EntityManager, type AnyEntity, type ConnectionType, type CountByOptions, type Dictionary, type EntityData, type EntityKey, type EntityName, type EntityRepository, type FilterQuery, type GetRepository, type LoggingOptions, type QueryResult, type RawQueryFragment } from '@mikro-orm/core';
|
|
2
2
|
import type { AbstractSqlDriver } from './AbstractSqlDriver.js';
|
|
3
3
|
import type { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
|
|
4
4
|
import type { QueryBuilder } from './query/QueryBuilder.js';
|
|
@@ -29,6 +29,10 @@ export declare class SqlEntityManager<Driver extends AbstractSqlDriver = Abstrac
|
|
|
29
29
|
getKysely<TDB = undefined, TOptions extends GetKyselyOptions = GetKyselyOptions>(options?: TOptions): Kysely<TDB extends undefined ? InferKyselyDB<EntitiesFromManager<this>, TOptions> & InferClassEntityDB<AllEntitiesFromManager<this>, TOptions> : TDB>;
|
|
30
30
|
/** Executes a raw SQL query, using the current transaction context if available. */
|
|
31
31
|
execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[], method?: 'all' | 'get' | 'run', loggerContext?: LoggingOptions): Promise<T>;
|
|
32
|
+
/**
|
|
33
|
+
* @inheritDoc
|
|
34
|
+
*/
|
|
35
|
+
countBy<Entity extends object>(entityName: EntityName<Entity>, groupBy: EntityKey<Entity> | readonly EntityKey<Entity>[], options?: CountByOptions<Entity>): Promise<Dictionary<number>>;
|
|
32
36
|
getRepository<T extends object, U extends EntityRepository<T> = SqlEntityRepository<T>>(entityName: EntityName<T>): GetRepository<T, U>;
|
|
33
37
|
protected applyDiscriminatorCondition<Entity extends object>(entityName: EntityName<Entity>, where: FilterQuery<Entity>): FilterQuery<Entity>;
|
|
34
38
|
}
|
package/SqlEntityManager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityManager, } from '@mikro-orm/core';
|
|
1
|
+
import { EntityManager, raw, Utils, } from '@mikro-orm/core';
|
|
2
2
|
import { MikroKyselyPlugin } from './plugin/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* @inheritDoc
|
|
@@ -35,6 +35,41 @@ export class SqlEntityManager extends EntityManager {
|
|
|
35
35
|
async execute(query, params = [], method = 'all', loggerContext) {
|
|
36
36
|
return this.getDriver().execute(query, params, method, this.getContext(false).getTransactionContext(), loggerContext);
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* @inheritDoc
|
|
40
|
+
*/
|
|
41
|
+
async countBy(entityName, groupBy, options = {}) {
|
|
42
|
+
const em = this.getContext(false);
|
|
43
|
+
options = { ...options };
|
|
44
|
+
em.prepareOptions(options);
|
|
45
|
+
const meta = em.getMetadata().find(entityName);
|
|
46
|
+
const fields = Utils.asArray(groupBy);
|
|
47
|
+
const { where: rawWhere, ...countOptions } = options;
|
|
48
|
+
await em.tryFlush(entityName, options);
|
|
49
|
+
const where = await em.processWhere(entityName, rawWhere ?? {}, options, 'read');
|
|
50
|
+
const qb = em.createQueryBuilder(meta.class);
|
|
51
|
+
qb
|
|
52
|
+
.select([...fields, raw('count(*) as cnt')])
|
|
53
|
+
.where(where)
|
|
54
|
+
.groupBy(fields);
|
|
55
|
+
if (countOptions.having) {
|
|
56
|
+
qb.having(countOptions.having);
|
|
57
|
+
}
|
|
58
|
+
if (countOptions.schema) {
|
|
59
|
+
qb.withSchema(countOptions.schema);
|
|
60
|
+
}
|
|
61
|
+
const rows = await qb.execute('all', { mapResults: false });
|
|
62
|
+
const results = {};
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
const keyParts = fields.map(f => {
|
|
65
|
+
const col = meta.properties[f]?.fieldNames?.[0] ?? f;
|
|
66
|
+
return String(row[col]);
|
|
67
|
+
});
|
|
68
|
+
const key = keyParts.join(Utils.PK_SEPARATOR);
|
|
69
|
+
results[key] = +row.cnt;
|
|
70
|
+
}
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
38
73
|
getRepository(entityName) {
|
|
39
74
|
return super.getRepository(entityName);
|
|
40
75
|
}
|
|
@@ -56,6 +56,10 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
|
|
|
56
56
|
return this.combineParts();
|
|
57
57
|
}
|
|
58
58
|
compileInsert() {
|
|
59
|
+
if (this.options.insertSubQuery) {
|
|
60
|
+
super.compileInsert();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
59
63
|
if (!this.options.data) {
|
|
60
64
|
throw new Error('No data provided');
|
|
61
65
|
}
|
|
@@ -17,6 +17,7 @@ export declare class BaseMySqlPlatform extends AbstractSqlPlatform {
|
|
|
17
17
|
supportsMultiColumnCountDistinct(): boolean;
|
|
18
18
|
/** @internal */
|
|
19
19
|
createNativeQueryBuilder(): MySqlNativeQueryBuilder;
|
|
20
|
+
formatIndexHint(indexNames: string[]): string;
|
|
20
21
|
getDefaultCharset(): string;
|
|
21
22
|
init(orm: MikroORM): void;
|
|
22
23
|
getBeginTransactionSQL(options?: {
|
|
@@ -25,6 +25,9 @@ export class BaseMySqlPlatform extends AbstractSqlPlatform {
|
|
|
25
25
|
createNativeQueryBuilder() {
|
|
26
26
|
return new MySqlNativeQueryBuilder(this);
|
|
27
27
|
}
|
|
28
|
+
formatIndexHint(indexNames) {
|
|
29
|
+
return `use index(${indexNames.join(', ')})`;
|
|
30
|
+
}
|
|
28
31
|
getDefaultCharset() {
|
|
29
32
|
return 'utf8mb4';
|
|
30
33
|
}
|
|
@@ -3,6 +3,17 @@ import { NativeQueryBuilder } from '../../query/NativeQueryBuilder.js';
|
|
|
3
3
|
/** @internal */
|
|
4
4
|
export class MySqlNativeQueryBuilder extends NativeQueryBuilder {
|
|
5
5
|
compileInsert() {
|
|
6
|
+
if (this.options.insertSubQuery) {
|
|
7
|
+
super.compileInsert();
|
|
8
|
+
// Inject 'ignore' after 'insert' for MySQL's INSERT IGNORE ... SELECT syntax
|
|
9
|
+
if (this.options.onConflict?.ignore) {
|
|
10
|
+
const insertIdx = this.parts.indexOf('insert');
|
|
11
|
+
if (insertIdx >= 0) {
|
|
12
|
+
this.parts.splice(insertIdx + 1, 0, 'ignore');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
6
17
|
if (!this.options.data) {
|
|
7
18
|
throw new Error('No data provided');
|
|
8
19
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Dictionary, type Transaction, type Type } from '@mikro-orm/core';
|
|
2
|
-
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference } from '../../typings.js';
|
|
2
|
+
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, SqlTriggerDef } from '../../typings.js';
|
|
3
3
|
import type { AbstractSqlConnection } from '../../AbstractSqlConnection.js';
|
|
4
4
|
import { SchemaHelper } from '../../schema/SchemaHelper.js';
|
|
5
5
|
import type { DatabaseSchema } from '../../schema/DatabaseSchema.js';
|
|
@@ -32,6 +32,9 @@ export declare class MySqlSchemaHelper extends SchemaHelper {
|
|
|
32
32
|
protected appendMySqlIndexSuffix(sql: string, index: IndexDef): string;
|
|
33
33
|
getAllColumns(connection: AbstractSqlConnection, tables: Table[], ctx?: Transaction): Promise<Dictionary<Column[]>>;
|
|
34
34
|
getAllChecks(connection: AbstractSqlConnection, tables: Table[], ctx?: Transaction): Promise<Dictionary<CheckDef[]>>;
|
|
35
|
+
/** Generates SQL to create MySQL triggers. MySQL requires one trigger per event. */
|
|
36
|
+
createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
37
|
+
getAllTriggers(connection: AbstractSqlConnection, tables: Table[]): Promise<Dictionary<SqlTriggerDef[]>>;
|
|
35
38
|
getAllForeignKeys(connection: AbstractSqlConnection, tables: Table[], ctx?: Transaction): Promise<Dictionary<Dictionary<ForeignKey>>>;
|
|
36
39
|
getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
|
|
37
40
|
getRenameColumnSQL(tableName: string, oldColumnName: string, to: Column): string;
|
|
@@ -64,11 +64,15 @@ export class MySqlSchemaHelper extends SchemaHelper {
|
|
|
64
64
|
const checks = await this.getAllChecks(connection, tables, ctx);
|
|
65
65
|
const fks = await this.getAllForeignKeys(connection, tables, ctx);
|
|
66
66
|
const enums = await this.getAllEnumDefinitions(connection, tables, ctx);
|
|
67
|
+
const triggers = await this.getAllTriggers(connection, tables);
|
|
67
68
|
for (const t of tables) {
|
|
68
69
|
const key = this.getTableKey(t);
|
|
69
70
|
const table = schema.addTable(t.table_name, t.schema_name, t.table_comment);
|
|
70
71
|
const pks = await this.getPrimaryKeys(connection, indexes[key], table.name, table.schema);
|
|
71
72
|
table.init(columns[key], indexes[key], checks[key], pks, fks[key], enums[key]);
|
|
73
|
+
if (triggers[key]) {
|
|
74
|
+
table.setTriggers(triggers[key]);
|
|
75
|
+
}
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
78
|
async getAllIndexes(connection, tables, ctx) {
|
|
@@ -264,6 +268,85 @@ export class MySqlSchemaHelper extends SchemaHelper {
|
|
|
264
268
|
}
|
|
265
269
|
return ret;
|
|
266
270
|
}
|
|
271
|
+
/** Generates SQL to create MySQL triggers. MySQL requires one trigger per event. */
|
|
272
|
+
createTrigger(table, trigger) {
|
|
273
|
+
if (trigger.expression) {
|
|
274
|
+
return trigger.expression;
|
|
275
|
+
}
|
|
276
|
+
/* v8 ignore next 3 */
|
|
277
|
+
if (trigger.timing === 'instead of') {
|
|
278
|
+
throw new Error(`MySQL does not support INSTEAD OF triggers. Use BEFORE or AFTER for trigger "${trigger.name}".`);
|
|
279
|
+
}
|
|
280
|
+
/* v8 ignore next 5 */
|
|
281
|
+
if (trigger.forEach === 'statement') {
|
|
282
|
+
throw new Error(`MySQL does not support FOR EACH STATEMENT triggers. Use FOR EACH ROW for trigger "${trigger.name}".`);
|
|
283
|
+
}
|
|
284
|
+
const timing = trigger.timing.toUpperCase();
|
|
285
|
+
const ret = [];
|
|
286
|
+
for (const event of trigger.events) {
|
|
287
|
+
const name = trigger.events.length > 1 ? `${trigger.name}_${event}` : trigger.name;
|
|
288
|
+
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ROW begin ${trigger.body}; end`);
|
|
289
|
+
}
|
|
290
|
+
return ret.join(';\n');
|
|
291
|
+
}
|
|
292
|
+
async getAllTriggers(connection, tables) {
|
|
293
|
+
const names = tables.map(t => this.platform.quoteValue(t.table_name)).join(', ');
|
|
294
|
+
const sql = `select trigger_name as trigger_name, event_object_table as table_name, nullif(event_object_schema, schema()) as schema_name,
|
|
295
|
+
event_manipulation as event, action_timing as timing,
|
|
296
|
+
action_orientation as for_each, action_statement as body
|
|
297
|
+
from information_schema.triggers
|
|
298
|
+
where event_object_schema = database()
|
|
299
|
+
and event_object_table in (${names})
|
|
300
|
+
order by trigger_name, event_manipulation`;
|
|
301
|
+
const allTriggers = await connection.execute(sql);
|
|
302
|
+
const ret = {};
|
|
303
|
+
// First pass: collect all raw trigger names per table to detect multi-event groups.
|
|
304
|
+
// A base name is only used for grouping if multiple triggers share it (e.g. trg_multi_insert + trg_multi_update).
|
|
305
|
+
const namesByTable = new Map();
|
|
306
|
+
for (const row of allTriggers) {
|
|
307
|
+
const key = this.getTableKey(row);
|
|
308
|
+
namesByTable.set(key, [...(namesByTable.get(key) ?? []), row.trigger_name]);
|
|
309
|
+
}
|
|
310
|
+
const triggerMap = new Map();
|
|
311
|
+
for (const row of allTriggers) {
|
|
312
|
+
const key = this.getTableKey(row);
|
|
313
|
+
const eventLower = row.event.toLowerCase();
|
|
314
|
+
const tableNames = namesByTable.get(key) ?? [];
|
|
315
|
+
// Only strip event suffix when another trigger with the same base exists for this table
|
|
316
|
+
const candidateBase = row.trigger_name.endsWith(`_${eventLower}`)
|
|
317
|
+
? row.trigger_name.slice(0, -eventLower.length - 1)
|
|
318
|
+
: null;
|
|
319
|
+
const baseName = candidateBase && tableNames.some(n => n !== row.trigger_name && n.startsWith(`${candidateBase}_`))
|
|
320
|
+
? candidateBase
|
|
321
|
+
: row.trigger_name;
|
|
322
|
+
const dedupeKey = `${key}:${baseName}`;
|
|
323
|
+
if (triggerMap.has(dedupeKey)) {
|
|
324
|
+
const existing = triggerMap.get(dedupeKey);
|
|
325
|
+
const event = eventLower;
|
|
326
|
+
if (!existing.events.includes(event)) {
|
|
327
|
+
existing.events.push(event);
|
|
328
|
+
}
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
ret[key] ??= [];
|
|
332
|
+
// Strip BEGIN/END wrapper from MySQL action_statement
|
|
333
|
+
let body = row.body ?? '';
|
|
334
|
+
const beginEndMatch = /^\s*begin\s+([\s\S]*)\s*end\s*$/i.exec(body);
|
|
335
|
+
if (beginEndMatch) {
|
|
336
|
+
body = beginEndMatch[1].trim().replace(/;\s*$/, '');
|
|
337
|
+
}
|
|
338
|
+
const trigger = {
|
|
339
|
+
name: baseName,
|
|
340
|
+
timing: row.timing.toLowerCase(),
|
|
341
|
+
events: [eventLower],
|
|
342
|
+
forEach: (row.for_each ?? 'row').toLowerCase(),
|
|
343
|
+
body,
|
|
344
|
+
};
|
|
345
|
+
ret[key].push(trigger);
|
|
346
|
+
triggerMap.set(dedupeKey, trigger);
|
|
347
|
+
}
|
|
348
|
+
return ret;
|
|
349
|
+
}
|
|
267
350
|
async getAllForeignKeys(connection, tables, ctx) {
|
|
268
351
|
const sql = `select k.constraint_name as constraint_name, nullif(k.table_schema, schema()) as schema_name, k.table_name as table_name, k.column_name as column_name, k.referenced_table_name as referenced_table_name, k.referenced_column_name as referenced_column_name, c.update_rule as update_rule, c.delete_rule as delete_rule
|
|
269
352
|
from information_schema.key_column_usage k
|