@mikro-orm/sql 7.1.0-dev.5 → 7.1.0-dev.7
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
CHANGED
|
@@ -83,11 +83,22 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
83
83
|
* Uses single query with join via virtual relation on pivot.
|
|
84
84
|
*/
|
|
85
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[]>>;
|
|
86
91
|
/**
|
|
87
92
|
* Build a map from owner PKs to their related entities from pivot table results.
|
|
88
93
|
*/
|
|
89
94
|
private buildPivotResultMap;
|
|
90
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;
|
|
91
102
|
private getPivotOrderBy;
|
|
92
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>;
|
|
93
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
|
}
|
|
@@ -1099,8 +1106,29 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1099
1106
|
const pks = wrapped.getPrimaryKeys(true);
|
|
1100
1107
|
const snap = coll.getSnapshot();
|
|
1101
1108
|
const includes = (arr, item) => !!arr.find(i => this.comparePrimaryKeyArrays(i, item));
|
|
1102
|
-
|
|
1103
|
-
|
|
1109
|
+
// For union-target polymorphic M:N, prepend the per-row discriminator value so the pivot
|
|
1110
|
+
// persister can write it alongside the FK id. Memoized per sync-run because a collection can
|
|
1111
|
+
// hold hundreds of items of the same few types, and findDiscriminatorValue walks the prototype
|
|
1112
|
+
// chain + re-scans Object.entries each call.
|
|
1113
|
+
const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(coll.property);
|
|
1114
|
+
const classToDisc = new Map();
|
|
1115
|
+
const toDiff = (item) => {
|
|
1116
|
+
const keys = helper(item).getPrimaryKeys(true);
|
|
1117
|
+
if (!isUnionTargetMN) {
|
|
1118
|
+
return keys;
|
|
1119
|
+
}
|
|
1120
|
+
let disc = classToDisc.get(item.constructor);
|
|
1121
|
+
if (!classToDisc.has(item.constructor)) {
|
|
1122
|
+
disc = QueryHelper.findDiscriminatorValue(coll.property.discriminatorMap, item.constructor);
|
|
1123
|
+
if (disc === undefined) {
|
|
1124
|
+
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.`);
|
|
1125
|
+
}
|
|
1126
|
+
classToDisc.set(item.constructor, disc);
|
|
1127
|
+
}
|
|
1128
|
+
return [disc, ...keys];
|
|
1129
|
+
};
|
|
1130
|
+
const snapshot = snap ? snap.map(toDiff) : [];
|
|
1131
|
+
const current = coll.getItems(false).map(toDiff);
|
|
1104
1132
|
const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
|
|
1105
1133
|
const insertDiff = current.filter(item => !includes(snapshot, item));
|
|
1106
1134
|
const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
|
|
@@ -1172,21 +1200,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1172
1200
|
return {};
|
|
1173
1201
|
}
|
|
1174
1202
|
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1203
|
+
if (prop.discriminatorColumn && QueryHelper.isUnionTargetPolymorphic(prop)) {
|
|
1204
|
+
return this.loadFromUnionTargetPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
|
|
1205
|
+
}
|
|
1175
1206
|
if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
|
|
1176
1207
|
return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
|
|
1177
1208
|
}
|
|
1178
1209
|
const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
|
|
1179
1210
|
const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
|
|
1180
1211
|
const ownerMeta = pivotProp2.targetMeta;
|
|
1181
|
-
|
|
1182
|
-
// convert owner PKs to DB format for the query and convert result FKs back to
|
|
1183
|
-
// JS format for consistent key hashing in buildPivotResultMap.
|
|
1184
|
-
const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
|
|
1185
|
-
const needsConversion = pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
|
|
1186
|
-
let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
|
|
1187
|
-
if (needsConversion) {
|
|
1188
|
-
ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
|
|
1189
|
-
}
|
|
1212
|
+
const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
|
|
1190
1213
|
const cond = {
|
|
1191
1214
|
[pivotProp2.name]: { $in: ownerPks },
|
|
1192
1215
|
};
|
|
@@ -1340,6 +1363,102 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1340
1363
|
});
|
|
1341
1364
|
return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
|
|
1342
1365
|
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Load a union-target polymorphic M:N pivot (e.g. Post.attachments -> Image | Video).
|
|
1368
|
+
* Each pivot row's discriminator column selects which target table to hydrate.
|
|
1369
|
+
*/
|
|
1370
|
+
async loadFromUnionTargetPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options = {}, pivotJoin) {
|
|
1371
|
+
// :ref hints cannot be honored for union-target — EntityLoader.getReference needs a concrete
|
|
1372
|
+
// class per item, but the ref-mode map only carries flat PK values and would hydrate every
|
|
1373
|
+
// row as the first polymorph target. Fail loudly instead of silently corrupting the collection.
|
|
1374
|
+
if (pivotJoin) {
|
|
1375
|
+
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.`);
|
|
1376
|
+
}
|
|
1377
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1378
|
+
const targets = prop.polymorphTargets;
|
|
1379
|
+
const ownerProp = pivotMeta.relations.find(r => r.persist !== false && !r.polymorphic);
|
|
1380
|
+
const discriminatorColumn = prop.discriminatorColumn;
|
|
1381
|
+
const ownerMeta = ownerProp.targetMeta;
|
|
1382
|
+
const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
|
|
1383
|
+
const pivotRows = (await this.find(pivotMeta.class, { [ownerProp.name]: { $in: ownerPks } }, {
|
|
1384
|
+
ctx,
|
|
1385
|
+
orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
|
|
1386
|
+
fields: [ownerProp.name, discriminatorColumn, prop.discriminator],
|
|
1387
|
+
populateWhere: undefined,
|
|
1388
|
+
// @ts-ignore
|
|
1389
|
+
_populateWhere: 'infer',
|
|
1390
|
+
}));
|
|
1391
|
+
/* v8 ignore next 7 - custom-type PK conversion, tested via loadFromPivotTable path */
|
|
1392
|
+
if (needsConversion) {
|
|
1393
|
+
for (const item of pivotRows) {
|
|
1394
|
+
const fk = item[ownerProp.name];
|
|
1395
|
+
if (fk != null) {
|
|
1396
|
+
item[ownerProp.name] = pkProp.customType.convertToJSValue(fk, this.platform);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
const classMeta = new Map(targets.map(t => [t.class, t]));
|
|
1401
|
+
const rowsByTarget = new Map();
|
|
1402
|
+
for (const row of pivotRows) {
|
|
1403
|
+
const discValue = row[discriminatorColumn];
|
|
1404
|
+
const targetClass = prop.discriminatorMap[discValue];
|
|
1405
|
+
const targetMeta = classMeta.get(targetClass);
|
|
1406
|
+
/* v8 ignore next 3 - defensive: unknown discriminator value */
|
|
1407
|
+
if (!targetMeta) {
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
const list = rowsByTarget.get(targetMeta) ?? [];
|
|
1411
|
+
list.push(row);
|
|
1412
|
+
rowsByTarget.set(targetMeta, list);
|
|
1413
|
+
}
|
|
1414
|
+
// Strip the outer find's orderBy/fields/exclude before bulk-loading targets by PK — those apply
|
|
1415
|
+
// to the owner query, not each polymorph target (Image and Video wouldn't share an orderBy field).
|
|
1416
|
+
// populateFilter is a filter on the populated collection; since union-target splits the pivot
|
|
1417
|
+
// and target queries, we merge it into the target-level `where` instead of wrapping it on the
|
|
1418
|
+
// pivot query (where joins to target tables aren't available).
|
|
1419
|
+
// Hoisted above the loop since `options` doesn't change per target.
|
|
1420
|
+
const { orderBy: _o, fields: _f, exclude: _e, populateFilter, ...childOptions } = options;
|
|
1421
|
+
const populate = options.populate ?? [];
|
|
1422
|
+
const orphanedRows = new Set();
|
|
1423
|
+
for (const [targetMeta, rows] of rowsByTarget) {
|
|
1424
|
+
const targetIds = rows.map(r => r[prop.discriminator]);
|
|
1425
|
+
// Union-target pivot stores one scalar FK per row; composite-PK targets are rejected at
|
|
1426
|
+
// metadata validation time, so a single primary key column is guaranteed here.
|
|
1427
|
+
const pkCol = targetMeta.primaryKeys[0];
|
|
1428
|
+
let cond = { [pkCol]: { $in: targetIds } };
|
|
1429
|
+
if (!Utils.isEmpty(where)) {
|
|
1430
|
+
cond = { $and: [cond, where] };
|
|
1431
|
+
}
|
|
1432
|
+
if (!Utils.isEmpty(populateFilter)) {
|
|
1433
|
+
cond = { $and: [cond, populateFilter] };
|
|
1434
|
+
}
|
|
1435
|
+
const results = (await this.find(targetMeta.class, cond, {
|
|
1436
|
+
ctx,
|
|
1437
|
+
...childOptions,
|
|
1438
|
+
populate: populate,
|
|
1439
|
+
}));
|
|
1440
|
+
const byPk = new Map();
|
|
1441
|
+
for (const row of results) {
|
|
1442
|
+
Object.defineProperty(row, 'constructor', {
|
|
1443
|
+
value: targetMeta.class,
|
|
1444
|
+
enumerable: false,
|
|
1445
|
+
configurable: true,
|
|
1446
|
+
});
|
|
1447
|
+
byPk.set(Utils.getPrimaryKeyHash([row[pkCol]]), row);
|
|
1448
|
+
}
|
|
1449
|
+
for (const row of rows) {
|
|
1450
|
+
const pkHash = Utils.getPrimaryKeyHash([row[prop.discriminator]]);
|
|
1451
|
+
const entity = byPk.get(pkHash);
|
|
1452
|
+
if (entity == null) {
|
|
1453
|
+
orphanedRows.add(row);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
row[prop.discriminator] = entity;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
const result = orphanedRows.size > 0 ? pivotRows.filter(r => !orphanedRows.has(r)) : pivotRows;
|
|
1460
|
+
return this.buildPivotResultMap(owners, result, ownerProp.name, prop.discriminator);
|
|
1461
|
+
}
|
|
1343
1462
|
/**
|
|
1344
1463
|
* Build a map from owner PKs to their related entities from pivot table results.
|
|
1345
1464
|
*/
|
|
@@ -1364,6 +1483,21 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1364
1483
|
}
|
|
1365
1484
|
return undefined;
|
|
1366
1485
|
}
|
|
1486
|
+
/**
|
|
1487
|
+
* The pivot query builder doesn't convert custom types — manually convert owner PKs to the DB
|
|
1488
|
+
* representation. Returns `needsConversion` + `pkProp` so the caller can convert result FKs back
|
|
1489
|
+
* to JS format for consistent key hashing in `buildPivotResultMap`.
|
|
1490
|
+
*/
|
|
1491
|
+
convertOwnerPksForPivotQuery(owners, ownerMeta) {
|
|
1492
|
+
const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
|
|
1493
|
+
const needsConversion = !!pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
|
|
1494
|
+
let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
|
|
1495
|
+
/* v8 ignore next 4 - custom-type PK conversion, tested via loadFromPivotTable path */
|
|
1496
|
+
if (needsConversion) {
|
|
1497
|
+
ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
|
|
1498
|
+
}
|
|
1499
|
+
return { ownerPks, needsConversion, pkProp };
|
|
1500
|
+
}
|
|
1367
1501
|
getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
|
|
1368
1502
|
if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
|
|
1369
1503
|
return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
|
|
@@ -1430,6 +1564,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1430
1564
|
if (ref && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
|
|
1431
1565
|
return true;
|
|
1432
1566
|
}
|
|
1567
|
+
// Union-target polymorphic M:N cannot be loaded via a single JOIN because rows span multiple
|
|
1568
|
+
// target tables; fall through to SELECT_IN which dispatches through `loadFromPivotTable`.
|
|
1569
|
+
// Polymorphic M:1 (to-one with target_type discriminator) is handled via LEFT JOINs elsewhere.
|
|
1570
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY && QueryHelper.isUnionTargetPolymorphic(prop) && prop.owner) {
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1433
1573
|
// skip redundant joins for 1:1 owner population hints when using `mapToPk`
|
|
1434
1574
|
if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
|
|
1435
1575
|
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 };
|
|
@@ -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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.1.0-dev.
|
|
3
|
+
"version": "7.1.0-dev.7",
|
|
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",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@mikro-orm/core": "^7.0.11"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.1.0-dev.
|
|
56
|
+
"@mikro-orm/core": "7.1.0-dev.7"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|