@mikro-orm/sql 7.2.2-dev.3 → 7.2.2-dev.30
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 +4 -0
- package/AbstractSqlDriver.js +56 -30
- package/SqlEntityManager.js +2 -1
- package/dialects/mssql/MsSqlNativeQueryBuilder.js +2 -2
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +5 -1
- package/package.json +2 -2
- package/plugin/transformer.d.ts +2 -1
- package/plugin/transformer.js +39 -13
- package/query/NativeQueryBuilder.d.ts +1 -1
- package/query/NativeQueryBuilder.js +3 -3
- package/query/ObjectCriteriaNode.js +19 -6
- package/query/QueryBuilder.js +27 -19
- package/query/QueryBuilderHelper.js +8 -14
package/AbstractSqlDriver.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
18
18
|
evaluateFormula(formula: (...args: any[]) => string | Raw, columns: any, table: FormulaTable): string;
|
|
19
19
|
/** For TPT entities, returns ownProps (columns in this table); otherwise returns all props. */
|
|
20
20
|
protected getTableProps<T extends object>(meta: EntityMetadata<T>): EntityProperty<T>[];
|
|
21
|
+
/** Columns reloaded by UPDATE: raw values, PKs, generated/version columns and explicit `returning` hints. */
|
|
22
|
+
protected getUpdateReturningProperties<T extends object>(meta: EntityMetadata<T>, data: EntityDictionary<T>[]): EntityProperty<T>[];
|
|
23
|
+
/** Column expression for the `returning` clause of batch updates, applying `convertToJSValueSQL` of custom types. */
|
|
24
|
+
protected getReturningFieldSQL(prop: EntityProperty, fieldName: string, index: number): string;
|
|
21
25
|
/** Creates a FormulaTable object for use in formula callbacks. */
|
|
22
26
|
private createFormulaTable;
|
|
23
27
|
private validateSqlOptions;
|
package/AbstractSqlDriver.js
CHANGED
|
@@ -49,6 +49,32 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
49
49
|
getTableProps(meta) {
|
|
50
50
|
return meta.inheritanceType === 'tpt' && meta.ownProps ? meta.ownProps : meta.props;
|
|
51
51
|
}
|
|
52
|
+
/** Columns reloaded by UPDATE: raw values, PKs, generated/version columns and explicit `returning` hints. */
|
|
53
|
+
getUpdateReturningProperties(meta, data) {
|
|
54
|
+
const returning = new Set();
|
|
55
|
+
for (const row of data) {
|
|
56
|
+
for (const k of Utils.keys(row)) {
|
|
57
|
+
if (isRaw(row[k])) {
|
|
58
|
+
returning.add(meta.properties[k] ?? meta.root.properties[k]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
meta.getPrimaryProps().forEach(prop => returning.add(prop));
|
|
63
|
+
// a mixed STI batch shares one table, so the child columns live on the root
|
|
64
|
+
this.getTableProps(meta.root.discriminatorColumn ? meta.root : meta)
|
|
65
|
+
.filter(prop => prop.generated || prop.version || prop.returning)
|
|
66
|
+
.forEach(prop => returning.add(prop));
|
|
67
|
+
return [...returning];
|
|
68
|
+
}
|
|
69
|
+
/** Column expression for the `returning` clause of batch updates, applying `convertToJSValueSQL` of custom types. */
|
|
70
|
+
getReturningFieldSQL(prop, fieldName, index) {
|
|
71
|
+
const quoted = this.platform.quoteIdentifier(fieldName);
|
|
72
|
+
const customType = prop.customTypes?.[index] ?? prop.customType;
|
|
73
|
+
if (prop.hasConvertToJSValueSQL && customType?.convertToJSValueSQL) {
|
|
74
|
+
return `${customType.convertToJSValueSQL(quoted, this.platform)} as ${quoted}`;
|
|
75
|
+
}
|
|
76
|
+
return quoted;
|
|
77
|
+
}
|
|
52
78
|
/** Creates a FormulaTable object for use in formula callbacks. */
|
|
53
79
|
createFormulaTable(alias, meta, schema) {
|
|
54
80
|
const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
|
|
@@ -824,9 +850,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
824
850
|
props.forEach(prop => {
|
|
825
851
|
// For STI with conflicting fieldNames, use discriminator to determine which field gets value
|
|
826
852
|
if (prop.stiFieldNames && prop.stiFieldNameMap && meta.discriminatorColumn) {
|
|
827
|
-
|
|
853
|
+
// the map misses subtypes whose props were not initialized yet during discovery (root column owner, grandchildren)
|
|
854
|
+
const discriminator = row[meta.discriminatorColumn];
|
|
855
|
+
const childClass = meta.discriminatorMap?.[discriminator];
|
|
856
|
+
const activeField = prop.stiFieldNameMap[discriminator] ??
|
|
857
|
+
(childClass && this.metadata.find(childClass)?.properties[prop.name]?.fieldNames[0]);
|
|
828
858
|
for (const field of prop.stiFieldNames) {
|
|
829
|
-
params.push(field === activeField ? row[prop.name] : null);
|
|
859
|
+
params.push(field === activeField ? (row[prop.name] ?? null) : null);
|
|
830
860
|
keys.push('?');
|
|
831
861
|
}
|
|
832
862
|
return;
|
|
@@ -946,7 +976,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
946
976
|
/* v8 ignore next */
|
|
947
977
|
const uniqueFields = options.onConflictFields ??
|
|
948
978
|
(Utils.isPlainObject(where) ? Utils.keys(where) : meta.primaryKeys);
|
|
949
|
-
const returning = getOnConflictReturningFields(meta, data, uniqueFields, options);
|
|
979
|
+
const returning = getOnConflictReturningFields(meta, data, uniqueFields, options, this.platform);
|
|
950
980
|
qb.insert(data)
|
|
951
981
|
.onConflict(uniqueFields)
|
|
952
982
|
.returning(returning);
|
|
@@ -963,11 +993,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
963
993
|
}
|
|
964
994
|
else {
|
|
965
995
|
qb.update(data).where(where);
|
|
966
|
-
// reload generated columns and version fields
|
|
967
996
|
const returning = [];
|
|
968
997
|
this.getTableProps(meta)
|
|
969
|
-
.filter(prop => (prop.generated && !prop.primary) || prop.version)
|
|
970
|
-
.forEach(prop => returning.push(prop.
|
|
998
|
+
.filter(prop => (prop.generated && !prop.primary) || prop.version || prop.returning || isRaw(data[prop.name]))
|
|
999
|
+
.forEach(prop => returning.push(...prop.fieldNames));
|
|
971
1000
|
qb.returning(returning);
|
|
972
1001
|
}
|
|
973
1002
|
res = await this.rethrow(qb.execute('run', false));
|
|
@@ -1003,7 +1032,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1003
1032
|
: meta.primaryKeys);
|
|
1004
1033
|
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
1005
1034
|
qb.setAbortOptions(pickAbortOptions(options));
|
|
1006
|
-
let returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
|
|
1035
|
+
let returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options, this.platform);
|
|
1007
1036
|
if (meta.inheritanceType === 'tpt') {
|
|
1008
1037
|
// each TPT table only carries its own columns, the entity is reloaded instead of mapping the returned rows
|
|
1009
1038
|
const own = (key) => meta.primaryKeys.includes(key) ||
|
|
@@ -1032,20 +1061,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1032
1061
|
const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
|
|
1033
1062
|
const keys = new Set();
|
|
1034
1063
|
const fields = new Set();
|
|
1035
|
-
const returning =
|
|
1064
|
+
const returning = this.getUpdateReturningProperties(meta, data);
|
|
1036
1065
|
for (const row of data) {
|
|
1037
1066
|
for (const k of Utils.keys(row)) {
|
|
1038
1067
|
keys.add(k);
|
|
1039
|
-
if (isRaw(row[k])) {
|
|
1040
|
-
returning.add(k);
|
|
1041
|
-
}
|
|
1042
1068
|
}
|
|
1043
1069
|
}
|
|
1044
|
-
// reload generated columns and version fields
|
|
1045
|
-
meta.getPrimaryProps().forEach(prop => returning.add(prop.name));
|
|
1046
|
-
this.getTableProps(meta)
|
|
1047
|
-
.filter(prop => prop.generated || prop.version)
|
|
1048
|
-
.forEach(prop => returning.add(prop.name));
|
|
1049
1070
|
const pkCond = Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames))
|
|
1050
1071
|
.map(pk => `${this.platform.quoteIdentifier(pk)} = ?`)
|
|
1051
1072
|
.join(' and ');
|
|
@@ -1115,7 +1136,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1115
1136
|
}
|
|
1116
1137
|
sql += `, `;
|
|
1117
1138
|
}
|
|
1118
|
-
sql = sql.substring(0, sql.length - 2)
|
|
1139
|
+
sql = sql.substring(0, sql.length - 2);
|
|
1140
|
+
if (this.platform.usesOutputStatement() && returning.length > 0) {
|
|
1141
|
+
const fields = returning.flatMap(prop => prop.fieldNames);
|
|
1142
|
+
sql += ` output ${fields.map(field => 'inserted.' + this.platform.quoteIdentifier(field)).join(', ')}`;
|
|
1143
|
+
}
|
|
1144
|
+
sql += ' where ';
|
|
1119
1145
|
const pkProps = meta.primaryKeys.concat(...meta.getOwnConcurrencyCheckKeys());
|
|
1120
1146
|
const pks = Utils.flatten(pkProps.map(pk => meta.properties[pk].fieldNames));
|
|
1121
1147
|
const useTupleIn = pks.length <= 1 || this.platform.allowsComparingTuples();
|
|
@@ -1151,13 +1177,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1151
1177
|
else {
|
|
1152
1178
|
sql += conds.join(' or ');
|
|
1153
1179
|
}
|
|
1154
|
-
if (this.platform.usesReturningStatement() && returning.
|
|
1155
|
-
const returningFields =
|
|
1156
|
-
|
|
1157
|
-
sql +=
|
|
1158
|
-
returningFields.length > 0
|
|
1159
|
-
? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}`
|
|
1160
|
-
: '';
|
|
1180
|
+
if (this.platform.usesReturningStatement() && returning.length > 0) {
|
|
1181
|
+
const returningFields = returning.flatMap(prop => prop.fieldNames.map((field, idx) => this.getReturningFieldSQL(prop, field, idx)));
|
|
1182
|
+
sql += ` returning ${returningFields.join(', ')}`;
|
|
1161
1183
|
}
|
|
1162
1184
|
if (transform) {
|
|
1163
1185
|
sql = transform(sql, params);
|
|
@@ -1791,7 +1813,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1791
1813
|
if (!this.platform.shouldHaveColumn(prop, populate, exclude)) {
|
|
1792
1814
|
return false;
|
|
1793
1815
|
}
|
|
1794
|
-
if (!fields ||
|
|
1816
|
+
if (!fields ||
|
|
1817
|
+
fields.includes('*') ||
|
|
1818
|
+
prop.primary ||
|
|
1819
|
+
meta.root.discriminatorColumn === prop.name ||
|
|
1820
|
+
meta.root.targetKeys?.includes(prop.name)) {
|
|
1795
1821
|
return true;
|
|
1796
1822
|
}
|
|
1797
1823
|
return fields.some(f => f === prop.name || f.toString().startsWith(prop.name + '.'));
|
|
@@ -1847,11 +1873,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1847
1873
|
const tptMeta = this.metadata.get(targetMeta.class);
|
|
1848
1874
|
this.addTPTPolymorphicJoinsForRelation(qb, tptMeta, tableAlias, fields);
|
|
1849
1875
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
}
|
|
1854
|
-
else {
|
|
1876
|
+
// target filters apply per target, both for filter :ref hints and populated relations
|
|
1877
|
+
qb.scheduleFilterCheck(targetPath);
|
|
1878
|
+
if (!ref) {
|
|
1855
1879
|
// Select fields from each target table
|
|
1856
1880
|
fields.push(...this.getFieldsForJoinedLoad(qb, targetMeta, {
|
|
1857
1881
|
...options,
|
|
@@ -2451,6 +2475,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
2451
2475
|
}
|
|
2452
2476
|
if (!options.fields.includes('*') && !options.fields.includes(`${qb.alias}.*`)) {
|
|
2453
2477
|
ret.unshift(...meta.primaryKeys.filter(pk => !options.fields.includes(pk)));
|
|
2478
|
+
// `targetKey` values identify the entity like its PK, references resolve it by them
|
|
2479
|
+
ret.push(...(meta.root.targetKeys ?? []).filter(key => meta.properties[key] && !options.fields.includes(key)));
|
|
2454
2480
|
}
|
|
2455
2481
|
if (meta.root.inheritanceType === 'sti' &&
|
|
2456
2482
|
!options.fields.includes(`${qb.alias}.${meta.root.discriminatorColumn}`)) {
|
package/SqlEntityManager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityManager, raw, Utils, } from '@mikro-orm/core';
|
|
1
|
+
import { EntityManager, QueryFlag, raw, Utils, } from '@mikro-orm/core';
|
|
2
2
|
import { MikroKyselyPlugin } from './plugin/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* @inheritDoc
|
|
@@ -87,6 +87,7 @@ export class SqlEntityManager extends EntityManager {
|
|
|
87
87
|
// match `em.count()` semantics: an active transaction always wins over the requested connection type
|
|
88
88
|
const connectionType = em.getTransactionContext() ? 'write' : options.connectionType;
|
|
89
89
|
const qb = em.createQueryBuilder(meta.class, undefined, connectionType);
|
|
90
|
+
qb.unsetFlag(QueryFlag.CONVERT_CUSTOM_TYPES);
|
|
90
91
|
qb
|
|
91
92
|
.select([...fields, raw('count(*) as cnt')])
|
|
92
93
|
.where(where)
|
|
@@ -97,12 +97,12 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
|
|
|
97
97
|
compileUpsert() {
|
|
98
98
|
const clause = this.options.onConflict;
|
|
99
99
|
const dataAsArray = Utils.asArray(this.options.data);
|
|
100
|
-
const keys = Object.keys(
|
|
100
|
+
const keys = [...new Set(dataAsArray.flatMap(data => Object.keys(data)))];
|
|
101
101
|
const values = keys.map(() => '?');
|
|
102
102
|
const parts = [];
|
|
103
103
|
for (const data of dataAsArray) {
|
|
104
104
|
for (const key of keys) {
|
|
105
|
-
this.params.push(data[key]);
|
|
105
|
+
this.params.push(data[key] ?? null);
|
|
106
106
|
}
|
|
107
107
|
parts.push(`(${values.join(', ')})`);
|
|
108
108
|
}
|
|
@@ -419,7 +419,11 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
419
419
|
if ((col.udt_name === 'geometry' || col.udt_name === 'geography') && col.format_type) {
|
|
420
420
|
type = col.format_type;
|
|
421
421
|
}
|
|
422
|
-
|
|
422
|
+
// pgvector stores the dimension in atttypmod for all three of its vector types
|
|
423
|
+
if (['vector', 'halfvec', 'sparsevec'].includes(type) &&
|
|
424
|
+
col.length == null &&
|
|
425
|
+
col.custom_length != null &&
|
|
426
|
+
col.custom_length !== -1) {
|
|
423
427
|
col.length = col.custom_length;
|
|
424
428
|
}
|
|
425
429
|
if (col.length != null && !type.endsWith(`(${col.length})`) && !['text', 'date'].includes(type)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.2.2-dev.
|
|
3
|
+
"version": "7.2.2-dev.30",
|
|
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.2.1"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.2.2-dev.
|
|
56
|
+
"@mikro-orm/core": "7.2.2-dev.30"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
package/plugin/transformer.d.ts
CHANGED
|
@@ -92,6 +92,7 @@ export declare class MikroTransformer extends OperationNodeTransformer {
|
|
|
92
92
|
* This is called for SELECT queries when columnNamingStrategy is 'property'
|
|
93
93
|
*/
|
|
94
94
|
transformResult(rows: Record<string, any>[] | undefined, entityMap: Map<string, EntityMetadata>): Record<string, any>[] | undefined;
|
|
95
|
+
private getResultMaps;
|
|
95
96
|
buildGlobalFieldMap(entityMap: Map<string, EntityMetadata>): Record<string, EntityProperty>;
|
|
96
97
|
buildGlobalRelationFieldMap(entityMap: Map<string, EntityMetadata>): Record<string, string>;
|
|
97
98
|
/**
|
|
@@ -108,6 +109,6 @@ export declare class MikroTransformer extends OperationNodeTransformer {
|
|
|
108
109
|
/**
|
|
109
110
|
* Transform a single row by mapping column names to property names
|
|
110
111
|
*/
|
|
111
|
-
transformRow(row: Record<string, any>,
|
|
112
|
+
transformRow(row: Record<string, any>, fieldEntries: [string, EntityProperty][], relationEntries: [string, string][]): Record<string, any>;
|
|
112
113
|
prepareOutputValue(prop: EntityProperty | undefined, value: unknown): unknown;
|
|
113
114
|
}
|
package/plugin/transformer.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { ReferenceKind, isRaw, } from '@mikro-orm/core';
|
|
2
2
|
import { AliasNode, ColumnNode, ColumnUpdateNode, IdentifierNode, OperationNodeTransformer, PrimitiveValueListNode, RawNode, ReferenceNode, SchemableIdentifierNode, SelectAllNode, SelectionNode, TableNode, ValueListNode, ValueNode, ValuesNode, } from 'kysely';
|
|
3
|
+
/** Past this many query shapes the cache of one metadata storage starts over, so generated aliases cannot grow it. */
|
|
4
|
+
const RESULT_MAPS_LIMIT = 1000;
|
|
5
|
+
/**
|
|
6
|
+
* Result maps per query shape (the aliases and entities of its entity map). They are derived from metadata alone, so
|
|
7
|
+
* they are built once and shared by every transformer, as `getKysely()` creates a new one per call.
|
|
8
|
+
*/
|
|
9
|
+
const resultMapsCache = new WeakMap();
|
|
3
10
|
const EXPANDABLE_KINDS = new Set([
|
|
4
11
|
ReferenceKind.SCALAR,
|
|
5
12
|
ReferenceKind.EMBEDDED,
|
|
@@ -884,12 +891,7 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
884
891
|
if (byEntity) {
|
|
885
892
|
return byEntity;
|
|
886
893
|
}
|
|
887
|
-
|
|
888
|
-
const byTable = allMetadata.find(m => m.tableName === name);
|
|
889
|
-
if (byTable) {
|
|
890
|
-
return byTable;
|
|
891
|
-
}
|
|
892
|
-
return undefined;
|
|
894
|
+
return this.#metadata.getByTableName(name);
|
|
893
895
|
}
|
|
894
896
|
/**
|
|
895
897
|
* Transform result rows by mapping database column names to property names
|
|
@@ -906,11 +908,35 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
906
908
|
if (entityMap.size === 0) {
|
|
907
909
|
return rows;
|
|
908
910
|
}
|
|
909
|
-
//
|
|
910
|
-
const
|
|
911
|
-
const relationFieldMap = this.buildGlobalRelationFieldMap(entityMap);
|
|
911
|
+
// The global mapping from database field names to property objects, built once per query shape
|
|
912
|
+
const { fields, relations } = this.getResultMaps(entityMap);
|
|
912
913
|
// Transform each row
|
|
913
|
-
return rows.map(row => this.transformRow(row,
|
|
914
|
+
return rows.map(row => this.transformRow(row, fields, relations));
|
|
915
|
+
}
|
|
916
|
+
getResultMaps(entityMap) {
|
|
917
|
+
let byShape = resultMapsCache.get(this.#metadata);
|
|
918
|
+
if (!byShape) {
|
|
919
|
+
byShape = new Map();
|
|
920
|
+
resultMapsCache.set(this.#metadata, byShape);
|
|
921
|
+
}
|
|
922
|
+
let shape = '';
|
|
923
|
+
for (const [alias, meta] of entityMap) {
|
|
924
|
+
shape += `${alias}\0${meta._id}\0`;
|
|
925
|
+
}
|
|
926
|
+
const cached = byShape.get(shape);
|
|
927
|
+
if (cached && Array.from(entityMap.values()).every((meta, i) => meta.props === cached.props[i])) {
|
|
928
|
+
return cached;
|
|
929
|
+
}
|
|
930
|
+
if (byShape.size >= RESULT_MAPS_LIMIT) {
|
|
931
|
+
byShape.clear();
|
|
932
|
+
}
|
|
933
|
+
const maps = {
|
|
934
|
+
props: Array.from(entityMap.values(), meta => meta.props),
|
|
935
|
+
fields: Object.entries(this.buildGlobalFieldMap(entityMap)),
|
|
936
|
+
relations: Object.entries(this.buildGlobalRelationFieldMap(entityMap)),
|
|
937
|
+
};
|
|
938
|
+
byShape.set(shape, maps);
|
|
939
|
+
return maps;
|
|
914
940
|
}
|
|
915
941
|
buildGlobalFieldMap(entityMap) {
|
|
916
942
|
const map = {};
|
|
@@ -986,10 +1012,10 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
986
1012
|
/**
|
|
987
1013
|
* Transform a single row by mapping column names to property names
|
|
988
1014
|
*/
|
|
989
|
-
transformRow(row,
|
|
1015
|
+
transformRow(row, fieldEntries, relationEntries) {
|
|
990
1016
|
const transformed = { ...row };
|
|
991
1017
|
// First pass: map regular fields from fieldName to propertyName and convert values
|
|
992
|
-
for (const [fieldName, prop] of
|
|
1018
|
+
for (const [fieldName, prop] of fieldEntries) {
|
|
993
1019
|
if (!(fieldName in transformed)) {
|
|
994
1020
|
continue;
|
|
995
1021
|
}
|
|
@@ -1011,7 +1037,7 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
1011
1037
|
// Second pass: handle relation fields
|
|
1012
1038
|
// Only run if columnNamingStrategy is 'property', as we don't want to rename FKs otherwise
|
|
1013
1039
|
if (this.#options.columnNamingStrategy === 'property') {
|
|
1014
|
-
for (const [fieldName, relationPropertyName] of
|
|
1040
|
+
for (const [fieldName, relationPropertyName] of relationEntries) {
|
|
1015
1041
|
if (fieldName in transformed && !(relationPropertyName in transformed)) {
|
|
1016
1042
|
// Move the foreign key value to the relation property name
|
|
1017
1043
|
transformed[relationPropertyName] = transformed[fieldName];
|
|
@@ -127,7 +127,7 @@ export declare class NativeQueryBuilder implements Subquery {
|
|
|
127
127
|
as(alias: string): this;
|
|
128
128
|
toRaw(): RawQueryFragment;
|
|
129
129
|
protected compileSelect(): void;
|
|
130
|
-
/** Whether this COUNT query needs a subquery wrapper
|
|
130
|
+
/** Whether this COUNT query needs a subquery wrapper: multi-column distinct, or `distinct on` (keeps the null group). */
|
|
131
131
|
protected needsCountSubquery(): boolean;
|
|
132
132
|
protected getFields(countSubquery?: boolean): string;
|
|
133
133
|
protected compileInsert(): void;
|
|
@@ -330,12 +330,12 @@ export class NativeQueryBuilder {
|
|
|
330
330
|
this.parts.push(`)${asKeyword}${this.quote('dcnt')}`);
|
|
331
331
|
}
|
|
332
332
|
}
|
|
333
|
-
/** Whether this COUNT query needs a subquery wrapper
|
|
333
|
+
/** Whether this COUNT query needs a subquery wrapper: multi-column distinct, or `distinct on` (keeps the null group). */
|
|
334
334
|
needsCountSubquery() {
|
|
335
335
|
return (this.type === QueryType.COUNT &&
|
|
336
336
|
!!this.options.distinct &&
|
|
337
|
-
this.options.select.length > 1 &&
|
|
338
|
-
|
|
337
|
+
((this.options.select.length > 1 && !this.platform.supportsMultiColumnCountDistinct()) ||
|
|
338
|
+
!!this.options.distinctOn));
|
|
339
339
|
}
|
|
340
340
|
getFields(countSubquery) {
|
|
341
341
|
if (!this.options.select || this.options.select.length === 0) {
|
|
@@ -186,8 +186,9 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
186
186
|
const operator = Utils.isObject(payload) &&
|
|
187
187
|
Utils.getObjectQueryKeys(payload).every(k => {
|
|
188
188
|
if (k === '$not' && Utils.isPlainObject(payload[k])) {
|
|
189
|
-
// $not wrapping non-operator conditions (entity props) should be inlined
|
|
190
|
-
|
|
189
|
+
// $not wrapping non-operator conditions (entity props) or an empty one should be inlined
|
|
190
|
+
const inner = Utils.getObjectQueryKeys(payload[k]);
|
|
191
|
+
return inner.length > 0 && inner.every(ik => Utils.isOperator(ik, false));
|
|
191
192
|
}
|
|
192
193
|
return Utils.isOperator(k, false);
|
|
193
194
|
});
|
|
@@ -218,9 +219,12 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
218
219
|
}
|
|
219
220
|
else if (k === '$not' &&
|
|
220
221
|
Utils.isPlainObject(payload[k]) &&
|
|
221
|
-
Utils.
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
(!Utils.hasObjectKeys(payload[k]) ||
|
|
223
|
+
Utils.getObjectQueryKeys(payload[k]).some(ik => !Utils.isOperator(ik, false)))) {
|
|
224
|
+
// $not wraps entity conditions (from auto-join) or an empty one, inline at current level with the same key mapping
|
|
225
|
+
const inner = {};
|
|
226
|
+
this.inlineChildPayload(inner, payload[k], field, alias, childAlias);
|
|
227
|
+
this.inlineCondition(k, o, inner);
|
|
224
228
|
}
|
|
225
229
|
else if (Utils.isOperator(k, false)) {
|
|
226
230
|
const tmp = payload[k];
|
|
@@ -291,6 +295,13 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
291
295
|
});
|
|
292
296
|
const primaryKeys = knownKey &&
|
|
293
297
|
keys.every(key => {
|
|
298
|
+
if (key === '$not') {
|
|
299
|
+
// e.g. `{ author: { $not: { id: 1 } } }` compares the FK column, relation PKs still need the join
|
|
300
|
+
const childPayload = this.payload[key].payload;
|
|
301
|
+
return (Utils.isPlainObject(childPayload) &&
|
|
302
|
+
Utils.getObjectQueryKeys(childPayload).every(k => meta.primaryKeys.includes(k) &&
|
|
303
|
+
meta.properties[k].kind === ReferenceKind.SCALAR));
|
|
304
|
+
}
|
|
294
305
|
if (typeof key !== 'string' || !meta.primaryKeys.includes(key)) {
|
|
295
306
|
return false;
|
|
296
307
|
}
|
|
@@ -320,7 +331,9 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
320
331
|
else {
|
|
321
332
|
const prev = qb.state.fields?.slice();
|
|
322
333
|
const toOneProperty = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind);
|
|
323
|
-
|
|
334
|
+
// ordering must not drop rows whose polymorphic relation points to another target
|
|
335
|
+
const orderByPolymorphic = this.prop.polymorphic && options?.type === 'orderBy';
|
|
336
|
+
const joinType = toOneProperty && !this.prop.nullable && !orderByPolymorphic ? JoinType.innerJoin : JoinType.leftJoin;
|
|
324
337
|
qb[method](field, nestedAlias, undefined, joinType, path);
|
|
325
338
|
if (!qb.hasFlag(QueryFlag.INFER_POPULATE)) {
|
|
326
339
|
qb.state.fields = prev;
|
package/query/QueryBuilder.js
CHANGED
|
@@ -257,7 +257,11 @@ export class QueryBuilder {
|
|
|
257
257
|
if (field) {
|
|
258
258
|
this.#state.fields = Utils.asArray(field);
|
|
259
259
|
}
|
|
260
|
-
else if (
|
|
260
|
+
else if (this.#state.distinctOn) {
|
|
261
|
+
this.#state.fields = this.#state.distinctOn;
|
|
262
|
+
distinct = true;
|
|
263
|
+
}
|
|
264
|
+
else if (distinct || this.#state.flags.has(QueryFlag.DISTINCT) || this.hasToManyJoins()) {
|
|
261
265
|
this.#state.fields = this.mainAlias.meta.primaryKeys;
|
|
262
266
|
}
|
|
263
267
|
else {
|
|
@@ -916,18 +920,13 @@ export class QueryBuilder {
|
|
|
916
920
|
return;
|
|
917
921
|
}
|
|
918
922
|
if (this.type === QueryType.UPDATE && data) {
|
|
919
|
-
const returningProps = meta.hydrateProps
|
|
923
|
+
const returningProps = meta.hydrateProps
|
|
924
|
+
.filter(prop => prop.returning || (prop.fieldNames && isRaw(data[prop.fieldNames[0]])))
|
|
925
|
+
// a TPT table can only return its own columns
|
|
926
|
+
.filter(prop => meta.inheritanceType !== 'tpt' || prop.primary || meta.ownProps.some(p => p.name === prop.name));
|
|
920
927
|
if (returningProps.length > 0) {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
|
|
924
|
-
const sql = prop.customType.convertToJSValueSQL(aliased, this.platform) +
|
|
925
|
-
' as ' +
|
|
926
|
-
this.platform.quoteIdentifier(prop.fieldNames[0]);
|
|
927
|
-
return [raw(sql)];
|
|
928
|
-
}
|
|
929
|
-
return prop.fieldNames;
|
|
930
|
-
}));
|
|
928
|
+
const fields = returningProps.flatMap(prop => prop.fieldNames);
|
|
929
|
+
qb.returning(fields.map(field => this.helper.mapper(field, this.type)));
|
|
931
930
|
}
|
|
932
931
|
}
|
|
933
932
|
}
|
|
@@ -1033,9 +1032,11 @@ export class QueryBuilder {
|
|
|
1033
1032
|
* @internal
|
|
1034
1033
|
*/
|
|
1035
1034
|
addPolymorphicJoin(prop, targetMeta, ownerAlias, alias, type, path, schema) {
|
|
1036
|
-
// Override referencedColumnNames to use the specific target's PK columns
|
|
1035
|
+
// Override referencedColumnNames to use the specific target's PK (or `targetKey`) columns
|
|
1037
1036
|
// (polymorphic targets may have different PK column names, e.g. org_id vs user_id)
|
|
1038
|
-
const referencedColumnNames =
|
|
1037
|
+
const referencedColumnNames = prop.targetKey
|
|
1038
|
+
? targetMeta.properties[prop.targetKey].fieldNames
|
|
1039
|
+
: targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
|
|
1039
1040
|
const targetProp = { ...prop, targetMeta, referencedColumnNames };
|
|
1040
1041
|
const aliasedName = `${ownerAlias}.${prop.name}[${targetMeta.className}]#${alias}`;
|
|
1041
1042
|
this.#state.joins[aliasedName] = this.helper.joinManyToOneReference(targetProp, ownerAlias, alias, type, {}, schema);
|
|
@@ -1571,7 +1572,7 @@ export class QueryBuilder {
|
|
|
1571
1572
|
return;
|
|
1572
1573
|
}
|
|
1573
1574
|
if (prop?.embedded || (prop?.kind === ReferenceKind.EMBEDDED && prop.object)) {
|
|
1574
|
-
const name = prop.embeddedPath?.join('.') ?? prop.fieldNames[0];
|
|
1575
|
+
const name = prop.formula ? prop.name : (prop.embeddedPath?.join('.') ?? prop.fieldNames[0]);
|
|
1575
1576
|
const aliased = this.#state.aliases[a] ? `${a}.${name}` : name;
|
|
1576
1577
|
ret.push(getFieldName(aliased, customAlias));
|
|
1577
1578
|
return;
|
|
@@ -1586,11 +1587,11 @@ export class QueryBuilder {
|
|
|
1586
1587
|
}
|
|
1587
1588
|
const nest = (prop) => {
|
|
1588
1589
|
for (const childProp of Object.values(prop.embeddedProps)) {
|
|
1589
|
-
if (childProp.persist === false) {
|
|
1590
|
+
if (childProp.persist === false && !childProp.formula) {
|
|
1590
1591
|
continue;
|
|
1591
1592
|
}
|
|
1592
1593
|
if (childProp.fieldNames && (childProp.kind !== ReferenceKind.EMBEDDED || childProp.object)) {
|
|
1593
|
-
const name = childProp.fieldNames[0];
|
|
1594
|
+
const name = childProp.formula ? childProp.name : childProp.fieldNames[0];
|
|
1594
1595
|
ret.push(getFieldName(this.#state.aliases[a] ? `${a}.${name}` : name));
|
|
1595
1596
|
}
|
|
1596
1597
|
else {
|
|
@@ -1671,7 +1672,8 @@ export class QueryBuilder {
|
|
|
1671
1672
|
return `${currentAlias}.${embeddedProp.fieldNames[0]}`;
|
|
1672
1673
|
}
|
|
1673
1674
|
}
|
|
1674
|
-
|
|
1675
|
+
const name = embeddedProp?.formula ? embeddedProp.name : (embeddedProp?.fieldNames?.[0] ?? propName);
|
|
1676
|
+
return `${currentAlias}.${name}`;
|
|
1675
1677
|
}
|
|
1676
1678
|
// Handle relations - auto-join if not the last part
|
|
1677
1679
|
if (prop.kind === ReferenceKind.MANY_TO_ONE ||
|
|
@@ -1764,6 +1766,9 @@ export class QueryBuilder {
|
|
|
1764
1766
|
case QueryType.COUNT: {
|
|
1765
1767
|
const fields = this.#state.fields.map(f => this.helper.mapper(f, this.type, undefined, undefined, schema));
|
|
1766
1768
|
qb.count(fields, this.#state.flags.has(QueryFlag.DISTINCT));
|
|
1769
|
+
if (this.#state.distinctOn && this.#state.flags.has(QueryFlag.DISTINCT)) {
|
|
1770
|
+
qb.distinctOn(this.prepareFields(this.#state.distinctOn, 'where', schema));
|
|
1771
|
+
}
|
|
1767
1772
|
this.helper.processJoins(qb, this.#state.joins, joinSchema, schemaOverride);
|
|
1768
1773
|
break;
|
|
1769
1774
|
}
|
|
@@ -2327,7 +2332,10 @@ export class QueryBuilder {
|
|
|
2327
2332
|
const fieldName = this.helper.mapper(field, this.type, undefined, null);
|
|
2328
2333
|
// virtual fields (e.g. `qb.as(...)`) have no column to reference inside `min()`; inline their
|
|
2329
2334
|
// expression instead, as a select alias is not resolvable there on some dialects (e.g. PostgreSQL)
|
|
2330
|
-
const virtual = !prop
|
|
2335
|
+
const virtual = (!prop || prop.persist === false) &&
|
|
2336
|
+
!prop?.formula &&
|
|
2337
|
+
!prop?.hasConvertToJSValueSQL &&
|
|
2338
|
+
!pks.includes(fieldName)
|
|
2331
2339
|
? this.resolveVirtualField(f, fieldName)
|
|
2332
2340
|
: undefined;
|
|
2333
2341
|
const expr = virtual ? virtual.expr : this.platform.quoteIdentifier(fieldName);
|
|
@@ -112,7 +112,7 @@ export class QueryBuilderHelper {
|
|
|
112
112
|
const aliasPrefix = isTableNameAliasRequired ? resolvedAlias + '.' : '';
|
|
113
113
|
const fkIdx2 = prop?.fieldNames.findIndex(name => name === f) ?? -1;
|
|
114
114
|
const fkIdx = fkIdx2 === -1 ? 0 : fkIdx2;
|
|
115
|
-
if (a === prop?.embedded?.[0]) {
|
|
115
|
+
if (a === prop?.embedded?.[0] && !prop.formula) {
|
|
116
116
|
return aliasPrefix + prop.fieldNames[fkIdx];
|
|
117
117
|
}
|
|
118
118
|
const noPrefix = prop?.persist === false;
|
|
@@ -120,29 +120,23 @@ export class QueryBuilderHelper {
|
|
|
120
120
|
return raw(this.prefix(field, isTableNameAliasRequired));
|
|
121
121
|
}
|
|
122
122
|
if (prop?.formula) {
|
|
123
|
-
const
|
|
123
|
+
const formulaAlias = isTableAlias ? a : this.#alias;
|
|
124
|
+
const alias2 = this.#platform.quoteIdentifier(formulaAlias).toString();
|
|
124
125
|
const aliasName = alias === undefined ? prop.fieldNames[0] : alias;
|
|
125
126
|
const as = aliasName === null ? '' : ` as ${this.#platform.quoteIdentifier(aliasName)}`;
|
|
126
127
|
const meta = this.#aliasMap[a]?.meta ?? this.#metadata.get(this.#entityName);
|
|
127
128
|
const table = this.createFormulaTable(alias2, meta, schema);
|
|
128
|
-
const columns = meta.createColumnMappingObject(p => this.getTPTAliasForProperty(p.name,
|
|
129
|
+
const columns = meta.createColumnMappingObject(p => this.getTPTAliasForProperty(p.name, formulaAlias), alias2);
|
|
129
130
|
let value = this.#driver.evaluateFormula(prop.formula, columns, table);
|
|
130
131
|
if (!this.isTableNameAliasRequired(type)) {
|
|
131
132
|
value = value.replaceAll(alias2 + '.', '');
|
|
132
133
|
}
|
|
133
134
|
return raw(`${value}${as}`);
|
|
134
135
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const prefixed = this.prefix(field, isTableNameAliasRequired, true, fkIdx);
|
|
140
|
-
valueSQL = fk.customType.convertToJSValueSQL(prefixed, this.#platform);
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
const prefixed = this.prefix(field, isTableNameAliasRequired, true);
|
|
144
|
-
valueSQL = prop.customType.convertToJSValueSQL(prefixed, this.#platform);
|
|
145
|
-
}
|
|
136
|
+
const customType = prop?.customTypes?.[fkIdx] ?? prop?.customType;
|
|
137
|
+
if (prop?.hasConvertToJSValueSQL && customType?.convertToJSValueSQL && type !== QueryType.UPSERT) {
|
|
138
|
+
const prefixed = this.prefix(field, isTableNameAliasRequired, true, fkIdx);
|
|
139
|
+
const valueSQL = customType.convertToJSValueSQL(prefixed, this.#platform);
|
|
146
140
|
if (alias === null) {
|
|
147
141
|
return raw(valueSQL);
|
|
148
142
|
}
|