@mikro-orm/sql 7.1.0-dev.6 → 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 +11 -0
- package/AbstractSqlDriver.js +144 -11
- package/PivotCollectionPersister.js +13 -2
- package/package.json +2 -2
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
|
@@ -1106,8 +1106,29 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1106
1106
|
const pks = wrapped.getPrimaryKeys(true);
|
|
1107
1107
|
const snap = coll.getSnapshot();
|
|
1108
1108
|
const includes = (arr, item) => !!arr.find(i => this.comparePrimaryKeyArrays(i, item));
|
|
1109
|
-
|
|
1110
|
-
|
|
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);
|
|
1111
1132
|
const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
|
|
1112
1133
|
const insertDiff = current.filter(item => !includes(snapshot, item));
|
|
1113
1134
|
const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
|
|
@@ -1179,21 +1200,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1179
1200
|
return {};
|
|
1180
1201
|
}
|
|
1181
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
|
+
}
|
|
1182
1206
|
if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
|
|
1183
1207
|
return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
|
|
1184
1208
|
}
|
|
1185
1209
|
const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
|
|
1186
1210
|
const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
|
|
1187
1211
|
const ownerMeta = pivotProp2.targetMeta;
|
|
1188
|
-
|
|
1189
|
-
// convert owner PKs to DB format for the query and convert result FKs back to
|
|
1190
|
-
// JS format for consistent key hashing in buildPivotResultMap.
|
|
1191
|
-
const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
|
|
1192
|
-
const needsConversion = pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
|
|
1193
|
-
let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
|
|
1194
|
-
if (needsConversion) {
|
|
1195
|
-
ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
|
|
1196
|
-
}
|
|
1212
|
+
const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
|
|
1197
1213
|
const cond = {
|
|
1198
1214
|
[pivotProp2.name]: { $in: ownerPks },
|
|
1199
1215
|
};
|
|
@@ -1347,6 +1363,102 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1347
1363
|
});
|
|
1348
1364
|
return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
|
|
1349
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
|
+
}
|
|
1350
1462
|
/**
|
|
1351
1463
|
* Build a map from owner PKs to their related entities from pivot table results.
|
|
1352
1464
|
*/
|
|
@@ -1371,6 +1483,21 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1371
1483
|
}
|
|
1372
1484
|
return undefined;
|
|
1373
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
|
+
}
|
|
1374
1501
|
getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
|
|
1375
1502
|
if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
|
|
1376
1503
|
return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
|
|
@@ -1437,6 +1564,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1437
1564
|
if (ref && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
|
|
1438
1565
|
return true;
|
|
1439
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
|
+
}
|
|
1440
1573
|
// skip redundant joins for 1:1 owner population hints when using `mapToPk`
|
|
1441
1574
|
if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
|
|
1442
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 };
|
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"
|