@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.
Files changed (66) hide show
  1. package/AbstractSqlConnection.d.ts +5 -4
  2. package/AbstractSqlConnection.js +20 -6
  3. package/AbstractSqlDriver.d.ts +19 -13
  4. package/AbstractSqlDriver.js +225 -47
  5. package/AbstractSqlPlatform.d.ts +35 -0
  6. package/AbstractSqlPlatform.js +51 -5
  7. package/PivotCollectionPersister.d.ts +2 -11
  8. package/PivotCollectionPersister.js +59 -59
  9. package/README.md +5 -4
  10. package/SqlEntityManager.d.ts +2 -2
  11. package/SqlEntityManager.js +5 -5
  12. package/dialects/index.d.ts +1 -0
  13. package/dialects/index.js +1 -0
  14. package/dialects/mssql/MsSqlNativeQueryBuilder.d.ts +2 -0
  15. package/dialects/mssql/MsSqlNativeQueryBuilder.js +8 -4
  16. package/dialects/mysql/BaseMySqlPlatform.d.ts +6 -0
  17. package/dialects/mysql/BaseMySqlPlatform.js +18 -2
  18. package/dialects/mysql/MySqlSchemaHelper.d.ts +1 -1
  19. package/dialects/mysql/MySqlSchemaHelper.js +25 -14
  20. package/dialects/oracledb/OracleDialect.d.ts +78 -0
  21. package/dialects/oracledb/OracleDialect.js +166 -0
  22. package/dialects/oracledb/OracleNativeQueryBuilder.d.ts +19 -0
  23. package/dialects/oracledb/OracleNativeQueryBuilder.js +249 -0
  24. package/dialects/oracledb/index.d.ts +2 -0
  25. package/dialects/oracledb/index.js +2 -0
  26. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +6 -0
  27. package/dialects/postgresql/BasePostgreSqlPlatform.js +49 -37
  28. package/dialects/postgresql/PostgreSqlSchemaHelper.js +75 -59
  29. package/dialects/sqlite/BaseSqliteConnection.js +2 -2
  30. package/dialects/sqlite/NodeSqliteDialect.js +3 -1
  31. package/dialects/sqlite/SqlitePlatform.d.ts +1 -0
  32. package/dialects/sqlite/SqlitePlatform.js +7 -1
  33. package/dialects/sqlite/SqliteSchemaHelper.js +23 -17
  34. package/index.d.ts +1 -1
  35. package/index.js +0 -1
  36. package/package.json +30 -30
  37. package/plugin/index.d.ts +1 -14
  38. package/plugin/index.js +13 -13
  39. package/plugin/transformer.d.ts +6 -22
  40. package/plugin/transformer.js +91 -82
  41. package/query/ArrayCriteriaNode.d.ts +1 -1
  42. package/query/CriteriaNode.js +28 -10
  43. package/query/CriteriaNodeFactory.js +20 -4
  44. package/query/NativeQueryBuilder.d.ts +28 -3
  45. package/query/NativeQueryBuilder.js +65 -3
  46. package/query/ObjectCriteriaNode.js +75 -31
  47. package/query/QueryBuilder.d.ts +199 -100
  48. package/query/QueryBuilder.js +544 -358
  49. package/query/QueryBuilderHelper.d.ts +18 -14
  50. package/query/QueryBuilderHelper.js +364 -147
  51. package/query/ScalarCriteriaNode.js +17 -8
  52. package/query/enums.d.ts +2 -0
  53. package/query/enums.js +2 -0
  54. package/query/raw.js +1 -1
  55. package/schema/DatabaseSchema.d.ts +7 -5
  56. package/schema/DatabaseSchema.js +68 -45
  57. package/schema/DatabaseTable.d.ts +8 -6
  58. package/schema/DatabaseTable.js +191 -107
  59. package/schema/SchemaComparator.d.ts +1 -3
  60. package/schema/SchemaComparator.js +76 -50
  61. package/schema/SchemaHelper.d.ts +2 -13
  62. package/schema/SchemaHelper.js +30 -9
  63. package/schema/SqlSchemaGenerator.d.ts +4 -14
  64. package/schema/SqlSchemaGenerator.js +26 -12
  65. package/typings.d.ts +10 -5
  66. 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
