@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.
Files changed (59) hide show
  1. package/AbstractSqlConnection.d.ts +5 -4
  2. package/AbstractSqlConnection.js +18 -5
  3. package/AbstractSqlDriver.d.ts +1 -1
  4. package/AbstractSqlDriver.js +39 -10
  5. package/AbstractSqlPlatform.d.ts +34 -0
  6. package/AbstractSqlPlatform.js +47 -3
  7. package/PivotCollectionPersister.d.ts +2 -11
  8. package/PivotCollectionPersister.js +59 -59
  9. package/README.md +5 -4
  10. package/SqlEntityManager.d.ts +1 -1
  11. package/dialects/index.d.ts +1 -0
  12. package/dialects/index.js +1 -0
  13. package/dialects/mysql/BaseMySqlPlatform.d.ts +6 -0
  14. package/dialects/mysql/BaseMySqlPlatform.js +17 -0
  15. package/dialects/mysql/MySqlSchemaHelper.d.ts +1 -1
  16. package/dialects/mysql/MySqlSchemaHelper.js +6 -6
  17. package/dialects/oracledb/OracleDialect.d.ts +78 -0
  18. package/dialects/oracledb/OracleDialect.js +166 -0
  19. package/dialects/oracledb/OracleNativeQueryBuilder.d.ts +19 -0
  20. package/dialects/oracledb/OracleNativeQueryBuilder.js +249 -0
  21. package/dialects/oracledb/index.d.ts +2 -0
  22. package/dialects/oracledb/index.js +2 -0
  23. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +6 -0
  24. package/dialects/postgresql/BasePostgreSqlPlatform.js +12 -8
  25. package/dialects/postgresql/PostgreSqlSchemaHelper.js +13 -13
  26. package/dialects/sqlite/SqlitePlatform.d.ts +1 -0
  27. package/dialects/sqlite/SqlitePlatform.js +3 -0
  28. package/dialects/sqlite/SqliteSchemaHelper.js +12 -8
  29. package/index.d.ts +1 -1
  30. package/index.js +0 -1
  31. package/package.json +3 -3
  32. package/plugin/index.d.ts +1 -14
  33. package/plugin/index.js +13 -13
  34. package/plugin/transformer.d.ts +6 -22
  35. package/plugin/transformer.js +81 -73
  36. package/query/ArrayCriteriaNode.d.ts +1 -1
  37. package/query/CriteriaNodeFactory.js +15 -3
  38. package/query/NativeQueryBuilder.d.ts +3 -3
  39. package/query/NativeQueryBuilder.js +4 -2
  40. package/query/ObjectCriteriaNode.js +4 -4
  41. package/query/QueryBuilder.d.ts +58 -62
  42. package/query/QueryBuilder.js +377 -370
  43. package/query/QueryBuilderHelper.d.ts +14 -11
  44. package/query/QueryBuilderHelper.js +324 -137
  45. package/query/ScalarCriteriaNode.js +3 -1
  46. package/query/enums.d.ts +2 -0
  47. package/query/enums.js +2 -0
  48. package/schema/DatabaseSchema.d.ts +7 -5
  49. package/schema/DatabaseSchema.js +50 -33
  50. package/schema/DatabaseTable.d.ts +8 -6
  51. package/schema/DatabaseTable.js +84 -60
  52. package/schema/SchemaComparator.d.ts +1 -3
  53. package/schema/SchemaComparator.js +22 -20
  54. package/schema/SchemaHelper.d.ts +2 -13
  55. package/schema/SchemaHelper.js +2 -1
  56. package/schema/SqlSchemaGenerator.d.ts +4 -14
  57. package/schema/SqlSchemaGenerator.js +15 -7
  58. package/typings.d.ts +4 -1
  59. 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
- entityName;
8
- alias;
9
- aliasMap;
10
- subQueries;
11
- driver;
12
- tptAliasMap;
13
- platform;
14
- 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;
15
17
  constructor(entityName, alias, aliasMap, subQueries, driver, tptAliasMap = {}) {
16
- this.entityName = entityName;
17
- this.alias = alias;
18
- this.aliasMap = aliasMap;
19
- this.subQueries = subQueries;
20
- this.driver = driver;
21
- this.tptAliasMap = tptAliasMap;
22
- this.platform = this.driver.getPlatform();
23
- 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();
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.aliasMap[defaultAlias]?.meta ?? this.metadata.get(this.entityName);
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.tptAliasMap[parentMeta.className];
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.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));
71
73
  }
72
74
  else if (prop) {
73
- 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)));
74
76
  }
