@mikro-orm/sql 7.0.4 → 7.0.5-dev.1

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