@mikro-orm/sql 7.0.3-dev.8 → 7.0.3

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