@mikro-orm/sql 7.0.0-rc.3 → 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 +18 -5
- package/AbstractSqlDriver.d.ts +1 -1
- package/AbstractSqlDriver.js +39 -10
- package/AbstractSqlPlatform.d.ts +34 -0
- package/AbstractSqlPlatform.js +47 -3
- package/PivotCollectionPersister.d.ts +2 -11
- package/PivotCollectionPersister.js +59 -59
- package/README.md +5 -4
- package/SqlEntityManager.d.ts +1 -1
- package/dialects/index.d.ts +1 -0
- package/dialects/index.js +1 -0
- package/dialects/mysql/BaseMySqlPlatform.d.ts +6 -0
- package/dialects/mysql/BaseMySqlPlatform.js +17 -0
- package/dialects/mysql/MySqlSchemaHelper.d.ts +1 -1
- package/dialects/mysql/MySqlSchemaHelper.js +6 -6
- 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 +12 -8
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +13 -13
- package/dialects/sqlite/SqlitePlatform.d.ts +1 -0
- package/dialects/sqlite/SqlitePlatform.js +3 -0
- package/dialects/sqlite/SqliteSchemaHelper.js +12 -8
- package/index.d.ts +1 -1
- package/index.js +0 -1
- package/package.json +3 -3
- package/plugin/index.d.ts +1 -14
- package/plugin/index.js +13 -13
- package/plugin/transformer.d.ts +6 -22
- package/plugin/transformer.js +81 -73
- package/query/ArrayCriteriaNode.d.ts +1 -1
- package/query/CriteriaNodeFactory.js +15 -3
- package/query/NativeQueryBuilder.d.ts +3 -3
- package/query/NativeQueryBuilder.js +4 -2
- package/query/ObjectCriteriaNode.js +4 -4
- package/query/QueryBuilder.d.ts +58 -62
- package/query/QueryBuilder.js +377 -370
- package/query/QueryBuilderHelper.d.ts +14 -11
- package/query/QueryBuilderHelper.js +324 -137
- package/query/ScalarCriteriaNode.js +3 -1
- package/query/enums.d.ts +2 -0
- package/query/enums.js +2 -0
- package/schema/DatabaseSchema.d.ts +7 -5
- package/schema/DatabaseSchema.js +50 -33
- package/schema/DatabaseTable.d.ts +8 -6
- package/schema/DatabaseTable.js +84 -60
- package/schema/SchemaComparator.d.ts +1 -3
- package/schema/SchemaComparator.js +22 -20
- package/schema/SchemaHelper.d.ts +2 -13
- package/schema/SchemaHelper.js +2 -1
- package/schema/SqlSchemaGenerator.d.ts +4 -14
- package/schema/SqlSchemaGenerator.js +15 -7
- package/typings.d.ts +4 -1
- package/tsconfig.build.tsbuildinfo +0 -1
|
@@ -1,33 +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';
|
|
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';
|
|
3
3
|
/**
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
6
6
|
export class QueryBuilderHelper {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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;
|
|
15
17
|
constructor(entityName, alias, aliasMap, subQueries, driver, tptAliasMap = {}) {
|
|
16
|
-
this
|
|
17
|
-
this
|
|
18
|
-
this
|
|
19
|
-
this
|
|
20
|
-
this
|
|
21
|
-
this
|
|
22
|
-
this
|
|
23
|
-
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();
|
|
24
26
|
}
|
|
25
27
|
/**
|
|
26
28
|
* For TPT inheritance, finds the correct alias for a property based on which entity owns it.
|
|
27
29
|
* Returns the main alias if not a TPT property or if the property belongs to the main entity.
|
|
28
30
|
*/
|
|
29
31
|
getTPTAliasForProperty(propName, defaultAlias) {
|
|
30
|
-
const meta = this
|
|
32
|
+
const meta = this.#aliasMap[defaultAlias]?.meta ?? this.#metadata.get(this.#entityName);
|
|
31
33
|
if (meta?.inheritanceType !== 'tpt' || !meta.tptParent) {
|
|
32
34
|
return defaultAlias;
|
|
33
35
|
}
|
|
@@ -38,7 +40,7 @@ export class QueryBuilderHelper {
|
|
|
38
40
|
// Walk up the TPT hierarchy to find which parent owns this property
|
|
39
41
|
let parentMeta = meta.tptParent;
|
|
40
42
|
while (parentMeta) {
|
|
41
|
-
const parentAlias = this
|
|
43
|
+
const parentAlias = this.#tptAliasMap[parentMeta.className];
|
|
42
44
|
if (parentAlias && parentMeta.ownProps?.some(p => p.name === propName || p.fieldNames?.includes(propName))) {
|
|
43
45
|
return parentAlias;
|
|
44
46
|
}
|
|
@@ -67,13 +69,13 @@ export class QueryBuilderHelper {
|
|
|
67
69
|
const prop = this.getProperty(f, a);
|
|
68
70
|
const fkIdx2 = prop?.fieldNames.findIndex(name => name === f) ?? -1;
|
|
69
71
|
if (fkIdx2 !== -1) {
|
|
70
|
-
parts.push(this.mapper(a !== this
|
|
72
|
+
parts.push(this.mapper(a !== this.#alias ? `${a}.${prop.fieldNames[fkIdx2]}` : prop.fieldNames[fkIdx2], type, value, alias));
|
|
71
73
|
}
|
|
72
74
|
else if (prop) {
|
|
73
|
-
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)));
|
|
74
76
|
}
|
|
75
77
|
else {
|
|
76
|
-
parts.push(this.mapper(a !== this
|
|
78
|
+
parts.push(this.mapper(a !== this.#alias ? `${a}.${f}` : f, type, value, alias));
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
// flatten the value if we see we are expanding nested composite key
|
|
@@ -87,16 +89,16 @@ export class QueryBuilderHelper {
|
|
|
87
89
|
}
|
|
88
90
|
});
|
|
89
91
|
}
|
|
90
|
-
return raw('(' + parts.map(part => this
|
|
92
|
+
return raw('(' + parts.map(part => this.#platform.quoteIdentifier(part)).join(', ') + ')');
|
|
91
93
|
}
|
|
92
94
|
const [a, f] = this.splitField(field);
|
|
93
95
|
const prop = this.getProperty(f, a);
|
|
94
96
|
// For TPT inheritance, resolve the correct alias for this property
|
|
95
97
|
// Only apply TPT resolution when `a` is an actual table alias (in aliasMap),
|
|
96
98
|
// not when it's an embedded property name like 'profile1.identity.links'
|
|
97
|
-
const isTableAlias = !!this
|
|
98
|
-
const baseAlias = isTableAlias ? a : this
|
|
99
|
-
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;
|
|
100
102
|
const aliasPrefix = isTableNameAliasRequired ? resolvedAlias + '.' : '';
|
|
101
103
|
const fkIdx2 = prop?.fieldNames.findIndex(name => name === f) ?? -1;
|
|
102
104
|
const fkIdx = fkIdx2 === -1 ? 0 : fkIdx2;
|
|
@@ -108,13 +110,13 @@ export class QueryBuilderHelper {
|
|
|
108
110
|
return raw(this.prefix(field, isTableNameAliasRequired));
|
|
109
111
|
}
|
|
110
112
|
if (prop?.formula) {
|
|
111
|
-
const alias2 = this
|
|
113
|
+
const alias2 = this.#platform.quoteIdentifier(a).toString();
|
|
112
114
|
const aliasName = alias === undefined ? prop.fieldNames[0] : alias;
|
|
113
|
-
const as = aliasName === null ? '' : ` as ${this
|
|
114
|
-
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);
|
|
115
117
|
const table = this.createFormulaTable(alias2, meta, schema);
|
|
116
118
|
const columns = meta.createColumnMappingObject(p => this.getTPTAliasForProperty(p.name, a), alias2);
|
|
117
|
-
let value = this
|
|
119
|
+
let value = this.#driver.evaluateFormula(prop.formula, columns, table);
|
|
118
120
|
if (!this.isTableNameAliasRequired(type)) {
|
|
119
121
|
value = value.replaceAll(alias2 + '.', '');
|
|
120
122
|
}
|
|
@@ -125,16 +127,16 @@ export class QueryBuilderHelper {
|
|
|
125
127
|
if (prop.fieldNames.length > 1 && fkIdx !== -1) {
|
|
126
128
|
const fk = prop.targetMeta.getPrimaryProps()[fkIdx];
|
|
127
129
|
const prefixed = this.prefix(field, isTableNameAliasRequired, true, fkIdx);
|
|
128
|
-
valueSQL = fk.customType.convertToJSValueSQL(prefixed, this
|
|
130
|
+
valueSQL = fk.customType.convertToJSValueSQL(prefixed, this.#platform);
|
|
129
131
|
}
|
|
130
132
|
else {
|
|
131
133
|
const prefixed = this.prefix(field, isTableNameAliasRequired, true);
|
|
132
|
-
valueSQL = prop.customType.convertToJSValueSQL(prefixed, this
|
|
134
|
+
valueSQL = prop.customType.convertToJSValueSQL(prefixed, this.#platform);
|
|
133
135
|
}
|
|
134
136
|
if (alias === null) {
|
|
135
137
|
return raw(valueSQL);
|
|
136
138
|
}
|
|
137
|
-
return raw(`${valueSQL} as ${this
|
|
139
|
+
return raw(`${valueSQL} as ${this.#platform.quoteIdentifier(alias ?? prop.fieldNames[fkIdx])}`);
|
|
138
140
|
}
|
|
139
141
|
let ret = this.prefix(field, false, false, fkIdx);
|
|
140
142
|
if (alias) {
|
|
@@ -149,11 +151,11 @@ export class QueryBuilderHelper {
|
|
|
149
151
|
if (Array.isArray(data)) {
|
|
150
152
|
return data.map(d => this.processData(d, convertCustomTypes, true));
|
|
151
153
|
}
|
|
152
|
-
const meta = this
|
|
153
|
-
data = this
|
|
154
|
+
const meta = this.#metadata.find(this.#entityName);
|
|
155
|
+
data = this.#driver.mapDataToFieldNames(data, true, meta?.properties, convertCustomTypes);
|
|
154
156
|
if (!Utils.hasObjectKeys(data) && meta && multi) {
|
|
155
157
|
/* v8 ignore next */
|
|
156
|
-
data[meta.getPrimaryProps()[0].fieldNames[0]] = this
|
|
158
|
+
data[meta.getPrimaryProps()[0].fieldNames[0]] = this.#platform.usesDefaultKeyword() ? raw('default') : undefined;
|
|
157
159
|
}
|
|
158
160
|
return data;
|
|
159
161
|
}
|
|
@@ -163,11 +165,11 @@ export class QueryBuilderHelper {
|
|
|
163
165
|
const joinColumns = prop.owner ? prop.referencedColumnNames : prop2.joinColumns;
|
|
164
166
|
const inverseJoinColumns = prop.referencedColumnNames;
|
|
165
167
|
const primaryKeys = prop.owner ? prop.joinColumns : prop2.referencedColumnNames;
|
|
166
|
-
schema ??= prop.targetMeta?.schema === '*' ? '*' : this
|
|
168
|
+
schema ??= prop.targetMeta?.schema === '*' ? '*' : this.#driver.getSchemaName(prop.targetMeta);
|
|
167
169
|
cond = Utils.merge(cond, prop.where);
|
|
168
170
|
// For inverse side of polymorphic relations, add discriminator condition
|
|
169
171
|
if (!prop.owner && prop2.polymorphic && prop2.discriminatorColumn && prop2.discriminatorMap) {
|
|
170
|
-
const ownerMeta = this
|
|
172
|
+
const ownerMeta = this.#aliasMap[ownerAlias]?.meta ?? this.#metadata.get(this.#entityName);
|
|
171
173
|
const discriminatorValue = QueryHelper.findDiscriminatorValue(prop2.discriminatorMap, ownerMeta.class);
|
|
172
174
|
if (discriminatorValue) {
|
|
173
175
|
cond[`${alias}.${prop2.discriminatorColumn}`] = discriminatorValue;
|
|
@@ -194,7 +196,7 @@ export class QueryBuilderHelper {
|
|
|
194
196
|
ownerAlias,
|
|
195
197
|
alias,
|
|
196
198
|
table: this.getTableName(prop.targetMeta.class),
|
|
197
|
-
schema: prop.targetMeta?.schema === '*' ? '*' : this
|
|
199
|
+
schema: prop.targetMeta?.schema === '*' ? '*' : this.#driver.getSchemaName(prop.targetMeta, { schema }),
|
|
198
200
|
joinColumns: prop.referencedColumnNames,
|
|
199
201
|
// For polymorphic relations, fieldNames includes the discriminator column which is not
|
|
200
202
|
// part of the join condition - use joinColumns (the FK columns only) instead
|
|
@@ -202,7 +204,7 @@ export class QueryBuilderHelper {
|
|
|
202
204
|
};
|
|
203
205
|
}
|
|
204
206
|
joinManyToManyReference(prop, ownerAlias, alias, pivotAlias, type, cond, path, schema) {
|
|
205
|
-
const pivotMeta = this
|
|
207
|
+
const pivotMeta = this.#metadata.find(prop.pivotEntity);
|
|
206
208
|
const ret = {
|
|
207
209
|
[`${ownerAlias}.${prop.name}#${pivotAlias}`]: {
|
|
208
210
|
prop,
|
|
@@ -215,7 +217,7 @@ export class QueryBuilderHelper {
|
|
|
215
217
|
primaryKeys: prop.referencedColumnNames,
|
|
216
218
|
cond: {},
|
|
217
219
|
table: pivotMeta.tableName,
|
|
218
|
-
schema: prop.targetMeta?.schema === '*' ? '*' : this
|
|
220
|
+
schema: prop.targetMeta?.schema === '*' ? '*' : this.#driver.getSchemaName(pivotMeta, { schema }),
|
|
219
221
|
path: path.endsWith('[pivot]') ? path : `${path}[pivot]`,
|
|
220
222
|
},
|
|
221
223
|
};
|
|
@@ -229,16 +231,16 @@ export class QueryBuilderHelper {
|
|
|
229
231
|
ret[`${pivotAlias}.${prop2.name}#${alias}`].schema ??= tmp.length > 1 ? tmp[0] : undefined;
|
|
230
232
|
return ret;
|
|
231
233
|
}
|
|
232
|
-
processJoins(qb, joins, schema) {
|
|
234
|
+
processJoins(qb, joins, schema, schemaOverride) {
|
|
233
235
|
Object.values(joins).forEach(join => {
|
|
234
236
|
if ([JoinType.nestedInnerJoin, JoinType.nestedLeftJoin].includes(join.type)) {
|
|
235
237
|
return;
|
|
236
238
|
}
|
|
237
|
-
const { sql, params } = this.createJoinExpression(join, joins, schema);
|
|
239
|
+
const { sql, params } = this.createJoinExpression(join, joins, schema, schemaOverride);
|
|
238
240
|
qb.join(sql, params);
|
|
239
241
|
});
|
|
240
242
|
}
|
|
241
|
-
createJoinExpression(join, joins, schema) {
|
|
243
|
+
createJoinExpression(join, joins, schema, schemaOverride) {
|
|
242
244
|
let table = join.table;
|
|
243
245
|
const method = {
|
|
244
246
|
[JoinType.nestedInnerJoin]: 'inner join',
|
|
@@ -247,26 +249,26 @@ export class QueryBuilderHelper {
|
|
|
247
249
|
}[join.type] ?? join.type;
|
|
248
250
|
const conditions = [];
|
|
249
251
|
const params = [];
|
|
250
|
-
schema = join.schema
|
|
251
|
-
if (schema && schema !== this
|
|
252
|
+
schema = join.schema === '*' ? schema : (join.schema ?? schemaOverride);
|
|
253
|
+
if (schema && schema !== this.#platform.getDefaultSchemaName()) {
|
|
252
254
|
table = `${schema}.${table}`;
|
|
253
255
|
}
|
|
254
256
|
if (join.prop.name !== '__subquery__') {
|
|
255
257
|
join.primaryKeys.forEach((primaryKey, idx) => {
|
|
256
258
|
const right = `${join.alias}.${join.joinColumns[idx]}`;
|
|
257
259
|
if (join.prop.formula) {
|
|
258
|
-
const quotedAlias = this
|
|
259
|
-
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);
|
|
260
262
|
const table = this.createFormulaTable(quotedAlias, ownerMeta, schema);
|
|
261
263
|
const columns = ownerMeta.createColumnMappingObject(p => this.getTPTAliasForProperty(p.name, join.ownerAlias), quotedAlias);
|
|
262
|
-
const left = this
|
|
263
|
-
conditions.push(`${left} = ${this
|
|
264
|
+
const left = this.#driver.evaluateFormula(join.prop.formula, columns, table);
|
|
265
|
+
conditions.push(`${left} = ${this.#platform.quoteIdentifier(right)}`);
|
|
264
266
|
return;
|
|
265
267
|
}
|
|
266
268
|
const left = join.prop.object && join.prop.fieldNameRaw
|
|
267
269
|
? join.prop.fieldNameRaw.replaceAll(ALIAS_REPLACEMENT, join.ownerAlias)
|
|
268
|
-
: this
|
|
269
|
-
conditions.push(`${left} = ${this
|
|
270
|
+
: this.#platform.quoteIdentifier(`${join.ownerAlias}.${primaryKey}`);
|
|
271
|
+
conditions.push(`${left} = ${this.#platform.quoteIdentifier(right)}`);
|
|
270
272
|
});
|
|
271
273
|
}
|
|
272
274
|
if (join.prop.targetMeta?.root.inheritanceType === 'sti' &&
|
|
@@ -280,31 +282,36 @@ export class QueryBuilderHelper {
|
|
|
280
282
|
if (join.prop.polymorphic && join.prop.discriminatorColumn && join.prop.discriminatorMap) {
|
|
281
283
|
const discriminatorValue = QueryHelper.findDiscriminatorValue(join.prop.discriminatorMap, join.prop.targetMeta.class);
|
|
282
284
|
if (discriminatorValue) {
|
|
283
|
-
const discriminatorCol = this
|
|
285
|
+
const discriminatorCol = this.#platform.quoteIdentifier(`${join.ownerAlias}.${join.prop.discriminatorColumn}`);
|
|
284
286
|
conditions.push(`${discriminatorCol} = ?`);
|
|
285
287
|
params.push(discriminatorValue);
|
|
286
288
|
}
|
|
287
289
|
}
|
|
288
290
|
let sql = method + ' ';
|
|
289
291
|
if (join.nested) {
|
|
290
|
-
|
|
292
|
+
const asKeyword = this.#platform.usesAsKeyword() ? ' as ' : ' ';
|
|
293
|
+
sql += `(${this.#platform.quoteIdentifier(table)}${asKeyword}${this.#platform.quoteIdentifier(join.alias)}`;
|
|
291
294
|
for (const nested of join.nested) {
|
|
292
|
-
const { sql: nestedSql, params: nestedParams } = this.createJoinExpression(nested, joins, schema);
|
|
295
|
+
const { sql: nestedSql, params: nestedParams } = this.createJoinExpression(nested, joins, schema, schemaOverride);
|
|
293
296
|
sql += ' ' + nestedSql;
|
|
294
297
|
params.push(...nestedParams);
|
|
295
298
|
}
|
|
296
299
|
sql += `)`;
|
|
297
300
|
}
|
|
298
301
|
else if (join.subquery) {
|
|
299
|
-
|
|
302
|
+
const asKeyword = this.#platform.usesAsKeyword() ? ' as ' : ' ';
|
|
303
|
+
sql += `(${join.subquery})${asKeyword}${this.#platform.quoteIdentifier(join.alias)}`;
|
|
300
304
|
}
|
|
301
305
|
else {
|
|
302
|
-
sql +=
|
|
306
|
+
sql +=
|
|
307
|
+
this.#platform.quoteIdentifier(table) +
|
|
308
|
+
(this.#platform.usesAsKeyword() ? ' as ' : ' ') +
|
|
309
|
+
this.#platform.quoteIdentifier(join.alias);
|
|
303
310
|
}
|
|
304
|
-
const oldAlias = this
|
|
305
|
-
this
|
|
311
|
+
const oldAlias = this.#alias;
|
|
312
|
+
this.#alias = join.alias;
|
|
306
313
|
const subquery = this._appendQueryCondition(QueryType.SELECT, join.cond);
|
|
307
|
-
this
|
|
314
|
+
this.#alias = oldAlias;
|
|
308
315
|
if (subquery.sql) {
|
|
309
316
|
conditions.push(subquery.sql);
|
|
310
317
|
subquery.params.forEach(p => params.push(p));
|
|
@@ -327,12 +334,12 @@ export class QueryBuilderHelper {
|
|
|
327
334
|
];
|
|
328
335
|
}
|
|
329
336
|
isOneToOneInverse(field, meta) {
|
|
330
|
-
meta ??= this
|
|
337
|
+
meta ??= this.#metadata.find(this.#entityName);
|
|
331
338
|
const prop = meta.properties[field.replace(/:ref$/, '')];
|
|
332
339
|
return prop?.kind === ReferenceKind.ONE_TO_ONE && !prop.owner;
|
|
333
340
|
}
|
|
334
341
|
getTableName(entityName) {
|
|
335
|
-
const meta = this
|
|
342
|
+
const meta = this.#metadata.find(entityName);
|
|
336
343
|
return meta?.tableName ?? Utils.className(entityName);
|
|
337
344
|
}
|
|
338
345
|
/**
|
|
@@ -346,7 +353,7 @@ export class QueryBuilderHelper {
|
|
|
346
353
|
return false;
|
|
347
354
|
}
|
|
348
355
|
// when including the opening bracket/paren we consider it complex
|
|
349
|
-
return
|
|
356
|
+
return !/[{[(]/.exec(re.source);
|
|
350
357
|
}
|
|
351
358
|
getRegExpParam(re) {
|
|
352
359
|
const value = re.source
|
|
@@ -427,17 +434,37 @@ export class QueryBuilderHelper {
|
|
|
427
434
|
const parts = [];
|
|
428
435
|
const params = [];
|
|
429
436
|
if (this.isSimpleRegExp(cond[key])) {
|
|
430
|
-
parts.push(`${this
|
|
437
|
+
parts.push(`${this.#platform.quoteIdentifier(this.mapper(key, type))} like ?`);
|
|
431
438
|
params.push(this.getRegExpParam(cond[key]));
|
|
432
439
|
return { sql: parts.join(' and '), params };
|
|
433
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
|
+
}
|
|
434
461
|
if (Utils.isPlainObject(cond[key]) || cond[key] instanceof RegExp) {
|
|
435
462
|
return this.processObjectSubCondition(cond, key, type);
|
|
436
463
|
}
|
|
437
464
|
const op = cond[key] === null ? 'is' : '=';
|
|
438
465
|
if (Raw.isKnownFragmentSymbol(key)) {
|
|
439
466
|
const raw = Raw.getKnownFragment(key);
|
|
440
|
-
const sql = raw.sql.replaceAll(ALIAS_REPLACEMENT, this
|
|
467
|
+
const sql = raw.sql.replaceAll(ALIAS_REPLACEMENT, this.#alias);
|
|
441
468
|
const value = Utils.asArray(cond[key]);
|
|
442
469
|
params.push(...raw.params);
|
|
443
470
|
if (value.length > 0) {
|
|
@@ -450,13 +477,13 @@ export class QueryBuilderHelper {
|
|
|
450
477
|
return { sql: parts.join(' and '), params };
|
|
451
478
|
}
|
|
452
479
|
const fields = Utils.splitPrimaryKeys(key);
|
|
453
|
-
if (this
|
|
480
|
+
if (this.#subQueries[key]) {
|
|
454
481
|
const val = this.getValueReplacement(fields, cond[key], params, key);
|
|
455
|
-
parts.push(`(${this
|
|
482
|
+
parts.push(`(${this.#subQueries[key]}) ${op} ${val}`);
|
|
456
483
|
return { sql: parts.join(' and '), params };
|
|
457
484
|
}
|
|
458
485
|
const val = this.getValueReplacement(fields, cond[key], params, key);
|
|
459
|
-
parts.push(`${this
|
|
486
|
+
parts.push(`${this.#platform.quoteIdentifier(this.mapper(key, type, cond[key], null))} ${op} ${val}`);
|
|
460
487
|
return { sql: parts.join(' and '), params };
|
|
461
488
|
}
|
|
462
489
|
processObjectSubCondition(cond, key, type) {
|
|
@@ -478,7 +505,7 @@ export class QueryBuilderHelper {
|
|
|
478
505
|
return { sql: parts.join(' and '), params };
|
|
479
506
|
}
|
|
480
507
|
if (value instanceof RegExp) {
|
|
481
|
-
value = this
|
|
508
|
+
value = this.#platform.getRegExpValue(value);
|
|
482
509
|
}
|
|
483
510
|
// operators
|
|
484
511
|
const op = Object.keys(QueryOperator).find(op => op in value);
|
|
@@ -491,17 +518,17 @@ export class QueryBuilderHelper {
|
|
|
491
518
|
const fields = rawField ? [key] : Utils.splitPrimaryKeys(key);
|
|
492
519
|
if (fields.length > 1 && Array.isArray(value[op])) {
|
|
493
520
|
const singleTuple = !value[op].every((v) => Array.isArray(v));
|
|
494
|
-
if (!this
|
|
521
|
+
if (!this.#platform.allowsComparingTuples()) {
|
|
495
522
|
const mapped = fields.map(f => this.mapper(f, type));
|
|
496
523
|
if (op === '$in') {
|
|
497
524
|
const conds = value[op].map(() => {
|
|
498
|
-
return `(${mapped.map(field => `${this
|
|
525
|
+
return `(${mapped.map(field => `${this.#platform.quoteIdentifier(field)} = ?`).join(' and ')})`;
|
|
499
526
|
});
|
|
500
527
|
parts.push(`(${conds.join(' or ')})`);
|
|
501
528
|
params.push(...Utils.flatten(value[op]));
|
|
502
529
|
return { sql: parts.join(' and '), params };
|
|
503
530
|
}
|
|
504
|
-
parts.push(...mapped.map(field => `${this
|
|
531
|
+
parts.push(...mapped.map(field => `${this.#platform.quoteIdentifier(field)} = ?`));
|
|
505
532
|
params.push(...Utils.flatten(value[op]));
|
|
506
533
|
return { sql: parts.join(' and '), params };
|
|
507
534
|
}
|
|
@@ -511,9 +538,9 @@ export class QueryBuilderHelper {
|
|
|
511
538
|
value[op] = raw(sql, tmp);
|
|
512
539
|
}
|
|
513
540
|
}
|
|
514
|
-
if (this
|
|
541
|
+
if (this.#subQueries[key]) {
|
|
515
542
|
const val = this.getValueReplacement(fields, value[op], params, op);
|
|
516
|
-
parts.push(`(${this
|
|
543
|
+
parts.push(`(${this.#subQueries[key]}) ${replacement} ${val}`);
|
|
517
544
|
return { sql: parts.join(' and '), params };
|
|
518
545
|
}
|
|
519
546
|
const [a, f] = rawField ? [] : this.splitField(key);
|
|
@@ -526,7 +553,7 @@ export class QueryBuilderHelper {
|
|
|
526
553
|
if (!prop) {
|
|
527
554
|
throw new Error(`Cannot use $fulltext operator on ${String(key)}, property not found`);
|
|
528
555
|
}
|
|
529
|
-
const { sql, params: params2 } = raw(this
|
|
556
|
+
const { sql, params: params2 } = raw(this.#platform.getFullTextWhereClause(prop), {
|
|
530
557
|
column: this.mapper(key, type, undefined, null),
|
|
531
558
|
query: value[op],
|
|
532
559
|
});
|
|
@@ -536,6 +563,12 @@ export class QueryBuilderHelper {
|
|
|
536
563
|
else if (['$in', '$nin'].includes(op) && Array.isArray(value[op]) && value[op].length === 0) {
|
|
537
564
|
parts.push(`1 = ${op === '$in' ? 0 : 1}`);
|
|
538
565
|
}
|
|
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
|
+
}
|
|
539
572
|
else if (value[op] instanceof Raw || typeof value[op]?.toRaw === 'function') {
|
|
540
573
|
const query = value[op] instanceof Raw ? value[op] : value[op].toRaw();
|
|
541
574
|
const mappedKey = this.mapper(key, type, query, null);
|
|
@@ -543,13 +576,13 @@ export class QueryBuilderHelper {
|
|
|
543
576
|
if (['$in', '$nin'].includes(op)) {
|
|
544
577
|
sql = `(${sql})`;
|
|
545
578
|
}
|
|
546
|
-
parts.push(`${this
|
|
579
|
+
parts.push(`${this.#platform.quoteIdentifier(mappedKey)} ${replacement} ${sql}`);
|
|
547
580
|
params.push(...query.params);
|
|
548
581
|
}
|
|
549
582
|
else {
|
|
550
583
|
const mappedKey = this.mapper(key, type, value[op], null);
|
|
551
584
|
const val = this.getValueReplacement(fields, value[op], params, op, prop);
|
|
552
|
-
parts.push(`${this
|
|
585
|
+
parts.push(`${this.#platform.quoteIdentifier(mappedKey)} ${replacement} ${val}`);
|
|
553
586
|
}
|
|
554
587
|
return { sql: parts.join(' and '), params };
|
|
555
588
|
}
|
|
@@ -564,7 +597,7 @@ export class QueryBuilderHelper {
|
|
|
564
597
|
return `(${tmp.join(', ')})`;
|
|
565
598
|
}
|
|
566
599
|
if (prop?.customType instanceof ArrayType) {
|
|
567
|
-
const item = prop.customType.convertToDatabaseValue(value, this
|
|
600
|
+
const item = prop.customType.convertToDatabaseValue(value, this.#platform, {
|
|
568
601
|
fromQuery: true,
|
|
569
602
|
key,
|
|
570
603
|
mode: 'query',
|
|
@@ -592,7 +625,7 @@ export class QueryBuilderHelper {
|
|
|
592
625
|
replacement = op === '$eq' ? 'is' : 'is not';
|
|
593
626
|
}
|
|
594
627
|
if (op === '$re') {
|
|
595
|
-
replacement = this
|
|
628
|
+
replacement = this.#platform.getRegExpOperator(value[op], value.$flags);
|
|
596
629
|
}
|
|
597
630
|
if (replacement.includes('?')) {
|
|
598
631
|
replacement = replacement.replaceAll('?', '\\?');
|
|
@@ -637,7 +670,7 @@ export class QueryBuilderHelper {
|
|
|
637
670
|
const order = typeof direction === 'number' ? QueryOrderNumeric[direction] : direction;
|
|
638
671
|
if (Raw.isKnownFragmentSymbol(key)) {
|
|
639
672
|
const raw = Raw.getKnownFragment(key);
|
|
640
|
-
ret.push(...this
|
|
673
|
+
ret.push(...this.#platform.getOrderByExpression(this.#platform.formatQuery(raw.sql, raw.params), order, collation));
|
|
641
674
|
continue;
|
|
642
675
|
}
|
|
643
676
|
for (const f of Utils.splitPrimaryKeys(key)) {
|
|
@@ -651,60 +684,24 @@ export class QueryBuilderHelper {
|
|
|
651
684
|
const rawColumn = typeof column === 'string'
|
|
652
685
|
? column
|
|
653
686
|
.split('.')
|
|
654
|
-
.map(e => this
|
|
687
|
+
.map(e => this.#platform.quoteIdentifier(e))
|
|
655
688
|
.join('.')
|
|
656
689
|
: column;
|
|
657
690
|
const customOrder = prop?.customOrder;
|
|
658
|
-
let colPart = customOrder ? this
|
|
691
|
+
let colPart = customOrder ? this.#platform.generateCustomOrder(rawColumn, customOrder) : rawColumn;
|
|
659
692
|
if (isRaw(colPart)) {
|
|
660
|
-
colPart = this
|
|
693
|
+
colPart = this.#platform.formatQuery(colPart.sql, colPart.params);
|
|
661
694
|
}
|
|
662
695
|
if (Array.isArray(order)) {
|
|
663
696
|
order.forEach(part => ret.push(...this.getQueryOrderFromObject(type, part, populate, collation)));
|
|
664
697
|
}
|
|
665
698
|
else {
|
|
666
|
-
ret.push(...this
|
|
699
|
+
ret.push(...this.#platform.getOrderByExpression(colPart, order, collation));
|
|
667
700
|
}
|
|
668
701
|
}
|
|
669
702
|
}
|
|
670
703
|
return ret;
|
|
671
704
|
}
|
|
672
|
-
finalize(type, qb, meta, data, returning) {
|
|
673
|
-
const usesReturningStatement = this.platform.usesReturningStatement() || this.platform.usesOutputStatement();
|
|
674
|
-
if (!meta || !data || !usesReturningStatement) {
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
// always respect explicit returning hint
|
|
678
|
-
if (returning && returning.length > 0) {
|
|
679
|
-
qb.returning(returning.map(field => this.mapper(field, type)));
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
if (type === QueryType.INSERT) {
|
|
683
|
-
const returningProps = meta.hydrateProps
|
|
684
|
-
.filter(prop => prop.returning || (prop.persist !== false && ((prop.primary && prop.autoincrement) || prop.defaultRaw)))
|
|
685
|
-
.filter(prop => !(prop.name in data));
|
|
686
|
-
if (returningProps.length > 0) {
|
|
687
|
-
qb.returning(Utils.flatten(returningProps.map(prop => prop.fieldNames)));
|
|
688
|
-
}
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
if (type === QueryType.UPDATE) {
|
|
692
|
-
const returningProps = meta.hydrateProps.filter(prop => prop.fieldNames && isRaw(data[prop.fieldNames[0]]));
|
|
693
|
-
if (returningProps.length > 0) {
|
|
694
|
-
const fields = returningProps.flatMap((prop) => {
|
|
695
|
-
if (prop.hasConvertToJSValueSQL) {
|
|
696
|
-
const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
|
|
697
|
-
const sql = prop.customType.convertToJSValueSQL(aliased, this.platform) +
|
|
698
|
-
' as ' +
|
|
699
|
-
this.platform.quoteIdentifier(prop.fieldNames[0]);
|
|
700
|
-
return [raw(sql)];
|
|
701
|
-
}
|
|
702
|
-
return prop.fieldNames;
|
|
703
|
-
});
|
|
704
|
-
qb.returning(fields);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
705
|
splitField(field, greedyAlias = false) {
|
|
709
706
|
const parts = field.split('.');
|
|
710
707
|
const ref = parts[parts.length - 1].split(':')[1];
|
|
@@ -712,7 +709,7 @@ export class QueryBuilderHelper {
|
|
|
712
709
|
parts[parts.length - 1] = parts[parts.length - 1].substring(0, parts[parts.length - 1].indexOf(':'));
|
|
713
710
|
}
|
|
714
711
|
if (parts.length === 1) {
|
|
715
|
-
return [this
|
|
712
|
+
return [this.#alias, parts[0], ref];
|
|
716
713
|
}
|
|
717
714
|
if (greedyAlias) {
|
|
718
715
|
const fromField = parts.pop();
|
|
@@ -724,28 +721,28 @@ export class QueryBuilderHelper {
|
|
|
724
721
|
return [fromAlias, fromField, ref];
|
|
725
722
|
}
|
|
726
723
|
getLockSQL(qb, lockMode, lockTables = [], joinsMap) {
|
|
727
|
-
const meta = this
|
|
724
|
+
const meta = this.#metadata.find(this.#entityName);
|
|
728
725
|
if (lockMode === LockMode.OPTIMISTIC && meta && !meta.versionProperty) {
|
|
729
|
-
throw OptimisticLockError.lockFailed(Utils.className(this
|
|
726
|
+
throw OptimisticLockError.lockFailed(Utils.className(this.#entityName));
|
|
730
727
|
}
|
|
731
728
|
if (lockMode !== LockMode.OPTIMISTIC && lockTables.length === 0 && joinsMap) {
|
|
732
729
|
const joins = Object.values(joinsMap);
|
|
733
730
|
const innerJoins = joins.filter(join => [JoinType.innerJoin, JoinType.innerJoinLateral, JoinType.nestedInnerJoin].includes(join.type));
|
|
734
731
|
if (joins.length > innerJoins.length) {
|
|
735
|
-
lockTables.push(this
|
|
732
|
+
lockTables.push(this.#alias, ...innerJoins.map(join => join.alias));
|
|
736
733
|
}
|
|
737
734
|
}
|
|
738
735
|
qb.lockMode(lockMode, lockTables);
|
|
739
736
|
}
|
|
740
737
|
updateVersionProperty(qb, data) {
|
|
741
|
-
const meta = this
|
|
738
|
+
const meta = this.#metadata.find(this.#entityName);
|
|
742
739
|
if (!meta?.versionProperty || meta.versionProperty in data) {
|
|
743
740
|
return;
|
|
744
741
|
}
|
|
745
742
|
const versionProperty = meta.properties[meta.versionProperty];
|
|
746
|
-
let sql = this
|
|
743
|
+
let sql = this.#platform.quoteIdentifier(versionProperty.fieldNames[0]) + ' + 1';
|
|
747
744
|
if (versionProperty.runtimeType === 'Date') {
|
|
748
|
-
sql = this
|
|
745
|
+
sql = this.#platform.getCurrentTimestampSQL(versionProperty.length);
|
|
749
746
|
}
|
|
750
747
|
qb.update({ [versionProperty.fieldNames[0]]: raw(sql) });
|
|
751
748
|
}
|
|
@@ -753,8 +750,8 @@ export class QueryBuilderHelper {
|
|
|
753
750
|
let ret;
|
|
754
751
|
if (!this.isPrefixed(field)) {
|
|
755
752
|
// For TPT inheritance, resolve the correct alias for this property
|
|
756
|
-
const tptAlias = this.getTPTAliasForProperty(field, this
|
|
757
|
-
const alias = always ? (quote ? tptAlias : this
|
|
753
|
+
const tptAlias = this.getTPTAliasForProperty(field, this.#alias);
|
|
754
|
+
const alias = always ? (quote ? tptAlias : this.#platform.quoteIdentifier(tptAlias)) + '.' : '';
|
|
758
755
|
const fieldName = this.fieldName(field, tptAlias, always, idx);
|
|
759
756
|
if (fieldName instanceof Raw) {
|
|
760
757
|
return fieldName.sql;
|
|
@@ -767,7 +764,7 @@ export class QueryBuilderHelper {
|
|
|
767
764
|
// For TPT inheritance, resolve the correct alias for this property
|
|
768
765
|
// Only apply TPT resolution when `a` is an actual table alias (in aliasMap),
|
|
769
766
|
// not when it's an embedded property name like 'profile1.identity.links'
|
|
770
|
-
const isTableAlias = !!this
|
|
767
|
+
const isTableAlias = !!this.#aliasMap[a];
|
|
771
768
|
const resolvedAlias = isTableAlias ? this.getTPTAliasForProperty(f, a) : a;
|
|
772
769
|
const fieldName = this.fieldName(f, resolvedAlias, always, idx);
|
|
773
770
|
if (fieldName instanceof Raw) {
|
|
@@ -776,7 +773,7 @@ export class QueryBuilderHelper {
|
|
|
776
773
|
ret = resolvedAlias + '.' + fieldName;
|
|
777
774
|
}
|
|
778
775
|
if (quote) {
|
|
779
|
-
return this
|
|
776
|
+
return this.#platform.quoteIdentifier(ret);
|
|
780
777
|
}
|
|
781
778
|
return ret;
|
|
782
779
|
}
|
|
@@ -806,7 +803,7 @@ export class QueryBuilderHelper {
|
|
|
806
803
|
return { sql: `(${parts.join(' or ')})`, params };
|
|
807
804
|
}
|
|
808
805
|
isPrefixed(field) {
|
|
809
|
-
return
|
|
806
|
+
return !!/[\w`"[\]]+\./.exec(field);
|
|
810
807
|
}
|
|
811
808
|
fieldName(field, alias, always, idx = 0) {
|
|
812
809
|
const prop = this.getProperty(field, alias);
|
|
@@ -817,7 +814,7 @@ export class QueryBuilderHelper {
|
|
|
817
814
|
if (!always) {
|
|
818
815
|
return raw(prop.fieldNameRaw
|
|
819
816
|
.replace(new RegExp(ALIAS_REPLACEMENT_RE + '\\.?', 'g'), '')
|
|
820
|
-
.replace(this
|
|
817
|
+
.replace(this.#platform.quoteIdentifier('') + '.', ''));
|
|
821
818
|
}
|
|
822
819
|
if (alias) {
|
|
823
820
|
return raw(prop.fieldNameRaw.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), alias));
|
|
@@ -829,8 +826,8 @@ export class QueryBuilderHelper {
|
|
|
829
826
|
return prop.fieldNames?.[idx] ?? field;
|
|
830
827
|
}
|
|
831
828
|
getProperty(field, alias) {
|
|
832
|
-
const entityName = this
|
|
833
|
-
const meta = this
|
|
829
|
+
const entityName = this.#aliasMap[alias]?.entityName || this.#entityName;
|
|
830
|
+
const meta = this.#metadata.find(entityName);
|
|
834
831
|
// raw table name (e.g. CTE) — no metadata available
|
|
835
832
|
if (!meta) {
|
|
836
833
|
return undefined;
|
|
@@ -852,8 +849,198 @@ export class QueryBuilderHelper {
|
|
|
852
849
|
isTableNameAliasRequired(type) {
|
|
853
850
|
return [QueryType.SELECT, QueryType.COUNT].includes(type);
|
|
854
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
|
+
}
|
|
855
1042
|
processOnConflictCondition(cond, schema) {
|
|
856
|
-
const meta = this
|
|
1043
|
+
const meta = this.#metadata.get(this.#entityName);
|
|
857
1044
|
const tableName = meta.tableName;
|
|
858
1045
|
for (const key of Object.keys(cond)) {
|
|
859
1046
|
const mapped = this.mapper(key, QueryType.UPSERT);
|