@mikro-orm/sql 7.0.10 → 7.0.11-dev.0

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