- entityName;
9
- alias;
10
- aliasMap;
11
- subQueries;
12
- driver;
13
- tptAliasMap;
14
- platform;
15
- metadata;
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.entityName = entityName;
18
- this.alias = alias;
19
- this.aliasMap = aliasMap;
20
- this.subQueries = subQueries;
21
- this.driver = driver;
22
- this.tptAliasMap = tptAliasMap;
23
- this.platform = this.driver.getPlatform();
24
- this.metadata = this.driver.getMetadata();
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.aliasMap[defaultAlias]?.meta ?? this.metadata.get(this.entityName);
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.tptAliasMap[parentMeta.className];
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.alias ? `${a}.${prop.fieldNames[fkIdx2]}` : prop.fieldNames[fkIdx2], type, value, alias));
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.alias ? `${a}.${f}` : f, type, value, alias)));
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.alias ? `${a}.${f}` : f, type, value, alias));
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.platform.quoteIdentifier(part)).join(', ') + ')');
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.aliasMap[a];
99
- const baseAlias = isTableAlias ? a : this.alias;
100
- const resolvedAlias = isTableAlias ? this.getTPTAliasForProperty(prop?.name ?? f, a) : this.alias;
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.platform.quoteIdentifier(a).toString();
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.platform.quoteIdentifier(aliasName)}`;
115
- const meta = this.aliasMap[a]?.meta ?? this.metadata.get(this.entityName);
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.driver.evaluateFormula(prop.formula, columns, table);
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.platform);
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.platform);
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.platform.quoteIdentifier(alias ?? prop.fieldNames[fkIdx])}`);
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.metadata.find(this.entityName);
154
- data = this.driver.mapDataToFieldNames(data, true, meta?.properties, convertCustomTypes);
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.platform.usesDefaultKeyword() ? raw('default') : undefined;
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.driver.getSchemaName(prop.targetMeta);
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.aliasMap[ownerAlias]?.meta ?? this.metadata.get(this.entityName);
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, type, cond, ownerAlias, alias, table, schema,
179
- joinColumns, inverseJoinColumns, primaryKeys,
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, type, cond, ownerAlias, alias,
193
+ prop,
194
+ type,
195
+ cond,
196
+ ownerAlias,
197
+ alias,
185
198
  table: this.getTableName(prop.targetMeta.class),
