@mikro-orm/sql 7.0.0-rc.2 → 7.0.0
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/AbstractSqlConnection.d.ts +5 -4
- package/AbstractSqlConnection.js +20 -6
- package/AbstractSqlDriver.d.ts +19 -13
- package/AbstractSqlDriver.js +225 -47
- package/AbstractSqlPlatform.d.ts +35 -0
- package/AbstractSqlPlatform.js +51 -5
- package/PivotCollectionPersister.d.ts +2 -11
- package/PivotCollectionPersister.js +59 -59
- package/README.md +5 -4
- package/SqlEntityManager.d.ts +2 -2
- package/SqlEntityManager.js +5 -5
- package/dialects/index.d.ts +1 -0
- package/dialects/index.js +1 -0
- package/dialects/mssql/MsSqlNativeQueryBuilder.d.ts +2 -0
- package/dialects/mssql/MsSqlNativeQueryBuilder.js +8 -4
- package/dialects/mysql/BaseMySqlPlatform.d.ts +6 -0
- package/dialects/mysql/BaseMySqlPlatform.js +18 -2
- package/dialects/mysql/MySqlSchemaHelper.d.ts +1 -1
- package/dialects/mysql/MySqlSchemaHelper.js +25 -14
- package/dialects/oracledb/OracleDialect.d.ts +78 -0
- package/dialects/oracledb/OracleDialect.js +166 -0
- package/dialects/oracledb/OracleNativeQueryBuilder.d.ts +19 -0
- package/dialects/oracledb/OracleNativeQueryBuilder.js +249 -0
- package/dialects/oracledb/index.d.ts +2 -0
- package/dialects/oracledb/index.js +2 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +6 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +49 -37
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +75 -59
- package/dialects/sqlite/BaseSqliteConnection.js +2 -2
- package/dialects/sqlite/NodeSqliteDialect.js +3 -1
- package/dialects/sqlite/SqlitePlatform.d.ts +1 -0
- package/dialects/sqlite/SqlitePlatform.js +7 -1
- package/dialects/sqlite/SqliteSchemaHelper.js +23 -17
- package/index.d.ts +1 -1
- package/index.js +0 -1
- package/package.json +30 -30
- package/plugin/index.d.ts +1 -14
- package/plugin/index.js +13 -13
- package/plugin/transformer.d.ts +6 -22
- package/plugin/transformer.js +91 -82
- package/query/ArrayCriteriaNode.d.ts +1 -1
- package/query/CriteriaNode.js +28 -10
- package/query/CriteriaNodeFactory.js +20 -4
- package/query/NativeQueryBuilder.d.ts +28 -3
- package/query/NativeQueryBuilder.js +65 -3
- package/query/ObjectCriteriaNode.js +75 -31
- package/query/QueryBuilder.d.ts +199 -100
- package/query/QueryBuilder.js +544 -358
- package/query/QueryBuilderHelper.d.ts +18 -14
- package/query/QueryBuilderHelper.js +364 -147
- package/query/ScalarCriteriaNode.js +17 -8
- package/query/enums.d.ts +2 -0
- package/query/enums.js +2 -0
- package/query/raw.js +1 -1
- package/schema/DatabaseSchema.d.ts +7 -5
- package/schema/DatabaseSchema.js +68 -45
- package/schema/DatabaseTable.d.ts +8 -6
- package/schema/DatabaseTable.js +191 -107
- package/schema/SchemaComparator.d.ts +1 -3
- package/schema/SchemaComparator.js +76 -50
- package/schema/SchemaHelper.d.ts +2 -13
- package/schema/SchemaHelper.js +30 -9
- package/schema/SqlSchemaGenerator.d.ts +4 -14
- package/schema/SqlSchemaGenerator.js +26 -12
- package/typings.d.ts +10 -5
- package/tsconfig.build.tsbuildinfo +0 -1
|
@@ -1,34 +1,35 @@
|
|
|
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
|
-
import { JoinType, QueryType } from './enums.js';
|
|
3
|
-
import { NativeQueryBuilder } from './NativeQueryBuilder.js';
|
|
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
|
+
import { EMBEDDABLE_ARRAY_OPS, JoinType, QueryType } from './enums.js';
|
|
4
3
|
/**
|
|
5
4
|
* @internal
|
|
6
5
|
*/
|
|
7
6
|
export class QueryBuilderHelper {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
#platform;
|
|
8
|
+
#metadata;
|
|
9
|
+
#entityName;
|
|
10
|
+
#alias;
|
|
11
|
+
#aliasMap;
|
|
12
|
+
#subQueries;
|
|
13
|
+
#driver;
|
|
14
|
+
#tptAliasMap;
|
|
15
|
+
/** Monotonically increasing counter for unique JSON array iteration aliases within a single query. */
|
|
16
|
+
#jsonAliasCounter = 0;
|
|
16
17
|
constructor(entityName, alias, aliasMap, subQueries, driver, tptAliasMap = {}) {
|
|
17
|
-
this
|
|
18
|
-
this
|
|
19
|
-
this
|
|
20
|
-
this
|
|
21
|
-
this
|
|
22
|
-
this
|
|
23
|
-
this
|
|
24
|
-
this
|
|
18
|
+
this.#entityName = entityName;
|
|
19
|
+
this.#alias = alias;
|
|
20
|
+
this.#aliasMap = aliasMap;
|
|
21
|
+
this.#subQueries = subQueries;
|
|
22
|
+
this.#driver = driver;
|
|
23
|
+
this.#tptAliasMap = tptAliasMap;
|
|
24
|
+
this.#platform = this.#driver.getPlatform();
|
|
25
|
+
this.#metadata = this.#driver.getMetadata();
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
28
|
* For TPT inheritance, finds the correct alias for a property based on which entity owns it.
|
|
28
29
|
* Returns the main alias if not a TPT property or if the property belongs to the main entity.
|
|
29
30
|
*/
|
|
30
31
|
getTPTAliasForProperty(propName, defaultAlias) {
|
|
31
|
-
const meta = this
|
|
32
|
+
const meta = this.#aliasMap[defaultAlias]?.meta ?? this.#metadata.get(this.#entityName);
|
|
32
33
|
if (meta?.inheritanceType !== 'tpt' || !meta.tptParent) {
|
|
33
34
|
return defaultAlias;
|
|
34
35
|
}
|
|
@@ -39,7 +40,7 @@ export class QueryBuilderHelper {
|
|
|
39
40
|
// Walk up the TPT hierarchy to find which parent owns this property
|
|
40
41
|
let parentMeta = meta.tptParent;
|
|
41
42
|
while (parentMeta) {
|
|
42
|
-
const parentAlias = this
|
|
43
|
+
const parentAlias = this.#tptAliasMap[parentMeta.className];
|
|
43
44
|
if (parentAlias && parentMeta.ownProps?.some(p => p.name === propName || p.fieldNames?.includes(propName))) {
|
|
44
45
|
return parentAlias;
|
|
45
46
|
}
|
|
@@ -68,13 +69,13 @@ export class QueryBuilderHelper {
|
|
|
68
69
|
const prop = this.getProperty(f, a);
|
|
69
70
|
const fkIdx2 = prop?.fieldNames.findIndex(name => name === f) ?? -1;
|
|
70
71
|
if (fkIdx2 !== -1) {
|
|
71
|
-
parts.push(this.mapper(a !== this
|
|
72
|
+
parts.push(this.mapper(a !== this.#alias ? `${a}.${prop.fieldNames[fkIdx2]}` : prop.fieldNames[fkIdx2], type, value, alias));
|
|
72
73
|
}
|
|
73
74
|
else if (prop) {
|
|
74
|
-
parts.push(...prop.fieldNames.map(f => this.mapper(a !== this
|
|
75
|
+
parts.push(...prop.fieldNames.map(f => this.mapper(a !== this.#alias ? `${a}.${f}` : f, type, value, alias)));
|
|
75
76
|
}
|
|
76
77
|
else {
|
|
77
|
-
parts.push(this.mapper(a !== this
|
|
78
|
+
parts.push(this.mapper(a !== this.#alias ? `${a}.${f}` : f, type, value, alias));
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
// flatten the value if we see we are expanding nested composite key
|
|
@@ -88,16 +89,16 @@ export class QueryBuilderHelper {
|
|
|
88
89
|
}
|
|
89
90
|
});
|
|
90
91
|
}
|
|
91
|
-
return raw('(' + parts.map(part => this
|
|
92
|
+
return raw('(' + parts.map(part => this.#platform.quoteIdentifier(part)).join(', ') + ')');
|
|
92
93
|
}
|
|
93
94
|
const [a, f] = this.splitField(field);
|
|
94
95
|
const prop = this.getProperty(f, a);
|
|
95
96
|
// For TPT inheritance, resolve the correct alias for this property
|
|
96
97
|
// Only apply TPT resolution when `a` is an actual table alias (in aliasMap),
|
|
97
98
|
// not when it's an embedded property name like 'profile1.identity.links'
|
|
98
|
-
const isTableAlias = !!this
|
|
99
|
-
const baseAlias = isTableAlias ? a : this
|
|
100
|
-
const resolvedAlias = isTableAlias ? this.getTPTAliasForProperty(prop?.name ?? f, a) : this
|
|
99
|
+
const isTableAlias = !!this.#aliasMap[a];
|
|
100
|
+
const baseAlias = isTableAlias ? a : this.#alias;
|
|
101
|
+
const resolvedAlias = isTableAlias ? this.getTPTAliasForProperty(prop?.name ?? f, a) : this.#alias;
|
|
101
102
|
const aliasPrefix = isTableNameAliasRequired ? resolvedAlias + '.' : '';
|
|
102
103
|
const fkIdx2 = prop?.fieldNames.findIndex(name => name === f) ?? -1;
|
|
103
104
|
const fkIdx = fkIdx2 === -1 ? 0 : fkIdx2;
|
|
@@ -109,13 +110,13 @@ export class QueryBuilderHelper {
|
|
|
109
110
|
return raw(this.prefix(field, isTableNameAliasRequired));
|
|
110
111
|
}
|
|
111
112
|
if (prop?.formula) {
|
|
112
|
-
const alias2 = this
|
|
113
|
+
const alias2 = this.#platform.quoteIdentifier(a).toString();
|
|
113
114
|
const aliasName = alias === undefined ? prop.fieldNames[0] : alias;
|
|
114
|
-
const as = aliasName === null ? '' : ` as ${this
|
|
115
|
-
const meta = this
|
|
115
|
+
const as = aliasName === null ? '' : ` as ${this.#platform.quoteIdentifier(aliasName)}`;
|
|
116
|
+
const meta = this.#aliasMap[a]?.meta ?? this.#metadata.get(this.#entityName);
|
|
116
117
|
const table = this.createFormulaTable(alias2, meta, schema);
|
|
117
118
|
const columns = meta.createColumnMappingObject(p => this.getTPTAliasForProperty(p.name, a), alias2);
|
|
118
|
-
let value = this
|
|
119
|
+
let value = this.#driver.evaluateFormula(prop.formula, columns, table);
|
|
119
120
|
if (!this.isTableNameAliasRequired(type)) {
|
|
120
121
|
value = value.replaceAll(alias2 + '.', '');
|
|
121
122
|
}
|
|
@@ -126,16 +127,16 @@ export class QueryBuilderHelper {
|
|
|
126
127
|
if (prop.fieldNames.length > 1 && fkIdx !== -1) {
|
|
127
128
|
const fk = prop.targetMeta.getPrimaryProps()[fkIdx];
|
|
128
129
|
const prefixed = this.prefix(field, isTableNameAliasRequired, true, fkIdx);
|
|
129
|
-
valueSQL = fk.customType.convertToJSValueSQL(prefixed, this
|
|
130
|
+
valueSQL = fk.customType.convertToJSValueSQL(prefixed, this.#platform);
|
|
130
131
|
}
|
|
131
132
|
else {
|
|
132
133
|
const prefixed = this.prefix(field, isTableNameAliasRequired, true);
|
|
133
|
-
valueSQL = prop.customType.convertToJSValueSQL(prefixed, this
|
|
134
|
+
valueSQL = prop.customType.convertToJSValueSQL(prefixed, this.#platform);
|
|
134
135
|
}
|
|
135
136
|
if (alias === null) {
|
|
136
137
|
return raw(valueSQL);
|
|
137
138
|
}
|
|
138
|
-
return raw(`${valueSQL} as ${this
|
|
139
|
+
return raw(`${valueSQL} as ${this.#platform.quoteIdentifier(alias ?? prop.fieldNames[fkIdx])}`);
|
|
139
140
|
}
|
|
140
141
|
let ret = this.prefix(field, false, false, fkIdx);
|
|
141
142
|
if (alias) {
|
|
@@ -150,11 +151,11 @@ export class QueryBuilderHelper {
|
|
|
150
151
|
if (Array.isArray(data)) {
|
|
151
152
|
return data.map(d => this.processData(d, convertCustomTypes, true));
|
|
152
153
|
}
|
|
153
|
-
const meta = this
|
|
154
|
-
data = this
|
|
154
|
+
const meta = this.#metadata.find(this.#entityName);
|
|
155
|
+
data = this.#driver.mapDataToFieldNames(data, true, meta?.properties, convertCustomTypes);
|
|
155
156
|
if (!Utils.hasObjectKeys(data) && meta && multi) {
|
|
156
157
|
/* v8 ignore next */
|
|
157
|
-
data[meta.getPrimaryProps()[0].fieldNames[0]] = this
|
|
158
|
+
data[meta.getPrimaryProps()[0].fieldNames[0]] = this.#platform.usesDefaultKeyword() ? raw('default') : undefined;
|
|
158
159
|
}
|
|
159
160
|
return data;
|
|
160
161
|
}
|
|
@@ -164,26 +165,38 @@ export class QueryBuilderHelper {
|
|
|
164
165
|
const joinColumns = prop.owner ? prop.referencedColumnNames : prop2.joinColumns;
|
|
165
166
|
const inverseJoinColumns = prop.referencedColumnNames;
|
|
166
167
|
const primaryKeys = prop.owner ? prop.joinColumns : prop2.referencedColumnNames;
|
|
167
|
-
schema ??= prop.targetMeta?.schema === '*' ? '*' : this
|
|
168
|
+
schema ??= prop.targetMeta?.schema === '*' ? '*' : this.#driver.getSchemaName(prop.targetMeta);
|
|
168
169
|
cond = Utils.merge(cond, prop.where);
|
|
169
170
|
// For inverse side of polymorphic relations, add discriminator condition
|
|
170
171
|
if (!prop.owner && prop2.polymorphic && prop2.discriminatorColumn && prop2.discriminatorMap) {
|
|
171
|
-
const ownerMeta = this
|
|
172
|
+
const ownerMeta = this.#aliasMap[ownerAlias]?.meta ?? this.#metadata.get(this.#entityName);
|
|
172
173
|
const discriminatorValue = QueryHelper.findDiscriminatorValue(prop2.discriminatorMap, ownerMeta.class);
|
|
173
174
|
if (discriminatorValue) {
|
|
174
175
|
cond[`${alias}.${prop2.discriminatorColumn}`] = discriminatorValue;
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
178
|
return {
|
|
178
|
-
prop,
|
|
179
|
-
|
|
179
|
+
prop,
|
|
180
|
+
type,
|
|
181
|
+
cond,
|
|
182
|
+
ownerAlias,
|
|
183
|
+
alias,
|
|
184
|
+
table,
|
|
185
|
+
schema,
|
|
186
|
+
joinColumns,
|
|
187
|
+
inverseJoinColumns,
|
|
188
|
+
primaryKeys,
|
|
180
189
|
};
|
|
181
190
|
}
|
|
182
191
|
joinManyToOneReference(prop, ownerAlias, alias, type, cond = {}, schema) {
|
|
183
192
|
return {
|
|
184
|
-
prop,
|
|
193
|
+
prop,
|
|
194
|
+
type,
|
|
195
|
+
cond,
|
|
196
|
+
ownerAlias,
|
|
197
|
+
alias,
|
|
185
198
|
table: this.getTableName(prop.targetMeta.class),
|
|
186
|
-
schema: prop.targetMeta?.schema === '*' ? '*' : this
|
|
199
|
+
schema: prop.targetMeta?.schema === '*' ? '*' : this.#driver.getSchemaName(prop.targetMeta, { schema }),
|
|
187
200
|
joinColumns: prop.referencedColumnNames,
|
|
188
201
|
// For polymorphic relations, fieldNames includes the discriminator column which is not
|
|
189
202
|
// part of the join condition - use joinColumns (the FK columns only) instead
|
|
@@ -191,10 +204,12 @@ export class QueryBuilderHelper {
|
|
|
191
204
|
};
|
|
192
205
|
}
|
|
193
206
|
joinManyToManyReference(prop, ownerAlias, alias, pivotAlias, type, cond, path, schema) {
|
|
194
|
-
const pivotMeta = this
|
|
207
|
+
const pivotMeta = this.#metadata.find(prop.pivotEntity);
|
|
195
208
|
const ret = {
|
|
196
209
|
[`${ownerAlias}.${prop.name}#${pivotAlias}`]: {
|
|
197
|
-
prop,
|
|
210
|
+
prop,
|
|
211
|
+
type,
|
|
212
|
+
ownerAlias,
|
|
198
213
|
alias: pivotAlias,
|
|
199
214
|
inverseAlias: alias,
|
|
200
215
|
joinColumns: prop.joinColumns,
|
|
@@ -202,7 +217,7 @@ export class QueryBuilderHelper {
|
|
|
202
217
|
primaryKeys: prop.referencedColumnNames,
|
|
203
218
|
cond: {},
|
|
204
219
|
table: pivotMeta.tableName,
|
|
205
|
-
schema: prop.targetMeta?.schema === '*' ? '*' : this
|
|
220
|
+
schema: prop.targetMeta?.schema === '*' ? '*' : this.#driver.getSchemaName(pivotMeta, { schema }),
|
|
206
221
|
path: path.endsWith('[pivot]') ? path : `${path}[pivot]`,
|
|
207
222
|
},
|
|
208
223
|
};
|
|
@@ -216,16 +231,16 @@ export class QueryBuilderHelper {
|
|
|
216
231
|
ret[`${pivotAlias}.${prop2.name}#${alias}`].schema ??= tmp.length > 1 ? tmp[0] : undefined;
|
|
217
232
|
return ret;
|
|
218
233
|
}
|
|
219
|
-
processJoins(qb, joins, schema) {
|
|
234
|
+
processJoins(qb, joins, schema, schemaOverride) {
|
|
220
235
|
Object.values(joins).forEach(join => {
|
|
221
236
|
if ([JoinType.nestedInnerJoin, JoinType.nestedLeftJoin].includes(join.type)) {
|
|
222
237
|
return;
|
|
223
238
|
}
|
|
224
|
-
const { sql, params } = this.createJoinExpression(join, joins, schema);
|
|
239
|
+
const { sql, params } = this.createJoinExpression(join, joins, schema, schemaOverride);
|
|
225
240
|
qb.join(sql, params);
|
|
226
241
|
});
|
|
227
242
|
}
|
|
228
|
-
createJoinExpression(join, joins, schema) {
|
|
243
|
+
createJoinExpression(join, joins, schema, schemaOverride) {
|
|
229
244
|
let table = join.table;
|
|
230
245
|
const method = {
|
|
231
246
|
[JoinType.nestedInnerJoin]: 'inner join',
|
|
@@ -234,29 +249,31 @@ export class QueryBuilderHelper {
|
|
|
234
249
|
}[join.type] ?? join.type;
|
|
235
250
|
const conditions = [];
|
|
236
251
|
const params = [];
|
|
237
|
-
schema = join.schema
|
|
238
|
-
if (schema && schema !== this
|
|
252
|
+
schema = join.schema === '*' ? schema : (join.schema ?? schemaOverride);
|
|
253
|
+
if (schema && schema !== this.#platform.getDefaultSchemaName()) {
|
|
239
254
|
table = `${schema}.${table}`;
|
|
240
255
|
}
|
|
241
256
|
if (join.prop.name !== '__subquery__') {
|
|
242
257
|
join.primaryKeys.forEach((primaryKey, idx) => {
|
|
243
258
|
const right = `${join.alias}.${join.joinColumns[idx]}`;
|
|
244
259
|
if (join.prop.formula) {
|
|
245
|
-
const quotedAlias = this
|
|
246
|
-
const ownerMeta = this
|
|
260
|
+
const quotedAlias = this.#platform.quoteIdentifier(join.ownerAlias).toString();
|
|
261
|
+
const ownerMeta = this.#aliasMap[join.ownerAlias]?.meta ?? this.#metadata.get(this.#entityName);
|
|
247
262
|
const table = this.createFormulaTable(quotedAlias, ownerMeta, schema);
|
|
248
263
|
const columns = ownerMeta.createColumnMappingObject(p => this.getTPTAliasForProperty(p.name, join.ownerAlias), quotedAlias);
|
|
249
|
-
const left = this
|
|
250
|
-
conditions.push(`${left} = ${this
|
|
264
|
+
const left = this.#driver.evaluateFormula(join.prop.formula, columns, table);
|
|
265
|
+
conditions.push(`${left} = ${this.#platform.quoteIdentifier(right)}`);
|
|
251
266
|
return;
|
|
252
267
|
}
|
|
253
268
|
const left = join.prop.object && join.prop.fieldNameRaw
|
|
254
269
|
? join.prop.fieldNameRaw.replaceAll(ALIAS_REPLACEMENT, join.ownerAlias)
|
|
255
|
-
: this
|
|
256
|
-
conditions.push(`${left} = ${this
|
|
270
|
+
: this.#platform.quoteIdentifier(`${join.ownerAlias}.${primaryKey}`);
|
|
271
|
+
conditions.push(`${left} = ${this.#platform.quoteIdentifier(right)}`);
|
|
257
272
|
});
|
|
258
273
|
}
|
|
259
|
-
if (join.prop.targetMeta?.root.inheritanceType === 'sti' &&
|
|
274
|
+
if (join.prop.targetMeta?.root.inheritanceType === 'sti' &&
|
|
275
|
+
join.prop.targetMeta?.discriminatorValue &&
|
|
276
|
+
!join.path?.endsWith('[pivot]')) {
|
|
260
277
|
const typeProperty = join.prop.targetMeta.root.discriminatorColumn;
|
|
261
278
|
const alias = join.inverseAlias ?? join.alias;
|
|
262
279
|
join.cond[`${alias}.${typeProperty}`] = join.prop.targetMeta.discriminatorValue;
|
|
@@ -265,31 +282,36 @@ export class QueryBuilderHelper {
|
|
|
265
282
|
if (join.prop.polymorphic && join.prop.discriminatorColumn && join.prop.discriminatorMap) {
|
|
266
283
|
const discriminatorValue = QueryHelper.findDiscriminatorValue(join.prop.discriminatorMap, join.prop.targetMeta.class);
|
|
267
284
|
if (discriminatorValue) {
|
|
268
|
-
const discriminatorCol = this
|
|
285
|
+
const discriminatorCol = this.#platform.quoteIdentifier(`${join.ownerAlias}.${join.prop.discriminatorColumn}`);
|
|
269
286
|
conditions.push(`${discriminatorCol} = ?`);
|
|
270
287
|
params.push(discriminatorValue);
|
|
271
288
|
}
|
|
272
289
|
}
|
|
273
290
|
let sql = method + ' ';
|
|
274
291
|
if (join.nested) {
|
|
275
|
-
|
|
292
|
+
const asKeyword = this.#platform.usesAsKeyword() ? ' as ' : ' ';
|
|
293
|
+
sql += `(${this.#platform.quoteIdentifier(table)}${asKeyword}${this.#platform.quoteIdentifier(join.alias)}`;
|
|
276
294
|
for (const nested of join.nested) {
|
|
277
|
-
const { sql: nestedSql, params: nestedParams } = this.createJoinExpression(nested, joins, schema);
|
|
295
|
+
const { sql: nestedSql, params: nestedParams } = this.createJoinExpression(nested, joins, schema, schemaOverride);
|
|
278
296
|
sql += ' ' + nestedSql;
|
|
279
297
|
params.push(...nestedParams);
|
|
280
298
|
}
|
|
281
299
|
sql += `)`;
|
|
282
300
|
}
|
|
283
301
|
else if (join.subquery) {
|
|
284
|
-
|
|
302
|
+
const asKeyword = this.#platform.usesAsKeyword() ? ' as ' : ' ';
|
|
303
|
+
sql += `(${join.subquery})${asKeyword}${this.#platform.quoteIdentifier(join.alias)}`;
|
|
285
304
|
}
|
|
286
305
|
else {
|
|
287
|
-
sql +=
|
|
306
|
+
sql +=
|
|
307
|
+
this.#platform.quoteIdentifier(table) +
|
|
308
|
+
(this.#platform.usesAsKeyword() ? ' as ' : ' ') +
|
|
309
|
+
this.#platform.quoteIdentifier(join.alias);
|
|
288
310
|
}
|
|
289
|
-
const oldAlias = this
|
|
290
|
-
this
|
|
311
|
+
const oldAlias = this.#alias;
|
|
312
|
+
this.#alias = join.alias;
|
|
291
313
|
const subquery = this._appendQueryCondition(QueryType.SELECT, join.cond);
|
|
292
|
-
this
|
|
314
|
+
this.#alias = oldAlias;
|
|
293
315
|
if (subquery.sql) {
|
|
294
316
|
conditions.push(subquery.sql);
|
|
295
317
|
subquery.params.forEach(p => params.push(p));
|
|
@@ -312,12 +334,12 @@ export class QueryBuilderHelper {
|
|
|
312
334
|
];
|
|
313
335
|
}
|
|
314
336
|
isOneToOneInverse(field, meta) {
|
|
315
|
-
meta ??= this
|
|
337
|
+
meta ??= this.#metadata.find(this.#entityName);
|
|
316
338
|
const prop = meta.properties[field.replace(/:ref$/, '')];
|
|
317
339
|
return prop?.kind === ReferenceKind.ONE_TO_ONE && !prop.owner;
|
|
318
340
|
}
|
|
319
341
|
getTableName(entityName) {
|
|
320
|
-
const meta = this
|
|
342
|
+
const meta = this.#metadata.find(entityName);
|
|
321
343
|
return meta?.tableName ?? Utils.className(entityName);
|
|
322
344
|
}
|
|
323
345
|
/**
|
|
@@ -331,7 +353,7 @@ export class QueryBuilderHelper {
|
|
|
331
353
|
return false;
|
|
332
354
|
}
|
|
333
355
|
// when including the opening bracket/paren we consider it complex
|
|
334
|
-
return
|
|
356
|
+
return !/[{[(]/.exec(re.source);
|
|
335
357
|
}
|
|
336
358
|
getRegExpParam(re) {
|
|
337
359
|
const value = re.source
|
|
@@ -412,17 +434,37 @@ export class QueryBuilderHelper {
|
|
|
412
434
|
const parts = [];
|
|
413
435
|
const params = [];
|
|
414
436
|
if (this.isSimpleRegExp(cond[key])) {
|
|
415
|
-
parts.push(`${this
|
|
437
|
+
parts.push(`${this.#platform.quoteIdentifier(this.mapper(key, type))} like ?`);
|
|
416
438
|
params.push(this.getRegExpParam(cond[key]));
|
|
417
439
|
return { sql: parts.join(' and '), params };
|
|
418
440
|
}
|
|
441
|
+
if (Utils.isPlainObject(cond[key]) && !Raw.isKnownFragmentSymbol(key)) {
|
|
442
|
+
const [a, f] = this.splitField(key);
|
|
443
|
+
const prop = this.getProperty(f, a);
|
|
444
|
+
if (prop?.kind === ReferenceKind.EMBEDDED && prop.array) {
|
|
445
|
+
const keys = Object.keys(cond[key]);
|
|
446
|
+
const hasOnlyArrayOps = keys.every((k) => EMBEDDABLE_ARRAY_OPS.includes(k));
|
|
447
|
+
if (!hasOnlyArrayOps) {
|
|
448
|
+
return this.processEmbeddedArrayCondition(cond[key], prop, a);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// $elemMatch on JSON properties — iterate array elements via EXISTS subquery.
|
|
452
|
+
// When combined with other operators (e.g. $contains), processObjectSubCondition
|
|
453
|
+
// splits them first (size > 1), so $elemMatch arrives here alone.
|
|
454
|
+
if (prop && cond[key].$elemMatch != null && Utils.getObjectKeysSize(cond[key]) === 1) {
|
|
455
|
+
if (!(prop.customType instanceof JsonType)) {
|
|
456
|
+
throw new ValidationError(`$elemMatch can only be used on JSON array properties, but '${this.#entityName}.${prop.name}' has type '${prop.type}'`);
|
|
457
|
+
}
|
|
458
|
+
return this.processJsonElemMatch(cond[key].$elemMatch, prop, a);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
419
461
|
if (Utils.isPlainObject(cond[key]) || cond[key] instanceof RegExp) {
|
|
420
462
|
return this.processObjectSubCondition(cond, key, type);
|
|
421
463
|
}
|
|
422
464
|
const op = cond[key] === null ? 'is' : '=';
|
|
423
465
|
if (Raw.isKnownFragmentSymbol(key)) {
|
|
424
466
|
const raw = Raw.getKnownFragment(key);
|
|
425
|
-
const sql = raw.sql.replaceAll(ALIAS_REPLACEMENT, this
|
|
467
|
+
const sql = raw.sql.replaceAll(ALIAS_REPLACEMENT, this.#alias);
|
|
426
468
|
const value = Utils.asArray(cond[key]);
|
|
427
469
|
params.push(...raw.params);
|
|
428
470
|
if (value.length > 0) {
|
|
@@ -435,13 +477,13 @@ export class QueryBuilderHelper {
|
|
|
435
477
|
return { sql: parts.join(' and '), params };
|
|
436
478
|
}
|
|
437
479
|
const fields = Utils.splitPrimaryKeys(key);
|
|
438
|
-
if (this
|
|
480
|
+
if (this.#subQueries[key]) {
|
|
439
481
|
const val = this.getValueReplacement(fields, cond[key], params, key);
|
|
440
|
-
parts.push(`(${this
|
|
482
|
+
parts.push(`(${this.#subQueries[key]}) ${op} ${val}`);
|
|
441
483
|
return { sql: parts.join(' and '), params };
|
|
442
484
|
}
|
|
443
485
|
const val = this.getValueReplacement(fields, cond[key], params, key);
|
|
444
|
-
parts.push(`${this
|
|
486
|
+
parts.push(`${this.#platform.quoteIdentifier(this.mapper(key, type, cond[key], null))} ${op} ${val}`);
|
|
445
487
|
return { sql: parts.join(' and '), params };
|
|
446
488
|
}
|
|
447
489
|
processObjectSubCondition(cond, key, type) {
|
|
@@ -455,7 +497,7 @@ export class QueryBuilderHelper {
|
|
|
455
497
|
// grouped condition for one field, e.g. `{ age: { $gte: 10, $lt: 50 } }`
|
|
456
498
|
if (size > 1) {
|
|
457
499
|
const subCondition = Object.entries(value).map(([subKey, subValue]) => {
|
|
458
|
-
return
|
|
500
|
+
return { [key]: { [subKey]: subValue } };
|
|
459
501
|
});
|
|
460
502
|
for (const sub of subCondition) {
|
|
461
503
|
this.append(() => this._appendQueryCondition(type, sub, '$and'), parts, params);
|
|
@@ -463,7 +505,7 @@ export class QueryBuilderHelper {
|
|
|
463
505
|
return { sql: parts.join(' and '), params };
|
|
464
506
|
}
|
|
465
507
|
if (value instanceof RegExp) {
|
|
466
|
-
value = this
|
|
508
|
+
value = this.#platform.getRegExpValue(value);
|
|
467
509
|
}
|
|
468
510
|
// operators
|
|
469
511
|
const op = Object.keys(QueryOperator).find(op => op in value);
|
|
@@ -476,17 +518,17 @@ export class QueryBuilderHelper {
|
|
|
476
518
|
const fields = rawField ? [key] : Utils.splitPrimaryKeys(key);
|
|
477
519
|
if (fields.length > 1 && Array.isArray(value[op])) {
|
|
478
520
|
const singleTuple = !value[op].every((v) => Array.isArray(v));
|
|
479
|
-
if (!this
|
|
521
|
+
if (!this.#platform.allowsComparingTuples()) {
|
|
480
522
|
const mapped = fields.map(f => this.mapper(f, type));
|
|
481
523
|
if (op === '$in') {
|
|
482
524
|
const conds = value[op].map(() => {
|
|
483
|
-
return `(${mapped.map(field => `${this
|
|
525
|
+
return `(${mapped.map(field => `${this.#platform.quoteIdentifier(field)} = ?`).join(' and ')})`;
|
|
484
526
|
});
|
|
485
527
|
parts.push(`(${conds.join(' or ')})`);
|
|
486
528
|
params.push(...Utils.flatten(value[op]));
|
|
487
529
|
return { sql: parts.join(' and '), params };
|
|
488
530
|
}
|
|
489
|
-
parts.push(...mapped.map(field => `${this
|
|
531
|
+
parts.push(...mapped.map(field => `${this.#platform.quoteIdentifier(field)} = ?`));
|
|
490
532
|
params.push(...Utils.flatten(value[op]));
|
|
491
533
|
return { sql: parts.join(' and '), params };
|
|
492
534
|
}
|
|
@@ -496,9 +538,9 @@ export class QueryBuilderHelper {
|
|
|
496
538
|
value[op] = raw(sql, tmp);
|
|
497
539
|
}
|
|
498
540
|
}
|
|
499
|
-
if (this
|
|
541
|
+
if (this.#subQueries[key]) {
|
|
500
542
|
const val = this.getValueReplacement(fields, value[op], params, op);
|
|
501
|
-
parts.push(`(${this
|
|
543
|
+
parts.push(`(${this.#subQueries[key]}) ${replacement} ${val}`);
|
|
502
544
|
return { sql: parts.join(' and '), params };
|
|
503
545
|
}
|
|
504
546
|
const [a, f] = rawField ? [] : this.splitField(key);
|
|
@@ -511,7 +553,7 @@ export class QueryBuilderHelper {
|
|
|
511
553
|
if (!prop) {
|
|
512
554
|
throw new Error(`Cannot use $fulltext operator on ${String(key)}, property not found`);
|
|
513
555
|
}
|
|
514
|
-
const { sql, params: params2 } = raw(this
|
|
556
|
+
const { sql, params: params2 } = raw(this.#platform.getFullTextWhereClause(prop), {
|
|
515
557
|
column: this.mapper(key, type, undefined, null),
|
|
516
558
|
query: value[op],
|
|
517
559
|
});
|
|
@@ -521,20 +563,26 @@ export class QueryBuilderHelper {
|
|
|
521
563
|
else if (['$in', '$nin'].includes(op) && Array.isArray(value[op]) && value[op].length === 0) {
|
|
522
564
|
parts.push(`1 = ${op === '$in' ? 0 : 1}`);
|
|
523
565
|
}
|
|
524
|
-
else if (
|
|
525
|
-
const
|
|
566
|
+
else if (op === '$re') {
|
|
567
|
+
const mappedKey = this.mapper(key, type, value[op], null);
|
|
568
|
+
const processed = this.#platform.mapRegExpCondition(mappedKey, value);
|
|
569
|
+
parts.push(processed.sql);
|
|
570
|
+
params.push(...processed.params);
|
|
571
|
+
}
|
|
572
|
+
else if (value[op] instanceof Raw || typeof value[op]?.toRaw === 'function') {
|
|
573
|
+
const query = value[op] instanceof Raw ? value[op] : value[op].toRaw();
|
|
526
574
|
const mappedKey = this.mapper(key, type, query, null);
|
|
527
575
|
let sql = query.sql;
|
|
528
576
|
if (['$in', '$nin'].includes(op)) {
|
|
529
577
|
sql = `(${sql})`;
|
|
530
578
|
}
|
|
531
|
-
parts.push(`${this
|
|
579
|
+
parts.push(`${this.#platform.quoteIdentifier(mappedKey)} ${replacement} ${sql}`);
|
|
532
580
|
params.push(...query.params);
|
|
533
581
|
}
|
|
534
582
|
else {
|
|
535
583
|
const mappedKey = this.mapper(key, type, value[op], null);
|
|
536
584
|
const val = this.getValueReplacement(fields, value[op], params, op, prop);
|
|
537
|
-
parts.push(`${this
|
|
585
|
+
parts.push(`${this.#platform.quoteIdentifier(mappedKey)} ${replacement} ${val}`);
|
|
538
586
|
}
|
|
539
587
|
return { sql: parts.join(' and '), params };
|
|
540
588
|
}
|
|
@@ -549,7 +597,11 @@ export class QueryBuilderHelper {
|
|
|
549
597
|
return `(${tmp.join(', ')})`;
|
|
550
598
|
}
|
|
551
599
|
if (prop?.customType instanceof ArrayType) {
|
|
552
|
-
const item = prop.customType.convertToDatabaseValue(value, this
|
|
600
|
+
const item = prop.customType.convertToDatabaseValue(value, this.#platform, {
|
|
601
|
+
fromQuery: true,
|
|
602
|
+
key,
|
|
603
|
+
mode: 'query',
|
|
604
|
+
});
|
|
553
605
|
params.push(item);
|
|
554
606
|
}
|
|
555
607
|
else {
|
|
@@ -573,7 +625,7 @@ export class QueryBuilderHelper {
|
|
|
573
625
|
replacement = op === '$eq' ? 'is' : 'is not';
|
|
574
626
|
}
|
|
575
627
|
if (op === '$re') {
|
|
576
|
-
replacement = this
|
|
628
|
+
replacement = this.#platform.getRegExpOperator(value[op], value.$flags);
|
|
577
629
|
}
|
|
578
630
|
if (replacement.includes('?')) {
|
|
579
631
|
replacement = replacement.replaceAll('?', '\\?');
|
|
@@ -618,7 +670,7 @@ export class QueryBuilderHelper {
|
|
|
618
670
|
const order = typeof direction === 'number' ? QueryOrderNumeric[direction] : direction;
|
|
619
671
|
if (Raw.isKnownFragmentSymbol(key)) {
|
|
620
672
|
const raw = Raw.getKnownFragment(key);
|
|
621
|
-
ret.push(...this
|
|
673
|
+
ret.push(...this.#platform.getOrderByExpression(this.#platform.formatQuery(raw.sql, raw.params), order, collation));
|
|
622
674
|
continue;
|
|
623
675
|
}
|
|
624
676
|
for (const f of Utils.splitPrimaryKeys(key)) {
|
|
@@ -629,58 +681,27 @@ export class QueryBuilderHelper {
|
|
|
629
681
|
const noPrefix = (prop?.persist === false && !prop.formula && !prop.embedded) || Raw.isKnownFragment(f);
|
|
630
682
|
const column = this.mapper(noPrefix ? field : `${alias}.${field}`, type, undefined, null);
|
|
631
683
|
/* v8 ignore next */
|
|
632
|
-
const rawColumn = typeof column === 'string'
|
|
684
|
+
const rawColumn = typeof column === 'string'
|
|
685
|
+
? column
|
|
686
|
+
.split('.')
|
|
687
|
+
.map(e => this.#platform.quoteIdentifier(e))
|
|
688
|
+
.join('.')
|
|
689
|
+
: column;
|
|
633
690
|
const customOrder = prop?.customOrder;
|
|
634
|
-
let colPart = customOrder
|
|
635
|
-
? this.platform.generateCustomOrder(rawColumn, customOrder)
|
|
636
|
-
: rawColumn;
|
|
691
|
+
let colPart = customOrder ? this.#platform.generateCustomOrder(rawColumn, customOrder) : rawColumn;
|
|
637
692
|
if (isRaw(colPart)) {
|
|
638
|
-
colPart = this
|
|
693
|
+
colPart = this.#platform.formatQuery(colPart.sql, colPart.params);
|
|
639
694
|
}
|
|
640
695
|
if (Array.isArray(order)) {
|
|
641
696
|
order.forEach(part => ret.push(...this.getQueryOrderFromObject(type, part, populate, collation)));
|
|
642
697
|
}
|
|
643
698
|
else {
|
|
644
|
-
ret.push(...this
|
|
699
|
+
ret.push(...this.#platform.getOrderByExpression(colPart, order, collation));
|
|
645
700
|
}
|
|
646
701
|
}
|
|
647
702
|
}
|
|
648
703
|
return ret;
|
|
649
704
|
}
|
|
650
|
-
finalize(type, qb, meta, data, returning) {
|
|
651
|
-
const usesReturningStatement = this.platform.usesReturningStatement() || this.platform.usesOutputStatement();
|
|
652
|
-
if (!meta || !data || !usesReturningStatement) {
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
// always respect explicit returning hint
|
|
656
|
-
if (returning && returning.length > 0) {
|
|
657
|
-
qb.returning(returning.map(field => this.mapper(field, type)));
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
if (type === QueryType.INSERT) {
|
|
661
|
-
const returningProps = meta.hydrateProps
|
|
662
|
-
.filter(prop => prop.returning || (prop.persist !== false && ((prop.primary && prop.autoincrement) || prop.defaultRaw)))
|
|
663
|
-
.filter(prop => !(prop.name in data));
|
|
664
|
-
if (returningProps.length > 0) {
|
|
665
|
-
qb.returning(Utils.flatten(returningProps.map(prop => prop.fieldNames)));
|
|
666
|
-
}
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
if (type === QueryType.UPDATE) {
|
|
670
|
-
const returningProps = meta.hydrateProps.filter(prop => prop.fieldNames && isRaw(data[prop.fieldNames[0]]));
|
|
671
|
-
if (returningProps.length > 0) {
|
|
672
|
-
const fields = returningProps.flatMap((prop) => {
|
|
673
|
-
if (prop.hasConvertToJSValueSQL) {
|
|
674
|
-
const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
|
|
675
|
-
const sql = prop.customType.convertToJSValueSQL(aliased, this.platform) + ' as ' + this.platform.quoteIdentifier(prop.fieldNames[0]);
|
|
676
|
-
return [raw(sql)];
|
|
677
|
-
}
|
|
678
|
-
return prop.fieldNames;
|
|
679
|
-
});
|
|
680
|
-
qb.returning(fields);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
705
|
splitField(field, greedyAlias = false) {
|
|
685
706
|
const parts = field.split('.');
|
|
686
707
|
const ref = parts[parts.length - 1].split(':')[1];
|
|
@@ -688,7 +709,7 @@ export class QueryBuilderHelper {
|
|
|
688
709
|
parts[parts.length - 1] = parts[parts.length - 1].substring(0, parts[parts.length - 1].indexOf(':'));
|
|
689
710
|
}
|
|
690
711
|
if (parts.length === 1) {
|
|
691
|
-
return [this
|
|
712
|
+
return [this.#alias, parts[0], ref];
|
|
692
713
|
}
|
|
693
714
|
if (greedyAlias) {
|
|
694
715
|
const fromField = parts.pop();
|
|
@@ -700,28 +721,28 @@ export class QueryBuilderHelper {
|
|
|
700
721
|
return [fromAlias, fromField, ref];
|
|
701
722
|
}
|
|
702
723
|
getLockSQL(qb, lockMode, lockTables = [], joinsMap) {
|
|
703
|
-
const meta = this
|
|
724
|
+
const meta = this.#metadata.find(this.#entityName);
|
|
704
725
|
if (lockMode === LockMode.OPTIMISTIC && meta && !meta.versionProperty) {
|
|
705
|
-
throw OptimisticLockError.lockFailed(Utils.className(this
|
|
726
|
+
throw OptimisticLockError.lockFailed(Utils.className(this.#entityName));
|
|
706
727
|
}
|
|
707
728
|
if (lockMode !== LockMode.OPTIMISTIC && lockTables.length === 0 && joinsMap) {
|
|
708
729
|
const joins = Object.values(joinsMap);
|
|
709
730
|
const innerJoins = joins.filter(join => [JoinType.innerJoin, JoinType.innerJoinLateral, JoinType.nestedInnerJoin].includes(join.type));
|
|
710
731
|
if (joins.length > innerJoins.length) {
|
|
711
|
-
lockTables.push(this
|
|
732
|
+
lockTables.push(this.#alias, ...innerJoins.map(join => join.alias));
|
|
712
733
|
}
|
|
713
734
|
}
|
|
714
735
|
qb.lockMode(lockMode, lockTables);
|
|
715
736
|
}
|
|
716
737
|
updateVersionProperty(qb, data) {
|
|
717
|
-
const meta = this
|
|
738
|
+
const meta = this.#metadata.find(this.#entityName);
|
|
718
739
|
if (!meta?.versionProperty || meta.versionProperty in data) {
|
|
719
740
|
return;
|
|
720
741
|
}
|
|
721
742
|
const versionProperty = meta.properties[meta.versionProperty];
|
|
722
|
-
let sql = this
|
|
743
|
+
let sql = this.#platform.quoteIdentifier(versionProperty.fieldNames[0]) + ' + 1';
|
|
723
744
|
if (versionProperty.runtimeType === 'Date') {
|
|
724
|
-
sql = this
|
|
745
|
+
sql = this.#platform.getCurrentTimestampSQL(versionProperty.length);
|
|
725
746
|
}
|
|
726
747
|
qb.update({ [versionProperty.fieldNames[0]]: raw(sql) });
|
|
727
748
|
}
|
|
@@ -729,8 +750,8 @@ export class QueryBuilderHelper {
|
|
|
729
750
|
let ret;
|
|
730
751
|
if (!this.isPrefixed(field)) {
|
|
731
752
|
// For TPT inheritance, resolve the correct alias for this property
|
|
732
|
-
const tptAlias = this.getTPTAliasForProperty(field, this
|
|
733
|
-
const alias = always ? (quote ? tptAlias : this
|
|
753
|
+
const tptAlias = this.getTPTAliasForProperty(field, this.#alias);
|
|
754
|
+
const alias = always ? (quote ? tptAlias : this.#platform.quoteIdentifier(tptAlias)) + '.' : '';
|
|
734
755
|
const fieldName = this.fieldName(field, tptAlias, always, idx);
|
|
735
756
|
if (fieldName instanceof Raw) {
|
|
736
757
|
return fieldName.sql;
|
|
@@ -743,7 +764,7 @@ export class QueryBuilderHelper {
|
|
|
743
764
|
// For TPT inheritance, resolve the correct alias for this property
|
|
744
765
|
// Only apply TPT resolution when `a` is an actual table alias (in aliasMap),
|
|
745
766
|
// not when it's an embedded property name like 'profile1.identity.links'
|
|
746
|
-
const isTableAlias = !!this
|
|
767
|
+
const isTableAlias = !!this.#aliasMap[a];
|
|
747
768
|
const resolvedAlias = isTableAlias ? this.getTPTAliasForProperty(f, a) : a;
|
|
748
769
|
const fieldName = this.fieldName(f, resolvedAlias, always, idx);
|
|
749
770
|
if (fieldName instanceof Raw) {
|
|
@@ -752,7 +773,7 @@ export class QueryBuilderHelper {
|
|
|
752
773
|
ret = resolvedAlias + '.' + fieldName;
|
|
753
774
|
}
|
|
754
775
|
if (quote) {
|
|
755
|
-
return this
|
|
776
|
+
return this.#platform.quoteIdentifier(ret);
|
|
756
777
|
}
|
|
757
778
|
return ret;
|
|
758
779
|
}
|
|
@@ -770,7 +791,9 @@ export class QueryBuilderHelper {
|
|
|
770
791
|
// skip nesting parens if the value is simple = scalar or object without operators or with only single key, being the operator
|
|
771
792
|
const keys = Utils.getObjectQueryKeys(sub);
|
|
772
793
|
const val = sub[keys[0]];
|
|
773
|
-
const simple = !Utils.isPlainObject(val) ||
|
|
794
|
+
const simple = !Utils.isPlainObject(val) ||
|
|
795
|
+
Utils.getObjectKeysSize(val) === 1 ||
|
|
796
|
+
Object.keys(val).every(k => !Utils.isOperator(k));
|
|
774
797
|
if (keys.length === 1 && simple) {
|
|
775
798
|
this.append(() => this._appendQueryCondition(type, sub, operator), parts, params);
|
|
776
799
|
continue;
|
|
@@ -780,7 +803,7 @@ export class QueryBuilderHelper {
|
|
|
780
803
|
return { sql: `(${parts.join(' or ')})`, params };
|
|
781
804
|
}
|
|
782
805
|
isPrefixed(field) {
|
|
783
|
-
return
|
|
806
|
+
return !!/[\w`"[\]]+\./.exec(field);
|
|
784
807
|
}
|
|
785
808
|
fieldName(field, alias, always, idx = 0) {
|
|
786
809
|
const prop = this.getProperty(field, alias);
|
|
@@ -791,7 +814,7 @@ export class QueryBuilderHelper {
|
|
|
791
814
|
if (!always) {
|
|
792
815
|
return raw(prop.fieldNameRaw
|
|
793
816
|
.replace(new RegExp(ALIAS_REPLACEMENT_RE + '\\.?', 'g'), '')
|
|
794
|
-
.replace(this
|
|
817
|
+
.replace(this.#platform.quoteIdentifier('') + '.', ''));
|
|
795
818
|
}
|
|
796
819
|
if (alias) {
|
|
797
820
|
return raw(prop.fieldNameRaw.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), alias));
|
|
@@ -803,8 +826,12 @@ export class QueryBuilderHelper {
|
|
|
803
826
|
return prop.fieldNames?.[idx] ?? field;
|
|
804
827
|
}
|
|
805
828
|
getProperty(field, alias) {
|
|
806
|
-
const entityName = this
|
|
807
|
-
const meta = this
|
|
829
|
+
const entityName = this.#aliasMap[alias]?.entityName || this.#entityName;
|
|
830
|
+
const meta = this.#metadata.find(entityName);
|
|
831
|
+
// raw table name (e.g. CTE) — no metadata available
|
|
832
|
+
if (!meta) {
|
|
833
|
+
return undefined;
|
|
834
|
+
}
|
|
808
835
|
// check if `alias` is not matching an embedded property name instead of alias, e.g. `address.city`
|
|
809
836
|
if (alias) {
|
|
810
837
|
const prop = meta.properties[alias];
|
|
@@ -822,8 +849,198 @@ export class QueryBuilderHelper {
|
|
|
822
849
|
isTableNameAliasRequired(type) {
|
|
823
850
|
return [QueryType.SELECT, QueryType.COUNT].includes(type);
|
|
824
851
|
}
|
|
852
|
+
processEmbeddedArrayCondition(cond, prop, alias) {
|
|
853
|
+
const column = this.#platform.quoteIdentifier(`${alias}.${prop.fieldNames[0]}`);
|
|
854
|
+
const resolveProperty = (key) => {
|
|
855
|
+
const { embProp, jsonPropName } = this.resolveEmbeddedProp(prop, key);
|
|
856
|
+
return { name: jsonPropName, type: embProp.runtimeType ?? 'string' };
|
|
857
|
+
};
|
|
858
|
+
const invalidObjectError = (key) => ValidationError.invalidEmbeddableQuery(this.#entityName, key, prop.type);
|
|
859
|
+
const parts = [];
|
|
860
|
+
const allParams = [];
|
|
861
|
+
// Top-level $not generates NOT EXISTS (no element matches the inner condition).
|
|
862
|
+
const { $not, ...rest } = cond;
|
|
863
|
+
if (Utils.hasObjectKeys(rest)) {
|
|
864
|
+
const result = this.buildJsonArrayExists(rest, column, false, resolveProperty, invalidObjectError);
|
|
865
|
+
if (result) {
|
|
866
|
+
parts.push(result.sql);
|
|
867
|
+
allParams.push(...result.params);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if ($not != null) {
|
|
871
|
+
if (!Utils.isPlainObject($not)) {
|
|
872
|
+
throw new ValidationError(`Invalid query: $not in embedded array queries expects an object value`);
|
|
873
|
+
}
|
|
874
|
+
const result = this.buildJsonArrayExists($not, column, true, resolveProperty, invalidObjectError);
|
|
875
|
+
if (result) {
|
|
876
|
+
parts.push(result.sql);
|
|
877
|
+
allParams.push(...result.params);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (parts.length === 0) {
|
|
881
|
+
return { sql: '1 = 1', params: [] };
|
|
882
|
+
}
|
|
883
|
+
return { sql: parts.join(' and '), params: allParams };
|
|
884
|
+
}
|
|
885
|
+
buildJsonArrayExists(cond, column, negate, resolveProperty, invalidObjectError) {
|
|
886
|
+
const jeAlias = `__je${this.#jsonAliasCounter++}`;
|
|
887
|
+
const referencedProps = new Map();
|
|
888
|
+
const { sql: whereSql, params } = this.buildArrayElementWhere(cond, jeAlias, referencedProps, resolveProperty, invalidObjectError);
|
|
889
|
+
if (!whereSql) {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
const from = this.#platform.getJsonArrayFromSQL(column, jeAlias, [...referencedProps.values()]);
|
|
893
|
+
const exists = this.#platform.getJsonArrayExistsSQL(from, whereSql);
|
|
894
|
+
return { sql: negate ? `not ${exists}` : exists, params };
|
|
895
|
+
}
|
|
896
|
+
resolveEmbeddedProp(prop, key) {
|
|
897
|
+
const embProp = prop.embeddedProps[key] ?? Object.values(prop.embeddedProps).find(p => p.name === key);
|
|
898
|
+
if (!embProp) {
|
|
899
|
+
throw ValidationError.invalidEmbeddableQuery(this.#entityName, key, prop.type);
|
|
900
|
+
}
|
|
901
|
+
const prefix = `${prop.fieldNames[0]}~`;
|
|
902
|
+
const raw = embProp.fieldNames[0];
|
|
903
|
+
const jsonPropName = raw.startsWith(prefix) ? raw.slice(prefix.length) : raw;
|
|
904
|
+
return { embProp, jsonPropName };
|
|
905
|
+
}
|
|
906
|
+
buildEmbeddedArrayOperatorCondition(lhs, value, params) {
|
|
907
|
+
const supported = new Set(['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin', '$not', '$like', '$exists']);
|
|
908
|
+
const parts = [];
|
|
909
|
+
// Clone to avoid getOperatorReplacement mutating the original (it sets value[op] = null for $exists).
|
|
910
|
+
value = { ...value };
|
|
911
|
+
for (const op of Object.keys(value)) {
|
|
912
|
+
if (!supported.has(op)) {
|
|
913
|
+
throw new ValidationError(`Operator ${op} is not supported in embedded array queries`);
|
|
914
|
+
}
|
|
915
|
+
const replacement = this.getOperatorReplacement(op, value);
|
|
916
|
+
const val = value[op];
|
|
917
|
+
if (['$in', '$nin'].includes(op)) {
|
|
918
|
+
if (!Array.isArray(val)) {
|
|
919
|
+
throw new ValidationError(`Invalid query: ${op} operator expects an array value`);
|
|
920
|
+
}
|
|
921
|
+
else if (val.length === 0) {
|
|
922
|
+
parts.push(`1 = ${op === '$in' ? 0 : 1}`);
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
val.forEach((v) => params.push(v));
|
|
926
|
+
parts.push(`${lhs} ${replacement} (${val.map(() => '?').join(', ')})`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
else if (op === '$exists') {
|
|
930
|
+
parts.push(`${lhs} ${replacement} null`);
|
|
931
|
+
}
|
|
932
|
+
else if (val === null) {
|
|
933
|
+
parts.push(`${lhs} ${replacement} null`);
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
parts.push(`${lhs} ${replacement} ?`);
|
|
937
|
+
params.push(val);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return parts.join(' and ');
|
|
941
|
+
}
|
|
942
|
+
processJsonElemMatch(cond, prop, alias) {
|
|
943
|
+
const column = this.#platform.quoteIdentifier(`${alias}.${prop.fieldNames[0]}`);
|
|
944
|
+
const result = this.buildJsonArrayExists(cond, column, false, (key, value) => {
|
|
945
|
+
this.#platform.validateJsonPropertyName(key);
|
|
946
|
+
return { name: key, type: this.inferJsonValueType(value) };
|
|
947
|
+
}, () => ValidationError.invalidQueryCondition(cond));
|
|
948
|
+
return result ?? { sql: '1 = 1', params: [] };
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Shared logic for building WHERE conditions inside JSON array EXISTS subqueries.
|
|
952
|
+
* Used by both embedded array queries (metadata-driven) and $elemMatch (type-inferred).
|
|
953
|
+
*/
|
|
954
|
+
buildArrayElementWhere(cond, jeAlias, referencedProps, resolveProperty, invalidObjectError) {
|
|
955
|
+
const parts = [];
|
|
956
|
+
const params = [];
|
|
957
|
+
for (const k of Object.keys(cond)) {
|
|
958
|
+
if (k === '$and' || k === '$or') {
|
|
959
|
+
const items = cond[k];
|
|
960
|
+
if (items.length === 0) {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
const subParts = [];
|
|
964
|
+
for (const item of items) {
|
|
965
|
+
const sub = this.buildArrayElementWhere(item, jeAlias, referencedProps, resolveProperty, invalidObjectError);
|
|
966
|
+
if (sub.sql) {
|
|
967
|
+
subParts.push(sub.sql);
|
|
968
|
+
params.push(...sub.params);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (subParts.length > 0) {
|
|
972
|
+
const joiner = k === '$or' ? ' or ' : ' and ';
|
|
973
|
+
parts.push(`(${subParts.join(joiner)})`);
|
|
974
|
+
}
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
// Within $or/$and scope, $not provides element-level negation:
|
|
978
|
+
// "this element does not match the condition".
|
|
979
|
+
if (k === '$not') {
|
|
980
|
+
const sub = this.buildArrayElementWhere(cond[k], jeAlias, referencedProps, resolveProperty, invalidObjectError);
|
|
981
|
+
if (sub.sql) {
|
|
982
|
+
parts.push(`not (${sub.sql})`);
|
|
983
|
+
params.push(...sub.params);
|
|
984
|
+
}
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
const value = cond[k];
|
|
988
|
+
const { name, type } = resolveProperty(k, value);
|
|
989
|
+
referencedProps.set(k, { name, type });
|
|
990
|
+
const lhs = this.#platform.getJsonArrayElementPropertySQL(jeAlias, name, type);
|
|
991
|
+
if (Utils.isPlainObject(value)) {
|
|
992
|
+
const valueKeys = Object.keys(value);
|
|
993
|
+
if (valueKeys.some(vk => !Utils.isOperator(vk))) {
|
|
994
|
+
throw invalidObjectError(k);
|
|
995
|
+
}
|
|
996
|
+
const sub = this.buildEmbeddedArrayOperatorCondition(lhs, value, params);
|
|
997
|
+
parts.push(sub);
|
|
998
|
+
}
|
|
999
|
+
else if (value === null) {
|
|
1000
|
+
parts.push(`${lhs} is null`);
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
parts.push(`${lhs} = ?`);
|
|
1004
|
+
params.push(value);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return { sql: parts.join(' and '), params };
|
|
1008
|
+
}
|
|
1009
|
+
inferJsonValueType(value) {
|
|
1010
|
+
if (typeof value === 'number') {
|
|
1011
|
+
return 'number';
|
|
1012
|
+
}
|
|
1013
|
+
if (typeof value === 'boolean') {
|
|
1014
|
+
return 'boolean';
|
|
1015
|
+
}
|
|
1016
|
+
if (typeof value === 'bigint') {
|
|
1017
|
+
return 'bigint';
|
|
1018
|
+
}
|
|
1019
|
+
if (Utils.isPlainObject(value)) {
|
|
1020
|
+
for (const v of Object.values(value)) {
|
|
1021
|
+
if (typeof v === 'number') {
|
|
1022
|
+
return 'number';
|
|
1023
|
+
}
|
|
1024
|
+
if (typeof v === 'boolean') {
|
|
1025
|
+
return 'boolean';
|
|
1026
|
+
}
|
|
1027
|
+
if (typeof v === 'bigint') {
|
|
1028
|
+
return 'bigint';
|
|
1029
|
+
}
|
|
1030
|
+
if (Array.isArray(v) && v.length > 0) {
|
|
1031
|
+
if (typeof v[0] === 'number') {
|
|
1032
|
+
return 'number';
|
|
1033
|
+
}
|
|
1034
|
+
if (typeof v[0] === 'boolean') {
|
|
1035
|
+
return 'boolean';
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return 'string';
|
|
1041
|
+
}
|
|
825
1042
|
processOnConflictCondition(cond, schema) {
|
|
826
|
-
const meta = this
|
|
1043
|
+
const meta = this.#metadata.get(this.#entityName);
|
|
827
1044
|
const tableName = meta.tableName;
|
|
828
1045
|
for (const key of Object.keys(cond)) {
|
|
829
1046
|
const mapped = this.mapper(key, QueryType.UPSERT);
|