75
77
  else {
76
- 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));
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.platform.quoteIdentifier(part)).join(', ') + ')');
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.aliasMap[a];
98
- const baseAlias = isTableAlias ? a : this.alias;
99
- 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;
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.platform.quoteIdentifier(a).toString();
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.platform.quoteIdentifier(aliasName)}`;
114
- 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);
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.driver.evaluateFormula(prop.formula, columns, table);
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.platform);
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.platform);
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.platform.quoteIdentifier(alias ?? prop.fieldNames[fkIdx])}`);
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.metadata.find(this.entityName);
153
- 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);
154
156
  if (!Utils.hasObjectKeys(data) && meta && multi) {
155
157
  /* v8 ignore next */
156
- 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;
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.driver.getSchemaName(prop.targetMeta);
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.aliasMap[ownerAlias]?.meta ?? this.metadata.get(this.entityName);
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.driver.getSchemaName(prop.targetMeta, { schema }),
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.metadata.find(prop.pivotEntity);
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.driver.getSchemaName(pivotMeta, { schema }),
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 && join.schema !== '*' ? join.schema : schema;
251
- if (schema && schema !== this.platform.getDefaultSchemaName()) {
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.platform.quoteIdentifier(join.ownerAlias).toString();
259
- 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);
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.driver.evaluateFormula(join.prop.formula, columns, table);
263
- 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)}`);
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.platform.quoteIdentifier(`${join.ownerAlias}.${primaryKey}`);
269
- conditions.push(`${left} = ${this.platform.quoteIdentifier(right)}`);
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.platform.quoteIdentifier(`${join.ownerAlias}.${join.prop.discriminatorColumn}`);
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
- 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)}`;
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
- 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)}`;
300
304
  }
301
305
  else {
302
- 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);
303
310
  }
304
- const oldAlias = this.alias;
305
- this.alias = join.alias;
311
+ const oldAlias = this.#alias;
312
+ this.#alias = join.alias;
306
313
  const subquery = this._appendQueryCondition(QueryType.SELECT, join.cond);
307
- this.alias = oldAlias;
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.metadata.find(this.entityName);
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.metadata.find(entityName);
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 !re.source.match(/[{[(]/);
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.platform.quoteIdentifier(this.mapper(key, type))} like ?`);
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.alias);
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.subQueries[key]) {
480
+ if (this.#subQueries[key]) {
454
481
  const val = this.getValueReplacement(fields, cond[key], params, key);
455
- parts.push(`(${this.subQueries[key]}) ${op} ${val}`);
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.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}`);
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.platform.getRegExpValue(value);
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.platform.allowsComparingTuples()) {
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.platform.quoteIdentifier(field)} = ?`).join(' and ')})`;
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.platform.quoteIdentifier(field)} = ?`));
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.subQueries[key]) {
541
+ if (this.#subQueries[key]) {
515
542
  const val = this.getValueReplacement(fields, value[op], params, op);
516
- parts.push(`(${this.subQueries[key]}) ${replacement} ${val}`);
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.platform.getFullTextWhereClause(prop), {
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.platform.quoteIdentifier(mappedKey)} ${replacement} ${sql}`);
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.platform.quoteIdentifier(mappedKey)} ${replacement} ${val}`);
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.platform, {
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.platform.getRegExpOperator(value[op], value.$flags);
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.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));
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.platform.quoteIdentifier(e))
687
+ .map(e => this.#platform.quoteIdentifier(e))
655
688
  .join('.')
656
689
  : column;
657
690
  const customOrder = prop?.customOrder;
658
- let colPart = customOrder ? this.platform.generateCustomOrder(rawColumn, customOrder) : rawColumn;
691
+ let colPart = customOrder ? this.#platform.generateCustomOrder(rawColumn, customOrder) : rawColumn;
659
692
  if (isRaw(colPart)) {
660
- colPart = this.platform.formatQuery(colPart.sql, colPart.params);
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.platform.getOrderByExpression(colPart, order, collation));
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.alias, parts[0], ref];
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.metadata.find(this.entityName);
724
+ const meta = this.#metadata.find(this.#entityName);
728
725
  if (lockMode === LockMode.OPTIMISTIC && meta && !meta.versionProperty) {
729
- throw OptimisticLockError.lockFailed(Utils.className(this.entityName));
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.alias, ...innerJoins.map(join => join.alias));
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.metadata.find(this.entityName);
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.platform.quoteIdentifier(versionProperty.fieldNames[0]) + ' + 1';
743
+ let sql = this.#platform.quoteIdentifier(versionProperty.fieldNames[0]) + ' + 1';
747
744
  if (versionProperty.runtimeType === 'Date') {
748
- sql = this.platform.getCurrentTimestampSQL(versionProperty.length);
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.alias);
757
- 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)) + '.' : '';
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.aliasMap[a];
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.platform.quoteIdentifier(ret);
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 !!field.match(/[\w`"[\]]+\./);
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.platform.quoteIdentifier('') + '.', ''));
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.aliasMap[alias]?.entityName || this.entityName;
833
- const meta = this.metadata.find(entityName);
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.metadata.get(this.entityName);
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);