@mikro-orm/sql 7.2.0-dev.13 → 7.2.0-dev.14
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 +0 -7
- package/AbstractSqlDriver.js +46 -31
- package/AbstractSqlPlatform.d.ts +1 -1
- package/AbstractSqlPlatform.js +3 -0
- package/SqlEntityManager.js +3 -1
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +1 -1
- package/dialects/postgresql/BasePostgreSqlPlatform.js +2 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +1 -1
- package/dialects/sqlite/SqliteSchemaHelper.js +1 -1
- package/package.json +3 -3
- package/plugin/transformer.d.ts +7 -1
- package/plugin/transformer.js +60 -1
- package/query/CriteriaNodeFactory.js +4 -0
- package/query/ObjectCriteriaNode.js +2 -1
- package/query/QueryBuilder.d.ts +26 -3
- package/query/QueryBuilder.js +118 -16
- package/query/QueryBuilderHelper.d.ts +5 -0
- package/query/QueryBuilderHelper.js +29 -8
- package/schema/SchemaComparator.js +4 -4
- package/typings.d.ts +2 -0
package/AbstractSqlDriver.d.ts
CHANGED
|
@@ -118,13 +118,6 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
118
118
|
mergeJoinedResult<T extends object>(rawResults: EntityData<T>[], meta: EntityMetadata<T>, joinedProps: PopulateOptions<T>[]): EntityData<T>[];
|
|
119
119
|
protected shouldHaveColumn<T, U>(meta: EntityMetadata<T>, prop: EntityProperty<U>, populate: readonly PopulateOptions<U>[], fields?: readonly InternalField<U>[], exclude?: readonly InternalField<U>[]): boolean;
|
|
120
120
|
protected getFieldsForJoinedLoad<T extends object>(qb: AnyQueryBuilder<T>, meta: EntityMetadata<T>, options: FieldsForJoinedLoadOptions<T>): InternalField<T>[];
|
|
121
|
-
/**
|
|
122
|
-
* Walks the TPT inheritance chain of `leafMeta` and INNER JOINs each parent table.
|
|
123
|
-
* Registers the parent aliases in `qb.state.tptAlias` so column resolution finds them
|
|
124
|
-
* when filter conditions reference parent-table columns.
|
|
125
|
-
* @internal
|
|
126
|
-
*/
|
|
127
|
-
protected addTPTParentJoinsForRelation<T extends object>(qb: AnyQueryBuilder<T>, leafMeta: EntityMetadata, leafAlias: string, basePath: string): void;
|
|
128
121
|
/**
|
|
129
122
|
* Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
|
|
130
123
|
* @internal
|
package/AbstractSqlDriver.js
CHANGED
|
@@ -915,7 +915,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
915
915
|
if (!options.upsert && options.unionWhere?.length) {
|
|
916
916
|
where = (await this.applyUnionWhere(meta, where, options, true));
|
|
917
917
|
}
|
|
918
|
-
if (
|
|
918
|
+
if (options.upsert && meta.tptParent) {
|
|
919
|
+
res = await this.nativeUpdateMany(entityName, [where], [data], options);
|
|
920
|
+
}
|
|
921
|
+
else if (Utils.hasObjectKeys(data) || (meta.inheritanceType === 'tpt' && meta.ownsVersionProperty())) {
|
|
922
|
+
// a TPT table declaring the version property is bumped even when only other tables of the hierarchy changed
|
|
919
923
|
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
920
924
|
qb.setAbortOptions(pickAbortOptions(options));
|
|
921
925
|
if (options.upsert) {
|
|
@@ -941,7 +945,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
941
945
|
qb.update(data).where(where);
|
|
942
946
|
// reload generated columns and version fields
|
|
943
947
|
const returning = [];
|
|
944
|
-
meta
|
|
948
|
+
this.getTableProps(meta)
|
|
945
949
|
.filter(prop => (prop.generated && !prop.primary) || prop.version)
|
|
946
950
|
.forEach(prop => returning.push(prop.name));
|
|
947
951
|
qb.returning(returning);
|
|
@@ -957,13 +961,38 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
957
961
|
options.convertCustomTypes ??= true;
|
|
958
962
|
const meta = this.metadata.get(entityName);
|
|
959
963
|
if (options.upsert) {
|
|
964
|
+
if (meta.tptParent) {
|
|
965
|
+
// TPT parent tables go first, the PK they provide is the conflict target of this table
|
|
966
|
+
await this.nativeUpdateMany(meta.tptParent.class, where, data, options);
|
|
967
|
+
for (const [i, row] of data.entries()) {
|
|
968
|
+
if (meta.primaryKeys.some(pk => row[pk] == null)) {
|
|
969
|
+
const found = await this.findOne(meta.tptParent.class, where[i], {
|
|
970
|
+
fields: meta.primaryKeys,
|
|
971
|
+
ctx: options.ctx,
|
|
972
|
+
connectionType: 'write',
|
|
973
|
+
schema: options.schema,
|
|
974
|
+
});
|
|
975
|
+
meta.primaryKeys.forEach(pk => (row[pk] = found?.[pk]));
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
options = { ...options, onConflictFields: meta.primaryKeys, onConflictWhere: undefined };
|
|
979
|
+
}
|
|
960
980
|
const uniqueFields = options.onConflictFields ??
|
|
961
981
|
(Utils.isPlainObject(where[0])
|
|
962
982
|
? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key))
|
|
963
983
|
: meta.primaryKeys);
|
|
964
984
|
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
965
985
|
qb.setAbortOptions(pickAbortOptions(options));
|
|
966
|
-
|
|
986
|
+
let returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
|
|
987
|
+
if (meta.inheritanceType === 'tpt') {
|
|
988
|
+
// each TPT table only carries its own columns, the entity is reloaded instead of mapping the returned rows
|
|
989
|
+
const own = (key) => meta.primaryKeys.includes(key) ||
|
|
990
|
+
this.getTableProps(meta).some(prop => prop.name === key.split('.')[0]);
|
|
991
|
+
data = data.map(row => Object.fromEntries(Object.entries(row).filter(([key]) => own(key))));
|
|
992
|
+
options.onConflictMergeFields = options.onConflictMergeFields?.filter(f => own(f));
|
|
993
|
+
options.onConflictExcludeFields = options.onConflictExcludeFields?.filter(f => own(f));
|
|
994
|
+
returning = [];
|
|
995
|
+
}
|
|
967
996
|
qb.insert(data)
|
|
968
997
|
.onConflict(uniqueFields)
|
|
969
998
|
.returning(returning);
|
|
@@ -977,7 +1006,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
977
1006
|
if (options.onConflictWhere) {
|
|
978
1007
|
qb.where(options.onConflictWhere);
|
|
979
1008
|
}
|
|
980
|
-
|
|
1009
|
+
const res = await this.rethrow(qb.execute('run', false));
|
|
1010
|
+
return meta.inheritanceType === 'tpt' ? { ...res, row: undefined, rows: [] } : res;
|
|
981
1011
|
}
|
|
982
1012
|
const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
|
|
983
1013
|
const keys = new Set();
|
|
@@ -992,7 +1022,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
992
1022
|
}
|
|
993
1023
|
}
|
|
994
1024
|
// reload generated columns and version fields
|
|
995
|
-
meta.
|
|
1025
|
+
meta.getPrimaryProps().forEach(prop => returning.add(prop.name));
|
|
1026
|
+
this.getTableProps(meta)
|
|
1027
|
+
.filter(prop => prop.generated || prop.version)
|
|
1028
|
+
.forEach(prop => returning.add(prop.name));
|
|
996
1029
|
const pkCond = Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames))
|
|
997
1030
|
.map(pk => `${this.platform.quoteIdentifier(pk)} = ?`)
|
|
998
1031
|
.join(' and ');
|
|
@@ -1050,7 +1083,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1050
1083
|
return sql;
|
|
1051
1084
|
});
|
|
1052
1085
|
}
|
|
1053
|
-
if (meta.
|
|
1086
|
+
if (meta.ownsVersionProperty()) {
|
|
1054
1087
|
const versionProperty = meta.properties[meta.versionProperty];
|
|
1055
1088
|
const quotedFieldName = this.platform.quoteIdentifier(versionProperty.fieldNames[0]);
|
|
1056
1089
|
sql += `${quotedFieldName} = `;
|
|
@@ -1063,7 +1096,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1063
1096
|
sql += `, `;
|
|
1064
1097
|
}
|
|
1065
1098
|
sql = sql.substring(0, sql.length - 2) + ' where ';
|
|
1066
|
-
const pkProps = meta.primaryKeys.concat(...meta.
|
|
1099
|
+
const pkProps = meta.primaryKeys.concat(...meta.getOwnConcurrencyCheckKeys());
|
|
1067
1100
|
const pks = Utils.flatten(pkProps.map(pk => meta.properties[pk].fieldNames));
|
|
1068
1101
|
const useTupleIn = pks.length <= 1 || this.platform.allowsComparingTuples();
|
|
1069
1102
|
const condTemplate = useTupleIn
|
|
@@ -1649,6 +1682,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1649
1682
|
if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
|
|
1650
1683
|
return false;
|
|
1651
1684
|
}
|
|
1685
|
+
// Polymorphic to-one flattened from an object embeddable lives inside a JSON column, so the join
|
|
1686
|
+
// conditions would need JSON extraction for both the discriminator and the FK; fall back to SELECT_IN.
|
|
1687
|
+
if (prop.polymorphic && prop.object && prop.embedded) {
|
|
1688
|
+
return false;
|
|
1689
|
+
}
|
|
1652
1690
|
if (strategy !== LoadStrategy.JOINED) {
|
|
1653
1691
|
// force joined strategy for explicit 1:1 owner populate hint as it would require a join anyway
|
|
1654
1692
|
return prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner;
|
|
@@ -1779,7 +1817,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1779
1817
|
// INNER JOINs get nested inside the polymorphic LEFT JOIN by processNestedJoins, which
|
|
1780
1818
|
// keeps the resulting query valid for rows pointing to other polymorphic targets.
|
|
1781
1819
|
if (targetMeta.inheritanceType === 'tpt' && targetMeta.tptParent) {
|
|
1782
|
-
|
|
1820
|
+
qb.addTPTParentJoins(targetMeta, tableAlias, targetPath);
|
|
1783
1821
|
}
|
|
1784
1822
|
// For polymorphic targets that are TPT base classes, also LEFT JOIN
|
|
1785
1823
|
// all descendant tables so child-specific fields can be selected.
|
|
@@ -1827,10 +1865,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1827
1865
|
: JoinType.leftJoin;
|
|
1828
1866
|
const schema = prop.targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : prop.targetMeta.schema;
|
|
1829
1867
|
qb.join(field, tableAlias, {}, joinType, path, schema);
|
|
1830
|
-
// For relations to TPT child entities, INNER JOIN parent tables (GH #7469)
|
|
1831
|
-
if (meta2.inheritanceType === 'tpt' && meta2.tptParent) {
|
|
1832
|
-
this.addTPTParentJoinsForRelation(qb, meta2, tableAlias, path);
|
|
1833
|
-
}
|
|
1834
1868
|
// For relations to TPT base classes, add LEFT JOINs for all child tables (polymorphic loading)
|
|
1835
1869
|
if (meta2.inheritanceType === 'tpt' && meta2.tptChildren?.length && !ref) {
|
|
1836
1870
|
// Use the registry metadata to ensure allTPTDescendants is available
|
|
@@ -1877,25 +1911,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1877
1911
|
}
|
|
1878
1912
|
return fields;
|
|
1879
1913
|
}
|
|
1880
|
-
/**
|
|
1881
|
-
* Walks the TPT inheritance chain of `leafMeta` and INNER JOINs each parent table.
|
|
1882
|
-
* Registers the parent aliases in `qb.state.tptAlias` so column resolution finds them
|
|
1883
|
-
* when filter conditions reference parent-table columns.
|
|
1884
|
-
* @internal
|
|
1885
|
-
*/
|
|
1886
|
-
addTPTParentJoinsForRelation(qb, leafMeta, leafAlias, basePath) {
|
|
1887
|
-
let childAlias = leafAlias;
|
|
1888
|
-
let childMeta = leafMeta;
|
|
1889
|
-
while (childMeta.tptParent) {
|
|
1890
|
-
const parentMeta = childMeta.tptParent;
|
|
1891
|
-
const parentAlias = qb.getNextAlias(parentMeta.className);
|
|
1892
|
-
qb.createAlias(parentMeta.class, parentAlias);
|
|
1893
|
-
qb.state.tptAlias[`${leafAlias}:${parentMeta.className}`] = parentAlias;
|
|
1894
|
-
qb.addPropertyJoin(childMeta.tptParentProp, childAlias, parentAlias, JoinType.innerJoin, `${basePath}.[tpt]${childMeta.className}`);
|
|
1895
|
-
childAlias = parentAlias;
|
|
1896
|
-
childMeta = parentMeta;
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
1914
|
/**
|
|
1900
1915
|
* Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
|
|
1901
1916
|
* @internal
|
package/AbstractSqlPlatform.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export declare abstract class AbstractSqlPlatform extends Platform {
|
|
|
28
28
|
getReleaseSavepointSQL(savepointName: string): string;
|
|
29
29
|
quoteValue(value: any): string;
|
|
30
30
|
getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string | RawQueryFragment;
|
|
31
|
-
getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string | RawQueryFragment;
|
|
31
|
+
getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean | string, value?: unknown): string | RawQueryFragment;
|
|
32
32
|
/**
|
|
33
33
|
* Quotes a key for use inside a JSON path expression (e.g. `$.key`).
|
|
34
34
|
* Simple alphanumeric keys are left unquoted; others are wrapped in double quotes
|
package/AbstractSqlPlatform.js
CHANGED
|
@@ -75,6 +75,9 @@ export class AbstractSqlPlatform extends Platform {
|
|
|
75
75
|
getSearchJsonPropertyKey(path, type, aliased, value) {
|
|
76
76
|
const [a, ...b] = path;
|
|
77
77
|
const jsonPath = this.quoteValue(`$.${b.map(this.quoteJsonKey).join('.')}`);
|
|
78
|
+
if (typeof aliased === 'string') {
|
|
79
|
+
return raw(`json_extract(${this.quoteIdentifier(`${aliased}.${a}`)}, ${jsonPath})`);
|
|
80
|
+
}
|
|
78
81
|
if (aliased) {
|
|
79
82
|
return raw(alias => `json_extract(${this.quoteIdentifier(`${alias}.${a}`)}, ${jsonPath})`);
|
|
80
83
|
}
|
package/SqlEntityManager.js
CHANGED
|
@@ -81,7 +81,9 @@ export class SqlEntityManager extends EntityManager {
|
|
|
81
81
|
const { where: rawWhere, ...countOptions } = options;
|
|
82
82
|
await em.tryFlush(entityName, options);
|
|
83
83
|
const where = await em.processWhere(entityName, rawWhere ?? {}, options, 'read');
|
|
84
|
-
|
|
84
|
+
// match `em.count()` semantics: an active transaction always wins over the requested connection type
|
|
85
|
+
const connectionType = em.getTransactionContext() ? 'write' : options.connectionType;
|
|
86
|
+
const qb = em.createQueryBuilder(meta.class, undefined, connectionType);
|
|
85
87
|
qb
|
|
86
88
|
.select([...fields, raw('count(*) as cnt')])
|
|
87
89
|
.where(where)
|
|
@@ -88,7 +88,7 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
|
88
88
|
}): string;
|
|
89
89
|
getBlobDeclarationSQL(): string;
|
|
90
90
|
getJsonDeclarationSQL(): string;
|
|
91
|
-
getSearchJsonPropertyKey(path: string[], type: string | undefined | Type, aliased: boolean, value?: unknown): string | RawQueryFragment;
|
|
91
|
+
getSearchJsonPropertyKey(path: string[], type: string | undefined | Type, aliased: boolean | string, value?: unknown): string | RawQueryFragment;
|
|
92
92
|
getJsonIndexDefinition(index: IndexDef): string[];
|
|
93
93
|
quoteIdentifier(id: string | {
|
|
94
94
|
toString: () => string;
|
|
@@ -285,7 +285,8 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
|
285
285
|
getSearchJsonPropertyKey(path, type, aliased, value) {
|
|
286
286
|
const first = path.shift();
|
|
287
287
|
const last = path.pop();
|
|
288
|
-
const
|
|
288
|
+
const alias = typeof aliased === 'string' ? aliased : ALIAS_REPLACEMENT;
|
|
289
|
+
const root = this.quoteIdentifier(aliased ? `${alias}.${first}` : first);
|
|
289
290
|
type = typeof type === 'string' ? this.getMappedType(type).runtimeType : String(type);
|
|
290
291
|
const cast = (key) => raw(type in this.#jsonTypeCasts ? `(${key})::${this.#jsonTypeCasts[type]}` : key);
|
|
291
292
|
let lastOperator = '->>';
|
|
@@ -494,7 +494,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
494
494
|
// SchemaHelper.createCheck).
|
|
495
495
|
const m = /^check \(\((.*)\)\)$/is.exec(check.expression);
|
|
496
496
|
const single = m ? null : /^check \((.*)\)$/is.exec(check.expression);
|
|
497
|
-
const def = m ? m[1].replace(/\(([^()]*)\)::\w
|
|
497
|
+
const def = m ? m[1].replace(/\(([^()]*)\)::\w+(?:\[\])?/g, '$1') : single ? single[1] : check.expression;
|
|
498
498
|
ret[key].push({
|
|
499
499
|
name: check.name,
|
|
500
500
|
columnName: check.column_name,
|
|
@@ -489,7 +489,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
489
489
|
* Foreign key references can only point to tables in the same database.
|
|
490
490
|
*/
|
|
491
491
|
getReferencedTableName(referencedTableName, schema) {
|
|
492
|
-
const [
|
|
492
|
+
const [, tableName] = this.splitTableName(referencedTableName);
|
|
493
493
|
// Strip any schema prefix - SQLite REFERENCES clause doesn't support it
|
|
494
494
|
return tableName;
|
|
495
495
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.2.0-dev.
|
|
3
|
+
"version": "7.2.0-dev.14",
|
|
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",
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"kysely": "0.29.5"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@mikro-orm/core": "^7.1.
|
|
53
|
+
"@mikro-orm/core": "^7.1.15"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.2.0-dev.
|
|
56
|
+
"@mikro-orm/core": "7.2.0-dev.14"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
package/plugin/transformer.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type EntityMetadata, type EntityProperty, type Type } from '@mikro-orm/core';
|
|
2
|
-
import { type CommonTableExpressionNameNode, type DeleteQueryNode, type InsertQueryNode, type JoinNode, type MergeQueryNode, type OperationNode, type QueryId, type SelectQueryNode, type UpdateQueryNode, type WithNode, ColumnNode, IdentifierNode, OperationNodeTransformer, SelectionNode, TableNode, ValueNode } from 'kysely';
|
|
2
|
+
import { type BinaryOperationNode, type CommonTableExpressionNameNode, type DeleteQueryNode, type InsertQueryNode, type JoinNode, type MergeQueryNode, type OperationNode, type QueryId, type SelectQueryNode, type UpdateQueryNode, type WithNode, ColumnNode, IdentifierNode, OperationNodeTransformer, SelectionNode, TableNode, ValueNode } from 'kysely';
|
|
3
3
|
import type { MikroKyselyPluginOptions } from './index.js';
|
|
4
4
|
import type { SqlEntityManager } from '../SqlEntityManager.js';
|
|
5
5
|
export declare class MikroTransformer extends OperationNodeTransformer {
|
|
@@ -28,6 +28,12 @@ export declare class MikroTransformer extends OperationNodeTransformer {
|
|
|
28
28
|
processOnUpdateHooks(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode;
|
|
29
29
|
processInsertValues(node: InsertQueryNode, meta: EntityMetadata): InsertQueryNode;
|
|
30
30
|
processUpdateValues(node: UpdateQueryNode, meta: EntityMetadata): UpdateQueryNode;
|
|
31
|
+
transformBinaryOperation(node: BinaryOperationNode, queryId: QueryId): BinaryOperationNode;
|
|
32
|
+
/** Resolve the entity property a comparison's left operand refers to, so its value operand can be converted. */
|
|
33
|
+
resolveOperandProperty(operand: OperationNode): {
|
|
34
|
+
prop: EntityProperty;
|
|
35
|
+
fieldName: string;
|
|
36
|
+
} | undefined;
|
|
31
37
|
processInputValueNode(prop: EntityProperty | undefined, fieldName: string | undefined, valueNode: ValueNode): OperationNode;
|
|
32
38
|
expandSelections(selections: readonly SelectionNode[]): readonly SelectionNode[];
|
|
33
39
|
expandSelection(sel: SelectionNode): SelectionNode[] | null;
|
package/plugin/transformer.js
CHANGED
|
@@ -455,6 +455,60 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
455
455
|
updates,
|
|
456
456
|
};
|
|
457
457
|
}
|
|
458
|
+
transformBinaryOperation(node, queryId) {
|
|
459
|
+
const transformed = super.transformBinaryOperation(node, queryId);
|
|
460
|
+
if (!this.#options.convertValues) {
|
|
461
|
+
return transformed;
|
|
462
|
+
}
|
|
463
|
+
const resolved = this.resolveOperandProperty(transformed.leftOperand);
|
|
464
|
+
if (!resolved) {
|
|
465
|
+
return transformed;
|
|
466
|
+
}
|
|
467
|
+
const { prop, fieldName } = resolved;
|
|
468
|
+
const right = transformed.rightOperand;
|
|
469
|
+
if (ValueNode.is(right)) {
|
|
470
|
+
const converted = this.processInputValueNode(prop, fieldName, right);
|
|
471
|
+
return converted === right ? transformed : { ...transformed, rightOperand: converted };
|
|
472
|
+
}
|
|
473
|
+
if (PrimitiveValueListNode.is(right)) {
|
|
474
|
+
// upgrade to ValueListNode when the type needs SQL-side wrapping, since
|
|
475
|
+
// PrimitiveValueListNode can only hold primitives
|
|
476
|
+
if (prop.hasConvertToDatabaseValueSQL) {
|
|
477
|
+
const values = right.values.map(value => this.processInputValueNode(prop, fieldName, ValueNode.create(value)));
|
|
478
|
+
return { ...transformed, rightOperand: ValueListNode.create(values) };
|
|
479
|
+
}
|
|
480
|
+
const values = right.values.map(value => this.prepareInputValue(prop, value, true));
|
|
481
|
+
return values.every((value, idx) => value === right.values[idx])
|
|
482
|
+
? transformed
|
|
483
|
+
: { ...transformed, rightOperand: PrimitiveValueListNode.create(values) };
|
|
484
|
+
}
|
|
485
|
+
if (ValueListNode.is(right)) {
|
|
486
|
+
let changed = false;
|
|
487
|
+
const values = right.values.map(valueNode => {
|
|
488
|
+
if (!ValueNode.is(valueNode)) {
|
|
489
|
+
return valueNode;
|
|
490
|
+
}
|
|
491
|
+
const converted = this.processInputValueNode(prop, fieldName, valueNode);
|
|
492
|
+
if (converted !== valueNode) {
|
|
493
|
+
changed = true;
|
|
494
|
+
}
|
|
495
|
+
return converted;
|
|
496
|
+
});
|
|
497
|
+
return changed ? { ...transformed, rightOperand: ValueListNode.create(values) } : transformed;
|
|
498
|
+
}
|
|
499
|
+
return transformed;
|
|
500
|
+
}
|
|
501
|
+
/** Resolve the entity property a comparison's left operand refers to, so its value operand can be converted. */
|
|
502
|
+
resolveOperandProperty(operand) {
|
|
503
|
+
if (!ReferenceNode.is(operand) || !ColumnNode.is(operand.column)) {
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
const tableName = operand.table ? this.getTableName(operand.table) : undefined;
|
|
507
|
+
const meta = this.findOwnerMeta(tableName);
|
|
508
|
+
const fieldName = this.normalizeColumnName(operand.column.column);
|
|
509
|
+
const prop = this.findProperty(meta, fieldName);
|
|
510
|
+
return prop ? { prop, fieldName } : undefined;
|
|
511
|
+
}
|
|
458
512
|
processInputValueNode(prop, fieldName, valueNode) {
|
|
459
513
|
const converted = this.prepareInputValue(prop, valueNode.value, true);
|
|
460
514
|
const newValueNode = converted === valueNode.value
|
|
@@ -543,8 +597,13 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
543
597
|
if (name) {
|
|
544
598
|
return this.lookupInContextStack(name) ?? this.#subqueryAliasMap.get(name) ?? this.findEntityMetadata(name);
|
|
545
599
|
}
|
|
600
|
+
// the stack can be empty when transforming a raw root node with embedded expressions
|
|
601
|
+
const context = this.#contextStack[this.#contextStack.length - 1];
|
|
602
|
+
if (!context) {
|
|
603
|
+
return undefined;
|
|
604
|
+
}
|
|
546
605
|
let single;
|
|
547
|
-
for (const meta of
|
|
606
|
+
for (const meta of context.values()) {
|
|
548
607
|
if (!meta) {
|
|
549
608
|
continue;
|
|
550
609
|
}
|
|
@@ -57,6 +57,10 @@ export class CriteriaNodeFactory {
|
|
|
57
57
|
if (isNotEmbedded && prop?.customType instanceof JsonType) {
|
|
58
58
|
return this.createScalarNode(metadata, childEntity, val, node, key, validate);
|
|
59
59
|
}
|
|
60
|
+
// operator payloads under an unresolvable alias-prefixed key (e.g. `a.meta`) are opaque values, not entity criteria
|
|
61
|
+
if (!prop && !rawField && Utils.isOperator(key, false) && String(node.key).includes('.')) {
|
|
62
|
+
return this.createScalarNode(metadata, entityName, val, node, key, validate);
|
|
63
|
+
}
|
|
60
64
|
if (prop?.kind === ReferenceKind.SCALAR && val != null && Object.keys(val).some(f => f in GroupOperator)) {
|
|
61
65
|
throw ValidationError.cannotUseGroupOperatorsInsideScalars(entityName, prop.name, payload);
|
|
62
66
|
}
|
|
@@ -113,7 +113,8 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
113
113
|
}
|
|
114
114
|
else if (isRawField) {
|
|
115
115
|
const rawField = RawQueryFragment.getKnownFragment(field);
|
|
116
|
-
|
|
116
|
+
qb.ensureTPTJoins();
|
|
117
|
+
o[raw(qb.helper.replaceAliases(rawField.sql, alias), rawField.params)] = payload;
|
|
117
118
|
}
|
|
118
119
|
else if (!childNode.validate && !childNode.prop && !field.includes('.') && !operator) {
|
|
119
120
|
// wrap unknown fields in raw() to prevent alias prefixing (e.g. raw SQL aliases in HAVING)
|
package/query/QueryBuilder.d.ts
CHANGED
|
@@ -439,7 +439,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
439
439
|
*/
|
|
440
440
|
join<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field, alias: Alias, cond?: JoinCondition<JoinedEntityType<Entity, Context, Field & string>, Alias>, type?: JoinType, path?: string, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>, RawAliases, Fields, CTEs>;
|
|
441
441
|
/**
|
|
442
|
-
* Adds a JOIN clause to the query for a subquery.
|
|
442
|
+
* Adds a JOIN clause to the query for a subquery. Use `sql.ref('...')` to join a table or CTE by name.
|
|
443
443
|
*/
|
|
444
444
|
join<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, type?: JoinType, path?: string, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
|
|
445
445
|
/**
|
|
@@ -447,7 +447,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
447
447
|
*/
|
|
448
448
|
innerJoin<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field, alias: Alias, cond?: JoinCondition<JoinedEntityType<Entity, Context, Field & string>, Alias>, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>, RawAliases, Fields, CTEs>;
|
|
449
449
|
/**
|
|
450
|
-
* Adds an INNER JOIN clause to the query for a subquery.
|
|
450
|
+
* Adds an INNER JOIN clause to the query for a subquery. Use `sql.ref('...')` to join a table or CTE by name.
|
|
451
451
|
*/
|
|
452
452
|
innerJoin<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
|
|
453
453
|
innerJoinLateral<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
|
|
@@ -456,7 +456,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
456
456
|
*/
|
|
457
457
|
leftJoin<Field extends QBField<Entity, RootAlias, Context>, Alias extends string>(field: Field, alias: Alias, cond?: JoinCondition<JoinedEntityType<Entity, Context, Field & string>, Alias>, schema?: string): SelectQueryBuilder<Entity, RootAlias, ModifyHint<RootAlias, Context, Hint, Field> & {}, ModifyContext<Entity, Context, Field, Alias>, RawAliases, Fields, CTEs>;
|
|
458
458
|
/**
|
|
459
|
-
* Adds a LEFT JOIN clause to the query for a subquery.
|
|
459
|
+
* Adds a LEFT JOIN clause to the query for a subquery. Use `sql.ref('...')` to join a table or CTE by name.
|
|
460
460
|
*/
|
|
461
461
|
leftJoin<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
|
|
462
462
|
leftJoinLateral<Alias extends string>(field: RawQueryFragment | QueryBuilder<any>, alias: Alias, cond?: RawJoinCondition, schema?: string): SelectQueryBuilder<Entity, RootAlias, Hint, ModifyContext<Entity, Context, string, Alias>, RawAliases, Fields, CTEs>;
|
|
@@ -782,6 +782,8 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
782
782
|
* @internal
|
|
783
783
|
*/
|
|
784
784
|
getJoinForPath(path: string, options?: ICriteriaNodeProcessOptions): JoinOptions | undefined;
|
|
785
|
+
/** Branch-insensitive path matching still must not cross branches — segments with different explicit branch markers (e.g. `Mobile[0]` vs `Mobile[1]`) belong to sibling joins. */
|
|
786
|
+
private branchesConflict;
|
|
785
787
|
/**
|
|
786
788
|
* @internal
|
|
787
789
|
*/
|
|
@@ -948,6 +950,13 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
948
950
|
*/
|
|
949
951
|
addPropertyJoin(prop: EntityProperty, ownerAlias: string, alias: string, type: JoinType, path: string, schema?: string): string;
|
|
950
952
|
private joinReference;
|
|
953
|
+
/**
|
|
954
|
+
* Walks the TPT inheritance chain of `leafMeta` and INNER JOINs each parent table.
|
|
955
|
+
* Registers the parent aliases in `state.tptAlias` so column resolution finds them
|
|
956
|
+
* when conditions reference parent-table columns.
|
|
957
|
+
* @internal
|
|
958
|
+
*/
|
|
959
|
+
addTPTParentJoins(leafMeta: EntityMetadata, leafAlias: string, basePath: string): void;
|
|
951
960
|
protected prepareFields<T>(fields: InternalField<T>[], type?: 'where' | 'groupBy' | 'sub-query', schema?: string): (string | RawQueryFragment)[];
|
|
952
961
|
/**
|
|
953
962
|
* Resolves nested paths like `a.books.title` to their actual field references.
|
|
@@ -994,6 +1003,20 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
994
1003
|
processPopulateHint(): void;
|
|
995
1004
|
private processPopulateWhere;
|
|
996
1005
|
private mergeOnConditions;
|
|
1006
|
+
/**
|
|
1007
|
+
* `$or` branches can be moved to a join's `on` clause only when they all target that same join
|
|
1008
|
+
* and stay flat — a partial `$or` in the `on` clause would drop rows matching a sibling branch,
|
|
1009
|
+
* and nested operators cannot be preserved inside a distributed disjunction, as `mergeOnConditions`
|
|
1010
|
+
* would flatten them into `and` conjuncts.
|
|
1011
|
+
*/
|
|
1012
|
+
private canDistributeOrBranches;
|
|
1013
|
+
private getOrBranchAliases;
|
|
1014
|
+
/**
|
|
1015
|
+
* An entity filter's `$or` that cannot be distributed is applied intact to the `on` clause of the
|
|
1016
|
+
* outermost join common to all targeted joins, nesting the targeted joins under it so the clause
|
|
1017
|
+
* can reference their aliases.
|
|
1018
|
+
*/
|
|
1019
|
+
private mergeFilterOrCondition;
|
|
997
1020
|
/**
|
|
998
1021
|
* When adding an inner join on a left joined relation, we need to nest them,
|
|
999
1022
|
* otherwise the inner join could discard rows of the root table.
|
package/query/QueryBuilder.js
CHANGED
|
@@ -397,7 +397,10 @@ export class QueryBuilder {
|
|
|
397
397
|
* @internal
|
|
398
398
|
*/
|
|
399
399
|
scheduleFilterCheck(path) {
|
|
400
|
-
|
|
400
|
+
// deduplicate so filters forming a relation cycle cannot reschedule an already visited path forever
|
|
401
|
+
if (!this.#state.autoJoinedPaths.includes(path)) {
|
|
402
|
+
this.#state.autoJoinedPaths.push(path);
|
|
403
|
+
}
|
|
401
404
|
}
|
|
402
405
|
/**
|
|
403
406
|
* @internal
|
|
@@ -492,7 +495,10 @@ export class QueryBuilder {
|
|
|
492
495
|
return cond.some(c => this.condReferencesAlias(c, alias));
|
|
493
496
|
}
|
|
494
497
|
if (Utils.isPlainObject(cond)) {
|
|
495
|
-
return Object.entries(cond).some(([key, value]) =>
|
|
498
|
+
return Object.entries(cond).some(([key, value]) => {
|
|
499
|
+
const [keyAlias, field] = this.helper.splitField(key);
|
|
500
|
+
return this.helper.getTPTAliasForProperty(field, keyAlias) === alias || this.condReferencesAlias(value, alias);
|
|
501
|
+
});
|
|
496
502
|
}
|
|
497
503
|
return false;
|
|
498
504
|
}
|
|
@@ -901,6 +907,8 @@ export class QueryBuilder {
|
|
|
901
907
|
if (this.type === QueryType.INSERT) {
|
|
902
908
|
const returningProps = meta.hydrateProps
|
|
903
909
|
.filter(prop => prop.returning || (prop.persist !== false && ((prop.primary && prop.autoincrement) || prop.defaultRaw)))
|
|
910
|
+
// a TPT table can only return its own columns
|
|
911
|
+
.filter(prop => meta.inheritanceType !== 'tpt' || prop.primary || meta.ownProps.some(p => p.name === prop.name))
|
|
904
912
|
.filter(prop => !data || !(prop.name in data));
|
|
905
913
|
if (returningProps.length > 0) {
|
|
906
914
|
qb.returning(Utils.flatten(returningProps.map(prop => prop.fieldNames)));
|
|
@@ -990,12 +998,13 @@ export class QueryBuilder {
|
|
|
990
998
|
}
|
|
991
999
|
if (!join && options?.ignoreBranching) {
|
|
992
1000
|
join = joins.find(j => {
|
|
993
|
-
return j.path?.replace(/\[\d+]/g, '') === path.replace(/\[\d+]/g, '');
|
|
1001
|
+
return j.path?.replace(/\[\d+]/g, '') === path.replace(/\[\d+]/g, '') && !this.branchesConflict(j.path, path);
|
|
994
1002
|
});
|
|
995
1003
|
}
|
|
996
1004
|
if (!join && options?.matchPopulateJoins && options?.ignoreBranching) {
|
|
997
1005
|
join = joins.find(j => {
|
|
998
|
-
return j.path?.replace(/\[\d+]|\[populate]/g, '') === path.replace(/\[\d+]|\[populate]/g, '')
|
|
1006
|
+
return (j.path?.replace(/\[\d+]|\[populate]/g, '') === path.replace(/\[\d+]|\[populate]/g, '') &&
|
|
1007
|
+
!this.branchesConflict(j.path, path));
|
|
999
1008
|
});
|
|
1000
1009
|
}
|
|
1001
1010
|
if (!join && options?.matchPopulateJoins) {
|
|
@@ -1005,6 +1014,12 @@ export class QueryBuilder {
|
|
|
1005
1014
|
}
|
|
1006
1015
|
return join;
|
|
1007
1016
|
}
|
|
1017
|
+
/** Branch-insensitive path matching still must not cross branches — segments with different explicit branch markers (e.g. `Mobile[0]` vs `Mobile[1]`) belong to sibling joins. */
|
|
1018
|
+
branchesConflict(path1, path2) {
|
|
1019
|
+
const markers = (path) => path.split('.').map(segment => /\[(\d+)]/.exec(segment)?.[1]);
|
|
1020
|
+
const markers2 = markers(path2);
|
|
1021
|
+
return markers(path1).some((m, idx) => m != null && markers2[idx] != null && m !== markers2[idx]);
|
|
1022
|
+
}
|
|
1008
1023
|
/**
|
|
1009
1024
|
* @internal
|
|
1010
1025
|
*/
|
|
@@ -1410,19 +1425,19 @@ export class QueryBuilder {
|
|
|
1410
1425
|
prop.targetMeta = field.mainAlias.meta;
|
|
1411
1426
|
field = field.getNativeQuery();
|
|
1412
1427
|
}
|
|
1413
|
-
if (isRaw(field)) {
|
|
1414
|
-
field = this.platform.formatQuery(field.sql, field.params);
|
|
1415
|
-
}
|
|
1416
1428
|
const key = `${this.alias}.${prop.name}#${alias}`;
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1429
|
+
const join = { prop, alias, type, cond, schema, ownerAlias: this.alias };
|
|
1430
|
+
// `sql.ref('...')` is a bare table/CTE reference, join it by name instead of wrapping it as a sub-query
|
|
1431
|
+
if (isRaw(field) && field.sql === '??' && field.params.length === 1) {
|
|
1432
|
+
join.table = String(field.params[0]);
|
|
1433
|
+
}
|
|
1434
|
+
else {
|
|
1435
|
+
if (isRaw(field)) {
|
|
1436
|
+
field = this.platform.formatQuery(field.sql, field.params);
|
|
1437
|
+
}
|
|
1438
|
+
join.subquery = field.toString();
|
|
1439
|
+
}
|
|
1440
|
+
this.#state.joins[key] = join;
|
|
1426
1441
|
return { prop, key };
|
|
1427
1442
|
}
|
|
1428
1443
|
if (!subquery && type.includes('lateral')) {
|
|
@@ -1483,9 +1498,31 @@ export class QueryBuilder {
|
|
|
1483
1498
|
this.#state.joins[aliasedName] = this.helper.joinManyToOneReference(prop, ownerAlias, alias, type, cond, schema);
|
|
1484
1499
|
this.#state.joins[aliasedName].path ??= path;
|
|
1485
1500
|
}
|
|
1501
|
+
if (prop.targetMeta.inheritanceType === 'tpt' && prop.targetMeta.tptParent) {
|
|
1502
|
+
this.addTPTParentJoins(prop.targetMeta, alias, path);
|
|
1503
|
+
}
|
|
1486
1504
|
this.nestReferencedJoins(this.#state.joins[aliasedName]);
|
|
1487
1505
|
return { prop, key: aliasedName };
|
|
1488
1506
|
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Walks the TPT inheritance chain of `leafMeta` and INNER JOINs each parent table.
|
|
1509
|
+
* Registers the parent aliases in `state.tptAlias` so column resolution finds them
|
|
1510
|
+
* when conditions reference parent-table columns.
|
|
1511
|
+
* @internal
|
|
1512
|
+
*/
|
|
1513
|
+
addTPTParentJoins(leafMeta, leafAlias, basePath) {
|
|
1514
|
+
let childAlias = leafAlias;
|
|
1515
|
+
let childMeta = leafMeta;
|
|
1516
|
+
while (childMeta.tptParent) {
|
|
1517
|
+
const parentMeta = childMeta.tptParent;
|
|
1518
|
+
const parentAlias = this.getNextAlias(parentMeta.className);
|
|
1519
|
+
this.createAlias(parentMeta.class, parentAlias);
|
|
1520
|
+
this.#state.tptAlias[`${leafAlias}:${parentMeta.className}`] = parentAlias;
|
|
1521
|
+
this.addPropertyJoin(childMeta.tptParentProp, childAlias, parentAlias, JoinType.innerJoin, `${basePath}.[tpt]${childMeta.className}`);
|
|
1522
|
+
childAlias = parentAlias;
|
|
1523
|
+
childMeta = parentMeta;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1489
1526
|
prepareFields(fields, type = 'where', schema) {
|
|
1490
1527
|
const ret = [];
|
|
1491
1528
|
const getFieldName = (name, customAlias) => {
|
|
@@ -2038,6 +2075,13 @@ export class QueryBuilder {
|
|
|
2038
2075
|
for (const k of Object.keys(cond)) {
|
|
2039
2076
|
if (Utils.isOperator(k)) {
|
|
2040
2077
|
if (Array.isArray(cond[k])) {
|
|
2078
|
+
if (k === '$or' && !this.canDistributeOrBranches(cond[k], joins)) {
|
|
2079
|
+
// entity filters have no other sink, so their `$or` is kept intact on the outermost targeted join instead
|
|
2080
|
+
if (filter) {
|
|
2081
|
+
this.mergeFilterOrCondition(cond[k], joins);
|
|
2082
|
+
}
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2041
2085
|
cond[k].forEach((c) => this.mergeOnConditions(joins, c, filter, k));
|
|
2042
2086
|
}
|
|
2043
2087
|
/* v8 ignore next */
|
|
@@ -2068,8 +2112,66 @@ export class QueryBuilder {
|
|
|
2068
2112
|
else {
|
|
2069
2113
|
join.cond = { ...join.cond, [k]: cond[k] };
|
|
2070
2114
|
}
|
|
2115
|
+
this.nestReferencedJoins(join);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* `$or` branches can be moved to a join's `on` clause only when they all target that same join
|
|
2121
|
+
* and stay flat — a partial `$or` in the `on` clause would drop rows matching a sibling branch,
|
|
2122
|
+
* and nested operators cannot be preserved inside a distributed disjunction, as `mergeOnConditions`
|
|
2123
|
+
* would flatten them into `and` conjuncts.
|
|
2124
|
+
*/
|
|
2125
|
+
canDistributeOrBranches(branches, joins) {
|
|
2126
|
+
const aliases = this.getOrBranchAliases(branches);
|
|
2127
|
+
const targeted = joins.filter(j => aliases.has(j.alias));
|
|
2128
|
+
const flat = branches.every(branch => Object.keys(branch).every(k => !Utils.isOperator(k)));
|
|
2129
|
+
return aliases.size === 1 && targeted.length === 1 && flat;
|
|
2130
|
+
}
|
|
2131
|
+
getOrBranchAliases(branches) {
|
|
2132
|
+
const aliases = new Set();
|
|
2133
|
+
const collectAliases = (cond) => {
|
|
2134
|
+
for (const k of Object.keys(cond)) {
|
|
2135
|
+
if (Utils.isOperator(k)) {
|
|
2136
|
+
Utils.asArray(cond[k]).forEach((c) => collectAliases(c));
|
|
2137
|
+
}
|
|
2138
|
+
else {
|
|
2139
|
+
aliases.add(this.helper.splitField(k)[0]);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
};
|
|
2143
|
+
branches.forEach(collectAliases);
|
|
2144
|
+
return aliases;
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* An entity filter's `$or` that cannot be distributed is applied intact to the `on` clause of the
|
|
2148
|
+
* outermost join common to all targeted joins, nesting the targeted joins under it so the clause
|
|
2149
|
+
* can reference their aliases.
|
|
2150
|
+
*/
|
|
2151
|
+
mergeFilterOrCondition(branches, joins) {
|
|
2152
|
+
const aliases = this.getOrBranchAliases(branches);
|
|
2153
|
+
const chainOf = (join) => {
|
|
2154
|
+
const parent = joins.find(j => j.alias === join.ownerAlias);
|
|
2155
|
+
return parent ? [join, ...chainOf(parent)] : [join];
|
|
2156
|
+
};
|
|
2157
|
+
// chains go from each targeted join up to its root, so the first join present in all of them is the outermost common one
|
|
2158
|
+
const chains = joins.filter(j => aliases.has(j.alias)).map(chainOf);
|
|
2159
|
+
const anchor = chains[0]?.find(a => chains.every(chain => chain.includes(a)));
|
|
2160
|
+
/* v8 ignore next 3 */
|
|
2161
|
+
if (!anchor) {
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
for (const chain of chains) {
|
|
2165
|
+
// nest the chain below the anchor, so the anchor's `on` clause can reference the nested aliases
|
|
2166
|
+
for (let i = 0; chain[i] !== anchor; i++) {
|
|
2167
|
+
const nested = (chain[i + 1].nested ??= new Set());
|
|
2168
|
+
if (!nested.has(chain[i])) {
|
|
2169
|
+
chain[i].type = chain[i].type === JoinType.innerJoin ? JoinType.nestedInnerJoin : JoinType.nestedLeftJoin;
|
|
2170
|
+
nested.add(chain[i]);
|
|
2171
|
+
}
|
|
2071
2172
|
}
|
|
2072
2173
|
}
|
|
2174
|
+
anchor.cond = anchor.cond.$or ? { $and: [anchor.cond, { $or: branches }] } : { ...anchor.cond, $or: branches };
|
|
2073
2175
|
}
|
|
2074
2176
|
/**
|
|
2075
2177
|
* When adding an inner join on a left joined relation, we need to nest them,
|
|
@@ -14,6 +14,11 @@ export declare class QueryBuilderHelper {
|
|
|
14
14
|
* Returns the main alias if not a TPT property or if the property belongs to the main entity.
|
|
15
15
|
*/
|
|
16
16
|
getTPTAliasForProperty(propName: string, defaultAlias: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Replaces `ALIAS_REPLACEMENT` placeholders, resolving column-qualified ones via
|
|
19
|
+
* `getTPTAliasForProperty()` so TPT inherited columns map to their owning table.
|
|
20
|
+
*/
|
|
21
|
+
replaceAliases(sql: string, alias?: string): string;
|
|
17
22
|
mapper(field: string | Raw | RawQueryFragmentSymbol, type?: QueryType): string;
|
|
18
23
|
mapper(field: string | Raw | RawQueryFragmentSymbol, type?: QueryType, value?: any, alias?: string | null, schema?: string): string;
|
|
19
24
|
processData(data: Dictionary, convertCustomTypes: boolean, multi?: boolean): any;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, JsonType, inspect, isRaw, LockMode, OptimisticLockError, QueryOperator, QueryOrderNumeric, raw, Raw, QueryHelper, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';
|
|
2
2
|
import { EMBEDDABLE_ARRAY_OPS, JoinType, QueryType } from './enums.js';
|
|
3
|
+
/** Captures the column name that follows the alias placeholder. */
|
|
4
|
+
const QUALIFYING_ALIAS_RE = new RegExp(ALIAS_REPLACEMENT_RE + '(?=["\'`\\]]*\\.["\'`\\[]?([\\w$]+))', 'g');
|
|
3
5
|
/**
|
|
4
6
|
* @internal
|
|
5
7
|
*/
|
|
@@ -49,6 +51,15 @@ export class QueryBuilderHelper {
|
|
|
49
51
|
// Property not found in hierarchy, return default alias
|
|
50
52
|
return defaultAlias;
|
|
51
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Replaces `ALIAS_REPLACEMENT` placeholders, resolving column-qualified ones via
|
|
56
|
+
* `getTPTAliasForProperty()` so TPT inherited columns map to their owning table.
|
|
57
|
+
*/
|
|
58
|
+
replaceAliases(sql, alias = this.#alias) {
|
|
59
|
+
return sql
|
|
60
|
+
.replace(QUALIFYING_ALIAS_RE, (_, column) => this.getTPTAliasForProperty(column, alias))
|
|
61
|
+
.replaceAll(ALIAS_REPLACEMENT, alias);
|
|
62
|
+
}
|
|
52
63
|
mapper(field, type = QueryType.SELECT, value, alias, schema) {
|
|
53
64
|
if (isRaw(field)) {
|
|
54
65
|
return raw(field.sql, field.params);
|
|
@@ -97,7 +108,6 @@ export class QueryBuilderHelper {
|
|
|
97
108
|
// Only apply TPT resolution when `a` is an actual table alias (in aliasMap),
|
|
98
109
|
// not when it's an embedded property name like 'profile1.identity.links'
|
|
99
110
|
const isTableAlias = !!this.#aliasMap[a];
|
|
100
|
-
const baseAlias = isTableAlias ? a : this.#alias;
|
|
101
111
|
const resolvedAlias = isTableAlias ? this.getTPTAliasForProperty(prop?.name ?? f, a) : this.#alias;
|
|
102
112
|
const aliasPrefix = isTableNameAliasRequired ? resolvedAlias + '.' : '';
|
|
103
113
|
const fkIdx2 = prop?.fieldNames.findIndex(name => name === f) ?? -1;
|
|
@@ -249,7 +259,13 @@ export class QueryBuilderHelper {
|
|
|
249
259
|
}[join.type] ?? join.type;
|
|
250
260
|
const conditions = [];
|
|
251
261
|
const params = [];
|
|
252
|
-
|
|
262
|
+
if (join.prop.name === '__subquery__') {
|
|
263
|
+
// bare table/CTE references (`sql.ref()` joins) keep their literal name, only an explicit schema applies
|
|
264
|
+
schema = join.schema === '*' ? undefined : join.schema;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
schema = join.schema === '*' ? schema : (join.schema ?? schemaOverride);
|
|
268
|
+
}
|
|
253
269
|
if (schema && schema !== this.#platform.getDefaultSchemaName()) {
|
|
254
270
|
table = `${schema}.${table}`;
|
|
255
271
|
}
|
|
@@ -465,7 +481,7 @@ export class QueryBuilderHelper {
|
|
|
465
481
|
const op = cond[key] === null ? 'is' : '=';
|
|
466
482
|
if (Raw.isKnownFragmentSymbol(key)) {
|
|
467
483
|
const raw = Raw.getKnownFragment(key);
|
|
468
|
-
const sql = raw.sql
|
|
484
|
+
const sql = this.replaceAliases(raw.sql);
|
|
469
485
|
const value = Utils.asArray(cond[key]);
|
|
470
486
|
params.push(...raw.params);
|
|
471
487
|
if (value.length > 0) {
|
|
@@ -517,6 +533,11 @@ export class QueryBuilderHelper {
|
|
|
517
533
|
const replacement = this.getOperatorReplacement(op, value);
|
|
518
534
|
const rawField = Raw.isKnownFragmentSymbol(key);
|
|
519
535
|
const fields = rawField ? [key] : Utils.splitPrimaryKeys(key);
|
|
536
|
+
// a raw key's arity is opaque, so the row-value form is detected from the payload instead
|
|
537
|
+
const rowValues = rawField &&
|
|
538
|
+
['$in', '$nin'].includes(op) &&
|
|
539
|
+
Array.isArray(value[op]) &&
|
|
540
|
+
value[op].every((v) => Array.isArray(v));
|
|
520
541
|
if (fields.length > 1 && Array.isArray(value[op])) {
|
|
521
542
|
const singleTuple = !value[op].every((v) => Array.isArray(v));
|
|
522
543
|
if (!this.#platform.allowsComparingTuples()) {
|
|
@@ -576,7 +597,7 @@ export class QueryBuilderHelper {
|
|
|
576
597
|
if (opValueIsRaw || typeof opValue?.toRaw === 'function') {
|
|
577
598
|
const query = opValueIsRaw ? opValue : opValue.toRaw();
|
|
578
599
|
const mappedKey = this.mapper(key, type, query, null);
|
|
579
|
-
let sql = query.sql
|
|
600
|
+
let sql = this.replaceAliases(query.sql);
|
|
580
601
|
if (['$in', '$nin'].includes(op)) {
|
|
581
602
|
sql = `(${sql})`;
|
|
582
603
|
}
|
|
@@ -585,15 +606,15 @@ export class QueryBuilderHelper {
|
|
|
585
606
|
}
|
|
586
607
|
else {
|
|
587
608
|
const mappedKey = this.mapper(key, type, opValue, null);
|
|
588
|
-
const val = this.getValueReplacement(fields, opValue, params, op, prop);
|
|
609
|
+
const val = this.getValueReplacement(fields, opValue, params, op, prop, rowValues);
|
|
589
610
|
parts.push(`${this.#platform.quoteIdentifier(mappedKey)} ${replacement} ${val}`);
|
|
590
611
|
}
|
|
591
612
|
}
|
|
592
613
|
return { sql: parts.join(' and '), params };
|
|
593
614
|
}
|
|
594
|
-
getValueReplacement(fields, value, params, key, prop) {
|
|
615
|
+
getValueReplacement(fields, value, params, key, prop, rowValues = false) {
|
|
595
616
|
if (Array.isArray(value)) {
|
|
596
|
-
if (fields.length > 1) {
|
|
617
|
+
if (fields.length > 1 || rowValues) {
|
|
597
618
|
const tmp = [];
|
|
598
619
|
for (const field of value) {
|
|
599
620
|
tmp.push(`(${field.map(() => '?').join(', ')})`);
|
|
@@ -742,7 +763,7 @@ export class QueryBuilderHelper {
|
|
|
742
763
|
}
|
|
743
764
|
updateVersionProperty(qb, data) {
|
|
744
765
|
const meta = this.#metadata.find(this.#entityName);
|
|
745
|
-
if (!meta?.
|
|
766
|
+
if (!meta?.ownsVersionProperty() || meta.versionProperty in data) {
|
|
746
767
|
return;
|
|
747
768
|
}
|
|
748
769
|
const versionProperty = meta.properties[meta.versionProperty];
|
|
@@ -963,13 +963,13 @@ export class SchemaComparator {
|
|
|
963
963
|
}
|
|
964
964
|
diffTrigger(from, to) {
|
|
965
965
|
// Raw DDL expression cannot be meaningfully compared to introspected
|
|
966
|
-
// trigger metadata, so
|
|
967
|
-
if (to.expression) {
|
|
966
|
+
// trigger metadata, so it needs special handling when either side uses it.
|
|
967
|
+
if (from.expression || to.expression) {
|
|
968
968
|
// Both sides have expression — compare the raw DDL directly
|
|
969
|
-
if (from.expression) {
|
|
969
|
+
if (from.expression && to.expression) {
|
|
970
970
|
return this.diffExpression(from.expression, to.expression);
|
|
971
971
|
}
|
|
972
|
-
// Only
|
|
972
|
+
// Only one side has expression — the raw DDL cannot be compared to
|
|
973
973
|
// introspected metadata. Changes to the expression value won't be detected;
|
|
974
974
|
// drop and recreate the trigger manually to apply expression changes.
|
|
975
975
|
return false;
|
package/typings.d.ts
CHANGED
|
@@ -277,6 +277,8 @@ export interface IQueryBuilder<T> {
|
|
|
277
277
|
with(name: string, query: AnyQueryBuilder | NativeQueryBuilder | RawQueryFragment, options?: CteOptions): this;
|
|
278
278
|
withRecursive(name: string, query: AnyQueryBuilder | NativeQueryBuilder | RawQueryFragment, options?: CteOptions): this;
|
|
279
279
|
scheduleFilterCheck(path: string): void;
|
|
280
|
+
/** @internal */
|
|
281
|
+
ensureTPTJoins(): void;
|
|
280
282
|
withSchema(schema: string): this;
|
|
281
283
|
}
|
|
282
284
|
export interface ICriteriaNodeProcessOptions {
|