@mikro-orm/sql 7.0.0-dev.225 → 7.0.0-dev.226
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 +23 -0
- package/AbstractSqlDriver.js +248 -49
- package/PivotCollectionPersister.d.ts +5 -0
- package/PivotCollectionPersister.js +27 -9
- package/package.json +2 -2
- package/query/QueryBuilder.d.ts +6 -0
- package/query/QueryBuilder.js +15 -0
- package/query/QueryBuilderHelper.js +21 -2
- package/schema/DatabaseTable.js +1 -1
- package/tsconfig.build.tsbuildinfo +1 -1
package/AbstractSqlDriver.d.ts
CHANGED
|
@@ -26,6 +26,11 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
26
26
|
protected wrapVirtualExpressionInSubqueryStream<T extends object>(meta: EntityMetadata<T>, expression: string, where: FilterQuery<T>, options: FindOptions<T, any, any, any>, type: QueryType.SELECT): AsyncIterableIterator<T>;
|
|
27
27
|
mapResult<T extends object>(result: EntityData<T>, meta: EntityMetadata<T>, populate?: PopulateOptions<T>[], qb?: QueryBuilder<T, any, any, any>, map?: Dictionary): EntityData<T> | null;
|
|
28
28
|
private mapJoinedProps;
|
|
29
|
+
/**
|
|
30
|
+
* Maps a single property from a joined result row into the relation pojo.
|
|
31
|
+
* Handles polymorphic FKs, composite keys, Date parsing, and embedded objects.
|
|
32
|
+
*/
|
|
33
|
+
private mapJoinedProp;
|
|
29
34
|
count<T extends object>(entityName: EntityName<T>, where: any, options?: CountOptions<T>): Promise<number>;
|
|
30
35
|
nativeInsert<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
|
|
31
36
|
nativeInsertMany<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>, transform?: (sql: string) => string): Promise<QueryResult<T>>;
|
|
@@ -40,6 +45,24 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
40
45
|
private comparePrimaryKeyArrays;
|
|
41
46
|
syncCollections<T extends object, O extends object>(collections: Iterable<Collection<T, O>>, options?: DriverMethodOptions): Promise<void>;
|
|
42
47
|
loadFromPivotTable<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[]>>;
|
|
48
|
+
/**
|
|
49
|
+
* Load from a polymorphic M:N pivot table.
|
|
50
|
+
*/
|
|
51
|
+
protected loadFromPolymorphicPivotTable<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[]>>;
|
|
52
|
+
/**
|
|
53
|
+
* Load from owner side of polymorphic M:N (e.g., Post -> Tags)
|
|
54
|
+
*/
|
|
55
|
+
protected loadPolymorphicPivotOwnerSide<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, inverseProp?: EntityProperty): Promise<Dictionary<T[]>>;
|
|
56
|
+
/**
|
|
57
|
+
* Load from inverse side of polymorphic M:N (e.g., Tag -> Posts)
|
|
58
|
+
* Uses single query with join via virtual relation on pivot.
|
|
59
|
+
*/
|
|
60
|
+
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[]>>;
|
|
61
|
+
/**
|
|
62
|
+
* Build a map from owner PKs to their related entities from pivot table results.
|
|
63
|
+
*/
|
|
64
|
+
private buildPivotResultMap;
|
|
65
|
+
private wrapPopulateFilter;
|
|
43
66
|
private getPivotOrderBy;
|
|
44
67
|
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>;
|
|
45
68
|
stream<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T, any, any, any>): AsyncIterableIterator<T>;
|
package/AbstractSqlDriver.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ALIAS_REPLACEMENT_RE, DatabaseDriver, EntityManagerType, getLoadingStrategy, getOnConflictFields, getOnConflictReturningFields, helper, isRaw, LoadStrategy, parseJsonSafe, QueryFlag, QueryHelper, QueryOrder, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core';
|
|
1
|
+
import { ALIAS_REPLACEMENT_RE, DatabaseDriver, EntityManagerType, getLoadingStrategy, getOnConflictFields, getOnConflictReturningFields, helper, isRaw, LoadStrategy, parseJsonSafe, PolymorphicRef, QueryFlag, QueryHelper, QueryOrder, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core';
|
|
2
2
|
import { QueryBuilder } from './query/QueryBuilder.js';
|
|
3
3
|
import { JoinType, QueryType } from './query/enums.js';
|
|
4
4
|
import { SqlEntityManager } from './SqlEntityManager.js';
|
|
@@ -216,6 +216,43 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
216
216
|
if (!prop) {
|
|
217
217
|
return;
|
|
218
218
|
}
|
|
219
|
+
// Polymorphic to-one: iterate targets, find the matching one, build entity from its columns.
|
|
220
|
+
// Skip :ref hints — no JOINs were created, so the FK reference is already set by the result mapper.
|
|
221
|
+
if (prop.polymorphic && prop.polymorphTargets?.length && !ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
222
|
+
const basePath = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
|
|
223
|
+
const pathPrefix = !parentJoinPath ? '[populate]' : '';
|
|
224
|
+
let matched = false;
|
|
225
|
+
for (const targetMeta of prop.polymorphTargets) {
|
|
226
|
+
const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
|
|
227
|
+
const relationAlias = qb.getAliasForJoinPath(targetPath, { matchPopulateJoins: true });
|
|
228
|
+
const meta2 = targetMeta;
|
|
229
|
+
const targetProps = meta2.props.filter(p => this.platform.shouldHaveColumn(p, hint.children || []));
|
|
230
|
+
const hasPK = meta2.getPrimaryProps().every(pk => pk.fieldNames.every(name => root[`${relationAlias}__${name}`] != null));
|
|
231
|
+
if (hasPK && !matched) {
|
|
232
|
+
matched = true;
|
|
233
|
+
let relationPojo = {};
|
|
234
|
+
const tz = this.platform.getTimezone();
|
|
235
|
+
for (const p of targetProps) {
|
|
236
|
+
this.mapJoinedProp(relationPojo, p, relationAlias, root, tz, meta2);
|
|
237
|
+
}
|
|
238
|
+
// Inject the entity class constructor so that the factory creates the correct type
|
|
239
|
+
Object.defineProperty(relationPojo, 'constructor', { value: meta2.class, enumerable: false, configurable: true });
|
|
240
|
+
result[prop.name] = relationPojo;
|
|
241
|
+
const populateChildren = hint.children || [];
|
|
242
|
+
this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, targetPath);
|
|
243
|
+
}
|
|
244
|
+
// Clean up aliased columns for ALL targets (even non-matching ones)
|
|
245
|
+
for (const p of targetProps) {
|
|
246
|
+
for (const name of p.fieldNames) {
|
|
247
|
+
delete root[`${relationAlias}__${name}`];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (!matched) {
|
|
252
|
+
result[prop.name] = null;
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
219
256
|
const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref;
|
|
220
257
|
const meta2 = prop.targetMeta;
|
|
221
258
|
let path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
|
|
@@ -286,41 +323,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
286
323
|
});
|
|
287
324
|
const tz = this.platform.getTimezone();
|
|
288
325
|
for (const prop of targetProps) {
|
|
289
|
-
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
if (prop.fieldNames.length > 1) {
|
|
293
|
-
// composite keys
|
|
294
|
-
const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
|
|
295
|
-
const pk = Utils.mapFlatCompositePrimaryKey(fk, prop);
|
|
296
|
-
relationPojo[prop.name] = pk.every(val => val != null) ? pk : null;
|
|
297
|
-
}
|
|
298
|
-
else if (prop.runtimeType === 'Date') {
|
|
299
|
-
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
300
|
-
const value = root[alias];
|
|
301
|
-
if (tz && tz !== 'local' && typeof value === 'string' && !value.includes('+') && value.lastIndexOf('-') < 11 && !value.endsWith('Z')) {
|
|
302
|
-
relationPojo[prop.name] = this.platform.parseDate(value + tz);
|
|
303
|
-
}
|
|
304
|
-
else if (['string', 'number'].includes(typeof value)) {
|
|
305
|
-
relationPojo[prop.name] = this.platform.parseDate(value);
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
relationPojo[prop.name] = value;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
313
|
-
relationPojo[prop.name] = root[alias];
|
|
314
|
-
if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
|
|
315
|
-
const item = parseJsonSafe(relationPojo[prop.name]);
|
|
316
|
-
if (Array.isArray(item)) {
|
|
317
|
-
relationPojo[prop.name] = item.map(row => (row == null ? row : this.comparator.mapResult(prop.targetMeta, row)));
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
relationPojo[prop.name] = item == null ? item : this.comparator.mapResult(prop.targetMeta, item);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
326
|
+
this.mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta2);
|
|
324
327
|
}
|
|
325
328
|
// properties can be mapped to multiple places, e.g. when sharing a column in multiple FKs,
|
|
326
329
|
// so we need to delete them after everything is mapped from given level
|
|
@@ -345,6 +348,60 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
345
348
|
this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, path);
|
|
346
349
|
});
|
|
347
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Maps a single property from a joined result row into the relation pojo.
|
|
353
|
+
* Handles polymorphic FKs, composite keys, Date parsing, and embedded objects.
|
|
354
|
+
*/
|
|
355
|
+
mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta) {
|
|
356
|
+
if (prop.fieldNames.every(name => typeof root[`${relationAlias}__${name}`] === 'undefined')) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (prop.polymorphic) {
|
|
360
|
+
const discriminatorAlias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
361
|
+
const discriminatorValue = root[discriminatorAlias];
|
|
362
|
+
const pkFieldNames = prop.fieldNames.slice(1);
|
|
363
|
+
const pkValues = pkFieldNames.map(name => root[`${relationAlias}__${name}`]);
|
|
364
|
+
const pkValue = pkValues.length === 1 ? pkValues[0] : pkValues;
|
|
365
|
+
if (discriminatorValue != null && pkValue != null) {
|
|
366
|
+
relationPojo[prop.name] = new PolymorphicRef(discriminatorValue, pkValue);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
relationPojo[prop.name] = null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else if (prop.fieldNames.length > 1) {
|
|
373
|
+
// composite keys
|
|
374
|
+
const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
|
|
375
|
+
const pk = Utils.mapFlatCompositePrimaryKey(fk, prop);
|
|
376
|
+
relationPojo[prop.name] = pk.every(val => val != null) ? pk : null;
|
|
377
|
+
}
|
|
378
|
+
else if (prop.runtimeType === 'Date') {
|
|
379
|
+
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
380
|
+
const value = root[alias];
|
|
381
|
+
if (tz && tz !== 'local' && typeof value === 'string' && !value.includes('+') && value.lastIndexOf('-') < 11 && !value.endsWith('Z')) {
|
|
382
|
+
relationPojo[prop.name] = this.platform.parseDate(value + tz);
|
|
383
|
+
}
|
|
384
|
+
else if (['string', 'number'].includes(typeof value)) {
|
|
385
|
+
relationPojo[prop.name] = this.platform.parseDate(value);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
relationPojo[prop.name] = value;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
393
|
+
relationPojo[prop.name] = root[alias];
|
|
394
|
+
if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
|
|
395
|
+
const item = parseJsonSafe(relationPojo[prop.name]);
|
|
396
|
+
if (Array.isArray(item)) {
|
|
397
|
+
relationPojo[prop.name] = item.map(row => (row == null ? row : this.comparator.mapResult(prop.targetMeta, row)));
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
relationPojo[prop.name] = item == null ? item : this.comparator.mapResult(prop.targetMeta, item);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
348
405
|
async count(entityName, where, options = {}) {
|
|
349
406
|
const meta = this.metadata.get(entityName);
|
|
350
407
|
if (meta.virtual) {
|
|
@@ -471,7 +528,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
471
528
|
}
|
|
472
529
|
if (prop.fieldNames.length > 1) {
|
|
473
530
|
const newFields = [];
|
|
474
|
-
|
|
531
|
+
let rawParam;
|
|
532
|
+
const target = row[prop.name];
|
|
533
|
+
if (prop.polymorphic && target instanceof PolymorphicRef) {
|
|
534
|
+
rawParam = target.toTuple();
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
rawParam = Utils.asArray(target) ?? prop.fieldNames.map(() => null);
|
|
538
|
+
}
|
|
475
539
|
// Deep flatten nested arrays when needed (for deeply nested composite keys like Tag -> Comment -> Post -> User)
|
|
476
540
|
const needsFlatten = rawParam.length !== prop.fieldNames.length && rawParam.some(v => Array.isArray(v));
|
|
477
541
|
const allParam = needsFlatten ? Utils.flatten(rawParam, true) : rawParam;
|
|
@@ -644,6 +708,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
644
708
|
};
|
|
645
709
|
for (const key of keys) {
|
|
646
710
|
const prop = meta.properties[key] ?? meta.root.properties[key];
|
|
711
|
+
if (prop.polymorphic && prop.fieldNames.length > 1) {
|
|
712
|
+
for (let idx = 0; idx < data.length; idx++) {
|
|
713
|
+
const rowValue = data[idx][key];
|
|
714
|
+
if (rowValue instanceof PolymorphicRef) {
|
|
715
|
+
data[idx][key] = rowValue.toTuple();
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
647
719
|
prop.fieldNames.forEach((fieldName, fieldNameIdx) => {
|
|
648
720
|
if (fields.has(fieldName) || (prop.ownColumns && !prop.ownColumns.includes(fieldName))) {
|
|
649
721
|
return;
|
|
@@ -814,10 +886,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
814
886
|
}
|
|
815
887
|
}
|
|
816
888
|
async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
|
|
889
|
+
/* v8 ignore next */
|
|
817
890
|
if (owners.length === 0) {
|
|
818
891
|
return {};
|
|
819
892
|
}
|
|
820
893
|
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
894
|
+
if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
|
|
895
|
+
return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
|
|
896
|
+
}
|
|
821
897
|
const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
|
|
822
898
|
const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
|
|
823
899
|
const ownerMeta = pivotProp2.targetMeta;
|
|
@@ -851,21 +927,122 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
851
927
|
populateWhere: undefined,
|
|
852
928
|
// @ts-ignore
|
|
853
929
|
_populateWhere: 'infer',
|
|
854
|
-
populateFilter:
|
|
855
|
-
|
|
856
|
-
|
|
930
|
+
populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
|
|
931
|
+
});
|
|
932
|
+
return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name);
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Load from a polymorphic M:N pivot table.
|
|
936
|
+
*/
|
|
937
|
+
async loadFromPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
|
|
938
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
939
|
+
// Find the M:1 relation on the pivot pointing to the target entity.
|
|
940
|
+
// We exclude virtual polymorphic owner relations (persist: false) and non-M:1 relations.
|
|
941
|
+
const inverseProp = pivotMeta.relations.find(r => r.kind === ReferenceKind.MANY_TO_ONE && r.persist !== false && r.targetMeta === prop.targetMeta);
|
|
942
|
+
if (inverseProp) {
|
|
943
|
+
return this.loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp);
|
|
944
|
+
}
|
|
945
|
+
return this.loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options);
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Load from owner side of polymorphic M:N (e.g., Post -> Tags)
|
|
949
|
+
*/
|
|
950
|
+
async loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp) {
|
|
951
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
952
|
+
const targetMeta = prop.targetMeta;
|
|
953
|
+
// Build condition: discriminator = 'post' AND {discriminator} IN (...)
|
|
954
|
+
const cond = {
|
|
955
|
+
[prop.discriminatorColumn]: prop.discriminatorValue,
|
|
956
|
+
[prop.discriminator]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
|
|
957
|
+
};
|
|
958
|
+
if (!Utils.isEmpty(where)) {
|
|
959
|
+
cond[inverseProp.name] = { ...where };
|
|
960
|
+
}
|
|
961
|
+
const populateField = pivotJoin ? `${inverseProp.name}:ref` : inverseProp.name;
|
|
962
|
+
const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
|
|
963
|
+
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${inverseProp.name}.${f}`) : [];
|
|
964
|
+
const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${inverseProp.name}.${f}`) : [];
|
|
965
|
+
const fields = pivotJoin
|
|
966
|
+
? [inverseProp.name, prop.discriminator, prop.discriminatorColumn]
|
|
967
|
+
: [inverseProp.name, prop.discriminator, prop.discriminatorColumn, ...childFields];
|
|
968
|
+
const res = await this.find(pivotMeta.class, cond, {
|
|
969
|
+
ctx,
|
|
970
|
+
...options,
|
|
971
|
+
fields,
|
|
972
|
+
exclude: childExclude,
|
|
973
|
+
orderBy: this.getPivotOrderBy(prop, inverseProp, orderBy, options?.orderBy),
|
|
974
|
+
populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate, dataOnly: inverseProp.mapToPk && !pivotJoin }],
|
|
975
|
+
populateWhere: undefined,
|
|
976
|
+
// @ts-ignore
|
|
977
|
+
_populateWhere: 'infer',
|
|
978
|
+
populateFilter: this.wrapPopulateFilter(options, inverseProp.name),
|
|
857
979
|
});
|
|
980
|
+
return this.buildPivotResultMap(owners, res, prop.discriminator, inverseProp.name);
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Load from inverse side of polymorphic M:N (e.g., Tag -> Posts)
|
|
984
|
+
* Uses single query with join via virtual relation on pivot.
|
|
985
|
+
*/
|
|
986
|
+
async loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options) {
|
|
987
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
988
|
+
const targetMeta = prop.targetMeta;
|
|
989
|
+
// Find the relation to the entity we're starting from (e.g., Tag_inverse -> Tag)
|
|
990
|
+
// Exclude virtual polymorphic owner relations (persist: false) - we want the actual M:N inverse relation
|
|
991
|
+
const tagProp = pivotMeta.relations.find(r => r.persist !== false && r.targetMeta !== targetMeta);
|
|
992
|
+
// Find the virtual relation to the polymorphic owner (e.g., taggable_Post -> Post)
|
|
993
|
+
const ownerRelationName = `${prop.discriminator}_${targetMeta.tableName}`;
|
|
994
|
+
const ownerProp = pivotMeta.properties[ownerRelationName];
|
|
995
|
+
// Build condition: discriminator = 'post' AND Tag_inverse IN (tagIds)
|
|
996
|
+
const cond = {
|
|
997
|
+
[prop.discriminatorColumn]: prop.discriminatorValue,
|
|
998
|
+
[tagProp.name]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
|
|
999
|
+
};
|
|
1000
|
+
if (!Utils.isEmpty(where)) {
|
|
1001
|
+
cond[ownerRelationName] = { ...where };
|
|
1002
|
+
}
|
|
1003
|
+
const populateField = ownerRelationName;
|
|
1004
|
+
const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
|
|
1005
|
+
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${ownerRelationName}.${f}`) : [];
|
|
1006
|
+
const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${ownerRelationName}.${f}`) : [];
|
|
1007
|
+
const fields = [ownerRelationName, tagProp.name, prop.discriminatorColumn, ...childFields];
|
|
1008
|
+
const res = await this.find(pivotMeta.class, cond, {
|
|
1009
|
+
ctx,
|
|
1010
|
+
...options,
|
|
1011
|
+
fields,
|
|
1012
|
+
exclude: childExclude,
|
|
1013
|
+
orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
|
|
1014
|
+
populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate }],
|
|
1015
|
+
populateWhere: undefined,
|
|
1016
|
+
// @ts-ignore
|
|
1017
|
+
_populateWhere: 'infer',
|
|
1018
|
+
populateFilter: this.wrapPopulateFilter(options, ownerRelationName),
|
|
1019
|
+
});
|
|
1020
|
+
return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Build a map from owner PKs to their related entities from pivot table results.
|
|
1024
|
+
*/
|
|
1025
|
+
buildPivotResultMap(owners, results, keyProp, valueProp) {
|
|
858
1026
|
const map = {};
|
|
859
1027
|
for (const owner of owners) {
|
|
860
1028
|
const key = Utils.getPrimaryKeyHash(owner);
|
|
861
1029
|
map[key] = [];
|
|
862
1030
|
}
|
|
863
|
-
for (const item of
|
|
864
|
-
const key = Utils.getPrimaryKeyHash(Utils.asArray(item[
|
|
865
|
-
|
|
1031
|
+
for (const item of results) {
|
|
1032
|
+
const key = Utils.getPrimaryKeyHash(Utils.asArray(item[keyProp]));
|
|
1033
|
+
const entity = item[valueProp];
|
|
1034
|
+
if (map[key]) {
|
|
1035
|
+
map[key].push(entity);
|
|
1036
|
+
}
|
|
866
1037
|
}
|
|
867
1038
|
return map;
|
|
868
1039
|
}
|
|
1040
|
+
wrapPopulateFilter(options, propName) {
|
|
1041
|
+
if (!Utils.isEmpty(options?.populateFilter) || RawQueryFragment.hasObjectFragments(options?.populateFilter)) {
|
|
1042
|
+
return { [propName]: options?.populateFilter };
|
|
1043
|
+
}
|
|
1044
|
+
return undefined;
|
|
1045
|
+
}
|
|
869
1046
|
getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
|
|
870
1047
|
if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
|
|
871
1048
|
return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
|
|
@@ -1022,6 +1199,26 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1022
1199
|
for (const hint of joinedProps) {
|
|
1023
1200
|
const [propName, ref] = hint.field.split(':', 2);
|
|
1024
1201
|
const prop = meta.properties[propName];
|
|
1202
|
+
// Polymorphic to-one: create a LEFT JOIN per target type
|
|
1203
|
+
// Skip :ref hints — polymorphic to-one already has FK + discriminator in the row
|
|
1204
|
+
if (prop.polymorphic && prop.polymorphTargets?.length && !ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
1205
|
+
const basePath = options.parentJoinPath ? `${options.parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
|
|
1206
|
+
const pathPrefix = !options.parentJoinPath && populateWhereAll && !basePath.startsWith('[populate]') ? '[populate]' : '';
|
|
1207
|
+
for (const targetMeta of prop.polymorphTargets) {
|
|
1208
|
+
const tableAlias = qb.getNextAlias(targetMeta.className);
|
|
1209
|
+
const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
|
|
1210
|
+
const schema = targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : targetMeta.schema;
|
|
1211
|
+
qb.addPolymorphicJoin(prop, targetMeta, options.parentTableAlias, tableAlias, JoinType.leftJoin, targetPath, schema);
|
|
1212
|
+
// Select fields from each target table
|
|
1213
|
+
fields.push(...this.getFieldsForJoinedLoad(qb, targetMeta, {
|
|
1214
|
+
...options,
|
|
1215
|
+
populate: hint.children,
|
|
1216
|
+
parentTableAlias: tableAlias,
|
|
1217
|
+
parentJoinPath: targetPath,
|
|
1218
|
+
}));
|
|
1219
|
+
}
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1025
1222
|
// ignore ref joins of known FKs unless it's a filter hint
|
|
1026
1223
|
if (ref && !hint.filter && (prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))) {
|
|
1027
1224
|
continue;
|
|
@@ -1186,10 +1383,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1186
1383
|
where[prop.name] = Utils.copy(prop.where);
|
|
1187
1384
|
}
|
|
1188
1385
|
if (hint.children) {
|
|
1189
|
-
const
|
|
1190
|
-
if (
|
|
1191
|
-
|
|
1192
|
-
|
|
1386
|
+
const targetMeta = prop.targetMeta;
|
|
1387
|
+
if (targetMeta) {
|
|
1388
|
+
const inner = this.buildPopulateWhere(targetMeta, hint.children, {});
|
|
1389
|
+
if (!Utils.isEmpty(inner) || RawQueryFragment.hasObjectFragments(inner)) {
|
|
1390
|
+
where[prop.name] ??= {};
|
|
1391
|
+
Object.assign(where[prop.name], inner);
|
|
1392
|
+
}
|
|
1193
1393
|
}
|
|
1194
1394
|
}
|
|
1195
1395
|
}
|
|
@@ -1280,7 +1480,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1280
1480
|
}
|
|
1281
1481
|
const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
|
|
1282
1482
|
const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true });
|
|
1283
|
-
const meta2 = prop.targetMeta;
|
|
1284
1483
|
if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.fixedOrder && join) {
|
|
1285
1484
|
const alias = ref ? propAlias : join.ownerAlias;
|
|
1286
1485
|
orderBy.push({ [`${alias}.${prop.fixedOrderColumn}`]: QueryOrder.ASC });
|
|
@@ -1301,7 +1500,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1301
1500
|
}
|
|
1302
1501
|
}
|
|
1303
1502
|
if (hint.children) {
|
|
1304
|
-
const buildJoinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb,
|
|
1503
|
+
const buildJoinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, prop.targetMeta, hint.children, options, path);
|
|
1305
1504
|
orderBy.push(...buildJoinedPropsOrderBy);
|
|
1306
1505
|
}
|
|
1307
1506
|
}
|
|
@@ -17,6 +17,11 @@ export declare class PivotCollectionPersister<Entity extends object> {
|
|
|
17
17
|
private enqueueUpsert;
|
|
18
18
|
private createInsertStatement;
|
|
19
19
|
private enqueueDelete;
|
|
20
|
+
/**
|
|
21
|
+
* Build the keys and data arrays for pivot table operations.
|
|
22
|
+
* Handles polymorphic M:N by prepending the discriminator column/value.
|
|
23
|
+
*/
|
|
24
|
+
private buildPivotKeysAndData;
|
|
20
25
|
private collectStatements;
|
|
21
26
|
execute(): Promise<void>;
|
|
22
27
|
}
|
|
@@ -83,27 +83,45 @@ export class PivotCollectionPersister {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
createInsertStatement(prop, fks, pks) {
|
|
86
|
-
const data = prop
|
|
87
|
-
const keys = prop.owner
|
|
88
|
-
? [...prop.inverseJoinColumns, ...prop.joinColumns]
|
|
89
|
-
: [...prop.joinColumns, ...prop.inverseJoinColumns];
|
|
86
|
+
const { data, keys } = this.buildPivotKeysAndData(prop, fks, pks);
|
|
90
87
|
return new InsertStatement(keys, data, this.order++);
|
|
91
88
|
}
|
|
92
89
|
enqueueDelete(prop, deleteDiff, pks) {
|
|
93
90
|
if (deleteDiff === true) {
|
|
94
|
-
const
|
|
91
|
+
const { data, keys } = this.buildPivotKeysAndData(prop, [], pks, true);
|
|
92
|
+
const statement = new DeleteStatement(keys, data);
|
|
95
93
|
this.deletes.set(statement.getHash(), statement);
|
|
96
94
|
return;
|
|
97
95
|
}
|
|
98
96
|
for (const fks of deleteDiff) {
|
|
99
|
-
const data = prop
|
|
100
|
-
const keys = prop.owner
|
|
101
|
-
? [...prop.inverseJoinColumns, ...prop.joinColumns]
|
|
102
|
-
: [...prop.joinColumns, ...prop.inverseJoinColumns];
|
|
97
|
+
const { data, keys } = this.buildPivotKeysAndData(prop, fks, pks);
|
|
103
98
|
const statement = new DeleteStatement(keys, data);
|
|
104
99
|
this.deletes.set(statement.getHash(), statement);
|
|
105
100
|
}
|
|
106
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Build the keys and data arrays for pivot table operations.
|
|
104
|
+
* Handles polymorphic M:N by prepending the discriminator column/value.
|
|
105
|
+
*/
|
|
106
|
+
buildPivotKeysAndData(prop, fks, pks, deleteAll = false) {
|
|
107
|
+
let data;
|
|
108
|
+
let keys;
|
|
109
|
+
if (deleteAll) {
|
|
110
|
+
data = pks;
|
|
111
|
+
keys = prop.joinColumns;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
data = prop.owner ? [...fks, ...pks] : [...pks, ...fks];
|
|
115
|
+
keys = prop.owner
|
|
116
|
+
? [...prop.inverseJoinColumns, ...prop.joinColumns]
|
|
117
|
+
: [...prop.joinColumns, ...prop.inverseJoinColumns];
|
|
118
|
+
}
|
|
119
|
+
if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
|
|
120
|
+
data = [prop.discriminatorValue, ...data];
|
|
121
|
+
keys = [prop.discriminatorColumn, ...keys];
|
|
122
|
+
}
|
|
123
|
+
return { data, keys };
|
|
124
|
+
}
|
|
107
125
|
collectStatements(statements) {
|
|
108
126
|
const items = [];
|
|
109
127
|
for (const statement of statements.values()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.0.0-dev.
|
|
3
|
+
"version": "7.0.0-dev.226",
|
|
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
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
"@mikro-orm/core": "^6.6.4"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@mikro-orm/core": "7.0.0-dev.
|
|
59
|
+
"@mikro-orm/core": "7.0.0-dev.226"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/query/QueryBuilder.d.ts
CHANGED
|
@@ -662,6 +662,12 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
662
662
|
* @internal
|
|
663
663
|
*/
|
|
664
664
|
getNextAlias(entityName?: string | EntityName): string;
|
|
665
|
+
/**
|
|
666
|
+
* Registers a join for a specific polymorphic target type.
|
|
667
|
+
* Used by the driver to create per-target LEFT JOINs for JOINED loading.
|
|
668
|
+
* @internal
|
|
669
|
+
*/
|
|
670
|
+
addPolymorphicJoin(prop: EntityProperty, targetMeta: EntityMetadata, ownerAlias: string, alias: string, type: JoinType, path: string, schema?: string): void;
|
|
665
671
|
/**
|
|
666
672
|
* @internal
|
|
667
673
|
*/
|
package/query/QueryBuilder.js
CHANGED
|
@@ -784,6 +784,21 @@ export class QueryBuilder {
|
|
|
784
784
|
entityName = Utils.className(entityName);
|
|
785
785
|
return this.driver.config.getNamingStrategy().aliasName(entityName, this.aliasCounter++);
|
|
786
786
|
}
|
|
787
|
+
/**
|
|
788
|
+
* Registers a join for a specific polymorphic target type.
|
|
789
|
+
* Used by the driver to create per-target LEFT JOINs for JOINED loading.
|
|
790
|
+
* @internal
|
|
791
|
+
*/
|
|
792
|
+
addPolymorphicJoin(prop, targetMeta, ownerAlias, alias, type, path, schema) {
|
|
793
|
+
// Override referencedColumnNames to use the specific target's PK columns
|
|
794
|
+
// (polymorphic targets may have different PK column names, e.g. org_id vs user_id)
|
|
795
|
+
const referencedColumnNames = targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
|
|
796
|
+
const targetProp = { ...prop, targetMeta, referencedColumnNames };
|
|
797
|
+
const aliasedName = `${ownerAlias}.${prop.name}[${targetMeta.className}]#${alias}`;
|
|
798
|
+
this._joins[aliasedName] = this.helper.joinManyToOneReference(targetProp, ownerAlias, alias, type, {}, schema);
|
|
799
|
+
this._joins[aliasedName].path = path;
|
|
800
|
+
this.createAlias(targetMeta.class, alias);
|
|
801
|
+
}
|
|
787
802
|
/**
|
|
788
803
|
* @internal
|
|
789
804
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, inspect, isRaw, LockMode, OptimisticLockError, QueryOperator, QueryOrderNumeric, raw, Raw, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';
|
|
1
|
+
import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, inspect, isRaw, LockMode, OptimisticLockError, QueryOperator, QueryOrderNumeric, raw, Raw, QueryHelper, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';
|
|
2
2
|
import { JoinType, QueryType } from './enums.js';
|
|
3
3
|
import { NativeQueryBuilder } from './NativeQueryBuilder.js';
|
|
4
4
|
/**
|
|
@@ -133,6 +133,14 @@ export class QueryBuilderHelper {
|
|
|
133
133
|
const primaryKeys = prop.owner ? prop.joinColumns : prop2.referencedColumnNames;
|
|
134
134
|
schema ??= prop.targetMeta?.schema === '*' ? '*' : this.driver.getSchemaName(prop.targetMeta);
|
|
135
135
|
cond = Utils.merge(cond, prop.where);
|
|
136
|
+
// For inverse side of polymorphic relations, add discriminator condition
|
|
137
|
+
if (!prop.owner && prop2.polymorphic && prop2.discriminatorColumn && prop2.discriminatorMap) {
|
|
138
|
+
const ownerMeta = this.aliasMap[ownerAlias]?.meta ?? this.metadata.get(this.entityName);
|
|
139
|
+
const discriminatorValue = QueryHelper.findDiscriminatorValue(prop2.discriminatorMap, ownerMeta.class);
|
|
140
|
+
if (discriminatorValue) {
|
|
141
|
+
cond[`${alias}.${prop2.discriminatorColumn}`] = discriminatorValue;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
136
144
|
return {
|
|
137
145
|
prop, type, cond, ownerAlias, alias, table, schema,
|
|
138
146
|
joinColumns, inverseJoinColumns, primaryKeys,
|
|
@@ -144,7 +152,9 @@ export class QueryBuilderHelper {
|
|
|
144
152
|
table: this.getTableName(prop.targetMeta.class),
|
|
145
153
|
schema: prop.targetMeta?.schema === '*' ? '*' : this.driver.getSchemaName(prop.targetMeta, { schema }),
|
|
146
154
|
joinColumns: prop.referencedColumnNames,
|
|
147
|
-
|
|
155
|
+
// For polymorphic relations, fieldNames includes the discriminator column which is not
|
|
156
|
+
// part of the join condition - use joinColumns (the FK columns only) instead
|
|
157
|
+
primaryKeys: prop.polymorphic ? prop.joinColumns : prop.fieldNames,
|
|
148
158
|
};
|
|
149
159
|
}
|
|
150
160
|
joinManyToManyReference(prop, ownerAlias, alias, pivotAlias, type, cond, path, schema) {
|
|
@@ -218,6 +228,15 @@ export class QueryBuilderHelper {
|
|
|
218
228
|
const alias = join.inverseAlias ?? join.alias;
|
|
219
229
|
join.cond[`${alias}.${typeProperty}`] = join.prop.targetMeta.discriminatorValue;
|
|
220
230
|
}
|
|
231
|
+
// For polymorphic relations, add discriminator condition to filter by target entity type
|
|
232
|
+
if (join.prop.polymorphic && join.prop.discriminatorColumn && join.prop.discriminatorMap) {
|
|
233
|
+
const discriminatorValue = QueryHelper.findDiscriminatorValue(join.prop.discriminatorMap, join.prop.targetMeta.class);
|
|
234
|
+
if (discriminatorValue) {
|
|
235
|
+
const discriminatorCol = this.platform.quoteIdentifier(`${join.ownerAlias}.${join.prop.discriminatorColumn}`);
|
|
236
|
+
conditions.push(`${discriminatorCol} = ?`);
|
|
237
|
+
params.push(discriminatorValue);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
221
240
|
let sql = method + ' ';
|
|
222
241
|
if (join.nested) {
|
|
223
242
|
sql += `(${this.platform.quoteIdentifier(table)} as ${this.platform.quoteIdentifier(join.alias)}`;
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -103,7 +103,7 @@ export class DatabaseTable {
|
|
|
103
103
|
const defaultValue = this.platform.getSchemaHelper().normalizeDefaultValue(prop.defaultRaw, prop.length);
|
|
104
104
|
this.columns[field].default = defaultValue;
|
|
105
105
|
});
|
|
106
|
-
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
106
|
+
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.polymorphic) {
|
|
107
107
|
const constraintName = this.getIndexName(prop.foreignKeyName ?? true, prop.fieldNames, 'foreign');
|
|
108
108
|
let schema = prop.targetMeta.root.schema === '*' ? this.schema : (prop.targetMeta.root.schema ?? config.get('schema', this.platform.getDefaultSchemaName()));
|
|
109
109
|
if (prop.referencedTableName.includes('.')) {
|