186
- schema: prop.targetMeta?.schema === '*' ? '*' : this.driver.getSchemaName(prop.targetMeta, { schema }),
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.metadata.find(prop.pivotEntity);
207
+ const pivotMeta = this.#metadata.find(prop.pivotEntity);
195
208
  const ret = {
196
209
  [`${ownerAlias}.${prop.name}#${pivotAlias}`]: {
197
- prop, type, ownerAlias,
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.driver.getSchemaName(pivotMeta, { schema }),
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 && join.schema !== '*' ? join.schema : schema;
238
- if (schema && schema !== this.platform.getDefaultSchemaName()) {
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.platform.quoteIdentifier(join.ownerAlias).toString();
246
- const ownerMeta = this.aliasMap[join.ownerAlias]?.meta ?? this.metadata.get(this.entityName);
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.driver.evaluateFormula(join.prop.formula, columns, table);
250
- conditions.push(`${left} = ${this.platform.quoteIdentifier(right)}`);
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.platform.quoteIdentifier(`${join.ownerAlias}.${primaryKey}`);
256
- conditions.push(`${left} = ${this.platform.quoteIdentifier(right)}`);
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' && join.prop.targetMeta?.discriminatorValue && !join.path?.endsWith('[pivot]')) {
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.platform.quoteIdentifier(`${join.ownerAlias}.${join.prop.discriminatorColumn}`);
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
- sql += `(${this.platform.quoteIdentifier(table)} as ${this.platform.quoteIdentifier(join.alias)}`;
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
- sql += `(${join.subquery}) as ${this.platform.quoteIdentifier(join.alias)}`;
302
+ const asKeyword = this.#platform.usesAsKeyword() ? ' as ' : ' ';
303
+ sql += `(${join.subquery})${asKeyword}${this.#platform.quoteIdentifier(join.alias)}`;
285
304
  }
286
305
  else {
287
- sql += `${this.platform.quoteIdentifier(table)} as ${this.platform.quoteIdentifier(join.alias)}`;
306
+ sql +=
307
+ this.#platform.quoteIdentifier(table) +
308
+ (this.#platform.usesAsKeyword() ? ' as ' : ' ') +
309
+ this.#platform.quoteIdentifier(join.alias);
288
310
  }
289
- const oldAlias = this.alias;
290
- this.alias = join.alias;
311
+ const oldAlias = this.#alias;
312
+ this.#alias = join.alias;
291
313
  const subquery = this._appendQueryCondition(QueryType.SELECT, join.cond);
292
- this.alias = oldAlias;
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.metadata.find(this.entityName);
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.metadata.find(entityName);
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 !re.source.match(/[{[(]/);
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.platform.quoteIdentifier(this.mapper(key, type))} like ?`);
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.alias);
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.subQueries[key]) {
480
+ if (this.#subQueries[key]) {
439
481
  const val = this.getValueReplacement(fields, cond[key], params, key);
440
- parts.push(`(${this.subQueries[key]}) ${op} ${val}`);
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.platform.quoteIdentifier(this.mapper(key, type, cond[key], null))} ${op} ${val}`);
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 ({ [key]: { [subKey]: subValue } });
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.platform.getRegExpValue(value);
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.platform.allowsComparingTuples()) {
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.platform.quoteIdentifier(field)} = ?`).join(' and ')})`;
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.platform.quoteIdentifier(field)} = ?`));
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.subQueries[key]) {
541
+ if (this.#subQueries[key]) {
500
542
  const val = this.getValueReplacement(fields, value[op], params, op);
501
- parts.push(`(${this.subQueries[key]}) ${replacement} ${val}`);
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.platform.getFullTextWhereClause(prop), {
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 (value[op] instanceof Raw || value[op] instanceof NativeQueryBuilder) {
525
- const query = value[op] instanceof NativeQueryBuilder ? value[op].toRaw() : value[op];
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.platform.quoteIdentifier(mappedKey)} ${replacement} ${sql}`);
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.platform.quoteIdentifier(mappedKey)} ${replacement} ${val}`);
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.platform, { fromQuery: true, key, mode: 'query' });
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.platform.getRegExpOperator(value[op], value.$flags);
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.platform.getOrderByExpression(this.platform.formatQuery(raw.sql, raw.params), order, collation));
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' ? column.split('.').map(e => this.platform.quoteIdentifier(e)).join('.') : column;
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.platform.formatQuery(colPart.sql, colPart.params);
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.platform.getOrderByExpression(colPart, order, collation));
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.alias, parts[0], ref];
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.metadata.find(this.entityName);
724
+ const meta = this.#metadata.find(this.#entityName);
704
725
  if (lockMode === LockMode.OPTIMISTIC && meta && !meta.versionProperty) {
705
- throw OptimisticLockError.lockFailed(Utils.className(this.entityName));
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.alias, ...innerJoins.map(join => join.alias));
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.metadata.find(this.entityName);
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.platform.quoteIdentifier(versionProperty.fieldNames[0]) + ' + 1';
743
+ let sql = this.#platform.quoteIdentifier(versionProperty.fieldNames[0]) + ' + 1';
723
744
  if (versionProperty.runtimeType === 'Date') {
724
- sql = this.platform.getCurrentTimestampSQL(versionProperty.length);
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.alias);
733
- const alias = always ? (quote ? tptAlias : this.platform.quoteIdentifier(tptAlias)) + '.' : '';
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.aliasMap[a];
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.platform.quoteIdentifier(ret);
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) || Utils.getObjectKeysSize(val) === 1 || Object.keys(val).every(k => !Utils.isOperator(k));
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 !!field.match(/[\w`"[\]]+\./);
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.platform.quoteIdentifier('') + '.', ''));
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.aliasMap[alias]?.entityName || this.entityName;
807
- const meta = this.metadata.get(entityName);
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.metadata.get(this.entityName);
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);