@mikro-orm/sql 7.0.15-dev.9 → 7.0.15

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 (89) 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 +2100 -1972
  5. package/AbstractSqlPlatform.d.ts +86 -76
  6. package/AbstractSqlPlatform.js +169 -167
  7. package/PivotCollectionPersister.d.ts +33 -15
  8. package/PivotCollectionPersister.js +158 -160
  9. package/README.md +1 -1
  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/SqlMikroORM.d.ts +49 -8
  15. package/SqlMikroORM.js +8 -8
  16. package/dialects/mssql/MsSqlNativeQueryBuilder.d.ts +12 -12
  17. package/dialects/mssql/MsSqlNativeQueryBuilder.js +199 -201
  18. package/dialects/mysql/BaseMySqlPlatform.d.ts +65 -46
  19. package/dialects/mysql/BaseMySqlPlatform.js +137 -134
  20. package/dialects/mysql/MySqlExceptionConverter.d.ts +6 -6
  21. package/dialects/mysql/MySqlExceptionConverter.js +91 -77
  22. package/dialects/mysql/MySqlNativeQueryBuilder.d.ts +3 -3
  23. package/dialects/mysql/MySqlNativeQueryBuilder.js +66 -69
  24. package/dialects/mysql/MySqlSchemaHelper.d.ts +58 -39
  25. package/dialects/mysql/MySqlSchemaHelper.js +327 -319
  26. package/dialects/oracledb/OracleDialect.d.ts +81 -52
  27. package/dialects/oracledb/OracleDialect.js +155 -149
  28. package/dialects/oracledb/OracleNativeQueryBuilder.d.ts +12 -12
  29. package/dialects/oracledb/OracleNativeQueryBuilder.js +239 -243
  30. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +110 -107
  31. package/dialects/postgresql/BasePostgreSqlPlatform.js +370 -369
  32. package/dialects/postgresql/FullTextType.d.ts +10 -6
  33. package/dialects/postgresql/FullTextType.js +51 -51
  34. package/dialects/postgresql/PostgreSqlExceptionConverter.d.ts +5 -5
  35. package/dialects/postgresql/PostgreSqlExceptionConverter.js +55 -43
  36. package/dialects/postgresql/PostgreSqlNativeQueryBuilder.d.ts +1 -1
  37. package/dialects/postgresql/PostgreSqlNativeQueryBuilder.js +4 -4
  38. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +117 -82
  39. package/dialects/postgresql/PostgreSqlSchemaHelper.js +748 -712
  40. package/dialects/sqlite/BaseSqliteConnection.d.ts +3 -5
  41. package/dialects/sqlite/BaseSqliteConnection.js +21 -19
  42. package/dialects/sqlite/NodeSqliteDialect.d.ts +1 -1
  43. package/dialects/sqlite/NodeSqliteDialect.js +23 -23
  44. package/dialects/sqlite/SqliteDriver.d.ts +1 -1
  45. package/dialects/sqlite/SqliteDriver.js +3 -3
  46. package/dialects/sqlite/SqliteExceptionConverter.d.ts +6 -6
  47. package/dialects/sqlite/SqliteExceptionConverter.js +67 -51
  48. package/dialects/sqlite/SqliteNativeQueryBuilder.d.ts +2 -2
  49. package/dialects/sqlite/SqliteNativeQueryBuilder.js +7 -7
  50. package/dialects/sqlite/SqlitePlatform.d.ts +64 -73
  51. package/dialects/sqlite/SqlitePlatform.js +143 -143
  52. package/dialects/sqlite/SqliteSchemaHelper.d.ts +78 -61
  53. package/dialects/sqlite/SqliteSchemaHelper.js +541 -522
  54. package/package.json +3 -3
  55. package/plugin/index.d.ts +42 -35
  56. package/plugin/index.js +43 -36
  57. package/plugin/transformer.d.ts +137 -95
  58. package/plugin/transformer.js +1012 -881
  59. package/query/ArrayCriteriaNode.d.ts +4 -4
  60. package/query/ArrayCriteriaNode.js +18 -18
  61. package/query/CriteriaNode.d.ts +35 -25
  62. package/query/CriteriaNode.js +142 -132
  63. package/query/CriteriaNodeFactory.d.ts +49 -6
  64. package/query/CriteriaNodeFactory.js +97 -94
  65. package/query/NativeQueryBuilder.d.ts +120 -120
  66. package/query/NativeQueryBuilder.js +507 -501
  67. package/query/ObjectCriteriaNode.d.ts +12 -12
  68. package/query/ObjectCriteriaNode.js +298 -282
  69. package/query/QueryBuilder.d.ts +1558 -906
  70. package/query/QueryBuilder.js +2346 -2217
  71. package/query/QueryBuilderHelper.d.ts +153 -72
  72. package/query/QueryBuilderHelper.js +1084 -1032
  73. package/query/ScalarCriteriaNode.d.ts +3 -3
  74. package/query/ScalarCriteriaNode.js +53 -46
  75. package/query/enums.d.ts +14 -14
  76. package/query/enums.js +14 -14
  77. package/query/raw.d.ts +16 -6
  78. package/query/raw.js +10 -10
  79. package/schema/DatabaseSchema.d.ts +74 -50
  80. package/schema/DatabaseSchema.js +359 -331
  81. package/schema/DatabaseTable.d.ts +96 -73
  82. package/schema/DatabaseTable.js +1046 -974
  83. package/schema/SchemaComparator.d.ts +70 -66
  84. package/schema/SchemaComparator.js +790 -765
  85. package/schema/SchemaHelper.d.ts +128 -97
  86. package/schema/SchemaHelper.js +683 -668
  87. package/schema/SqlSchemaGenerator.d.ts +79 -59
  88. package/schema/SqlSchemaGenerator.js +525 -495
  89. package/typings.d.ts +405 -275
@@ -1,892 +1,1023 @@
1
- import { ReferenceKind, isRaw, } from '@mikro-orm/core';
2
- import { AliasNode, ColumnNode, ColumnUpdateNode, OperationNodeTransformer, PrimitiveValueListNode, ReferenceNode, SchemableIdentifierNode, TableNode, ValueListNode, ValueNode, ValuesNode, } from 'kysely';
1
+ import { ReferenceKind, isRaw } from '@mikro-orm/core';
2
+ import {
3
+ AliasNode,
4
+ ColumnNode,
5
+ ColumnUpdateNode,
6
+ IdentifierNode,
7
+ OperationNodeTransformer,
8
+ PrimitiveValueListNode,
9
+ RawNode,
10
+ ReferenceNode,
11
+ SchemableIdentifierNode,
12
+ SelectAllNode,
13
+ SelectionNode,
14
+ TableNode,
15
+ ValueListNode,
16
+ ValueNode,
17
+ ValuesNode,
18
+ } from 'kysely';
19
+ const EXPANDABLE_KINDS = new Set([
20
+ ReferenceKind.SCALAR,
21
+ ReferenceKind.EMBEDDED,
22
+ ReferenceKind.MANY_TO_ONE,
23
+ ReferenceKind.ONE_TO_ONE,
24
+ ]);
3
25
  export class MikroTransformer extends OperationNodeTransformer {
4
- /**
5
- * Context stack to support nested queries (subqueries, CTEs)
6
- * Each level of query scope has its own Map of table aliases/names to EntityMetadata
7
- * Top of stack (highest index) is the current scope
8
- */
9
- #contextStack = [];
10
- /**
11
- * Subquery alias map: maps subquery/CTE alias to its source table metadata
12
- * Used to resolve columns from subqueries/CTEs to their original table definitions
13
- */
14
- #subqueryAliasMap = new Map();
15
- #metadata;
16
- #platform;
17
- /**
18
- * Global map of all entities involved in the query.
19
- * Populated during AST transformation and used for result transformation.
20
- */
21
- #entityMap = new Map();
22
- #em;
23
- #options;
24
- constructor(em, options = {}) {
25
- super();
26
- this.#em = em;
27
- this.#options = options;
28
- this.#metadata = em.getMetadata();
29
- this.#platform = em.getDriver().getPlatform();
30
- }
31
- reset() {
32
- this.#subqueryAliasMap.clear();
33
- this.#entityMap.clear();
34
- }
35
- getOutputEntityMap() {
36
- return this.#entityMap;
37
- }
38
- /** @internal */
39
- getContextStack() {
40
- return this.#contextStack;
41
- }
42
- /** @internal */
43
- getSubqueryAliasMap() {
44
- return this.#subqueryAliasMap;
45
- }
46
- transformSelectQuery(node, queryId) {
47
- // Push a new context for this query scope (starts with inherited parent context)
48
- const currentContext = new Map();
49
- this.#contextStack.push(currentContext);
50
- try {
51
- // Process WITH clause (CTEs) first - they define names available in this scope
52
- if (node.with) {
53
- this.processWithNode(node.with, currentContext);
54
- }
55
- // Process FROM clause - main tables in this scope
56
- if (node.from?.froms) {
57
- for (const from of node.from.froms) {
58
- this.processFromItem(from, currentContext);
59
- }
60
- }
61
- // Process JOINs - additional tables joined into this scope
62
- if (node.joins) {
63
- for (const join of node.joins) {
64
- this.processJoinNode(join, currentContext);
65
- }
66
- }
67
- return super.transformSelectQuery(node, queryId);
68
- }
69
- finally {
70
- // Pop the context when exiting this query scope
71
- this.#contextStack.pop();
72
- }
73
- }
74
- transformInsertQuery(node, queryId) {
75
- const currentContext = new Map();
76
- this.#contextStack.push(currentContext);
77
- try {
78
- let entityMeta;
79
- if (node.into) {
80
- const tableName = this.getTableName(node.into);
81
- if (tableName) {
82
- const meta = this.findEntityMetadata(tableName);
83
- if (meta) {
84
- entityMeta = meta;
85
- currentContext.set(meta.tableName, meta);
86
- this.#entityMap.set(meta.tableName, meta);
87
- }
88
- }
89
- }
90
- const nodeWithHooks = this.#options.processOnCreateHooks && entityMeta ? this.processOnCreateHooks(node, entityMeta) : node;
91
- const nodeWithConvertedValues = this.#options.convertValues && entityMeta ? this.processInsertValues(nodeWithHooks, entityMeta) : nodeWithHooks;
92
- // Handle ON CONFLICT clause
93
- let finalNode = nodeWithConvertedValues;
94
- if (node.onConflict?.updates && entityMeta) {
95
- // Create a temporary UpdateQueryNode to reuse processOnUpdateHooks and processUpdateValues
96
- // We only care about the updates part
97
- const tempUpdateNode = {
98
- kind: 'UpdateQueryNode',
99
- table: node.into, // Dummy table
100
- updates: node.onConflict.updates,
101
- };
102
- const updatesWithHooks = this.#options.processOnUpdateHooks
103
- ? this.processOnUpdateHooks(tempUpdateNode, entityMeta).updates
104
- : node.onConflict.updates;
105
- const tempUpdateNodeWithHooks = {
106
- ...tempUpdateNode,
107
- updates: updatesWithHooks,
108
- };
109
- const updatesWithConvertedValues = this.#options.convertValues
110
- ? this.processUpdateValues(tempUpdateNodeWithHooks, entityMeta).updates
111
- : updatesWithHooks;
112
- if (updatesWithConvertedValues && updatesWithConvertedValues !== node.onConflict.updates) {
113
- // Construct the new OnConflictNode with updated values
114
- finalNode = {
115
- ...finalNode,
116
- onConflict: {
117
- ...node.onConflict,
118
- updates: updatesWithConvertedValues,
119
- },
120
- };
121
- }
122
- }
123
- return super.transformInsertQuery(finalNode, queryId);
124
- }
125
- finally {
126
- this.#contextStack.pop();
127
- }
128
- }
129
- transformUpdateQuery(node, queryId) {
130
- const currentContext = new Map();
131
- this.#contextStack.push(currentContext);
132
- try {
133
- let entityMeta;
134
- if (node.table && TableNode.is(node.table)) {
135
- const tableName = this.getTableName(node.table);
136
- if (tableName) {
137
- const meta = this.findEntityMetadata(tableName);
138
- if (meta) {
139
- entityMeta = meta;
140
- currentContext.set(meta.tableName, meta);
141
- this.#entityMap.set(meta.tableName, meta);
142
- }
143
- }
144
- }
145
- // Process FROM clause in UPDATE queries (for UPDATE with JOIN)
146
- if (node.from) {
147
- for (const fromItem of node.from.froms) {
148
- this.processFromItem(fromItem, currentContext);
149
- }
150
- }
151
- // Also process JOINs in UPDATE queries
152
- if (node.joins) {
153
- for (const join of node.joins) {
154
- this.processJoinNode(join, currentContext);
155
- }
156
- }
157
- const nodeWithHooks = this.#options.processOnUpdateHooks && entityMeta ? this.processOnUpdateHooks(node, entityMeta) : node;
158
- const nodeWithConvertedValues = this.#options.convertValues && entityMeta ? this.processUpdateValues(nodeWithHooks, entityMeta) : nodeWithHooks;
159
- return super.transformUpdateQuery(nodeWithConvertedValues, queryId);
160
- }
161
- finally {
162
- this.#contextStack.pop();
163
- }
164
- }
165
- transformDeleteQuery(node, queryId) {
166
- const currentContext = new Map();
167
- this.#contextStack.push(currentContext);
168
- try {
169
- const froms = node.from?.froms;
170
- if (froms && froms.length > 0) {
171
- const firstFrom = froms[0];
172
- if (TableNode.is(firstFrom)) {
173
- const tableName = this.getTableName(firstFrom);
174
- if (tableName) {
175
- const meta = this.findEntityMetadata(tableName);
176
- if (meta) {
177
- currentContext.set(meta.tableName, meta);
178
- this.#entityMap.set(meta.tableName, meta);
179
- }
180
- }
181
- }
182
- }
183
- // Also process JOINs in DELETE queries
184
- if (node.joins) {
185
- for (const join of node.joins) {
186
- this.processJoinNode(join, currentContext);
187
- }
188
- }
189
- return super.transformDeleteQuery(node, queryId);
190
- }
191
- finally {
192
- this.#contextStack.pop();
193
- }
194
- }
195
- transformMergeQuery(node, queryId) {
196
- const currentContext = new Map();
197
- this.#contextStack.push(currentContext);
198
- try {
199
- return super.transformMergeQuery(node, queryId);
200
- }
201
- finally {
202
- this.#contextStack.pop();
203
- }
204
- }
205
- transformIdentifier(node, queryId) {
206
- node = super.transformIdentifier(node, queryId);
207
- const parent = this.nodeStack[this.nodeStack.length - 2];
208
- // Transform table names when tableNamingStrategy is 'entity'
209
- if (this.#options.tableNamingStrategy === 'entity' && parent && SchemableIdentifierNode.is(parent)) {
210
- const meta = this.findEntityMetadata(node.name);
211
- if (meta) {
212
- return {
213
- ...node,
214
- name: meta.tableName,
215
- };
216
- }
217
- }
218
- // Transform column names when columnNamingStrategy is 'property'
219
- // Support ColumnNode, ColumnUpdateNode, and ReferenceNode (for JOIN conditions)
220
- if (this.#options.columnNamingStrategy === 'property' &&
221
- parent &&
222
- (ColumnNode.is(parent) || ColumnUpdateNode.is(parent) || ReferenceNode.is(parent))) {
223
- const ownerMeta = this.findOwnerEntityInContext();
224
- if (ownerMeta) {
225
- const prop = ownerMeta.properties[node.name];
226
- const fieldName = prop?.fieldNames?.[0];
227
- if (fieldName) {
228
- return {
229
- ...node,
230
- name: fieldName,
231
- };
232
- }
233
- }
234
- }
235
- return node;
236
- }
237
- /**
238
- * Find owner entity metadata for the current identifier in the context stack.
239
- * Supports both aliased and non-aliased table references.
240
- * Searches up the context stack to support correlated subqueries.
241
- * Also checks subquery/CTE aliases to resolve to their source tables.
242
- */
243
- findOwnerEntityInContext() {
244
- // Check if current column has a table reference (e.g., u.firstName)
245
- const reference = this.nodeStack.find(it => ReferenceNode.is(it));
246
- if (reference?.table && TableNode.is(reference.table)) {
247
- const tableName = this.getTableName(reference.table);
248
- if (tableName) {
249
- // First, check in subquery alias map (for CTE/subquery columns)
250
- if (this.#subqueryAliasMap.has(tableName)) {
251
- return this.#subqueryAliasMap.get(tableName);
252
- }
253
- // Find entity metadata to get the actual table name
254
- // Context uses table names (meta.tableName) as keys, not entity names
255
- const entityMeta = this.findEntityMetadata(tableName);
256
- if (entityMeta) {
257
- // Search in context stack using the actual table name
258
- const meta = this.lookupInContextStack(entityMeta.tableName);
259
- if (meta) {
260
- return meta;
261
- }
262
- // Also try with the entity name (for cases where context uses entity name)
263
- const metaByEntityName = this.lookupInContextStack(tableName);
264
- if (metaByEntityName) {
265
- return metaByEntityName;
266
- }
267
- }
268
- else {
269
- // If entity metadata not found, try direct lookup (for CTE/subquery cases)
270
- const meta = this.lookupInContextStack(tableName);
271
- if (meta) {
272
- return meta;
273
- }
274
- }
275
- }
276
- }
277
- // If no explicit table reference, use the first entity in current context
278
- if (this.#contextStack.length > 0) {
279
- const currentContext = this.#contextStack[this.#contextStack.length - 1];
280
- for (const [alias, meta] of currentContext.entries()) {
281
- if (meta) {
282
- return meta;
283
- }
284
- // If the context value is undefined but the alias is in subqueryAliasMap,
285
- // use the mapped metadata (for CTE/subquery cases)
286
- if (!meta && this.#subqueryAliasMap.has(alias)) {
287
- const mappedMeta = this.#subqueryAliasMap.get(alias);
288
- if (mappedMeta) {
289
- return mappedMeta;
290
- }
291
- }
292
- }
293
- }
294
- return undefined;
295
- }
296
- processOnCreateHooks(node, meta) {
297
- if (!node.columns || !node.values || !ValuesNode.is(node.values)) {
298
- return node;
299
- }
300
- const existingProps = new Set();
301
- for (const col of node.columns) {
302
- const prop = this.findProperty(meta, this.normalizeColumnName(col.column));
303
- if (prop) {
304
- existingProps.add(prop.name);
305
- }
306
- }
307
- const missingProps = meta.props.filter(prop => prop.onCreate && !existingProps.has(prop.name));
308
- if (missingProps.length === 0) {
309
- return node;
310
- }
311
- const newColumns = [...node.columns];
312
- for (const prop of missingProps) {
313
- newColumns.push(ColumnNode.create(prop.name));
314
- }
315
- const newRows = node.values.values.map(row => {
316
- const valuesToAdd = missingProps.map(prop => {
317
- const val = prop.onCreate(undefined, this.#em);
318
- return val;
319
- });
320
- if (ValueListNode.is(row)) {
321
- const newValues = [...row.values, ...valuesToAdd.map(v => ValueNode.create(v))];
322
- return ValueListNode.create(newValues);
323
- }
324
- if (PrimitiveValueListNode.is(row)) {
325
- const newValues = [...row.values, ...valuesToAdd];
326
- return PrimitiveValueListNode.create(newValues);
327
- }
328
- return row;
329
- });
330
- return {
331
- ...node,
332
- columns: Object.freeze(newColumns),
333
- values: ValuesNode.create(newRows),
26
+ /**
27
+ * Context stack to support nested queries (subqueries, CTEs)
28
+ * Each level of query scope has its own Map of table aliases/names to EntityMetadata
29
+ * Top of stack (highest index) is the current scope
30
+ */
31
+ #contextStack = [];
32
+ /**
33
+ * Subquery alias map: maps subquery/CTE alias to its source table metadata
34
+ * Used to resolve columns from subqueries/CTEs to their original table definitions
35
+ */
36
+ #subqueryAliasMap = new Map();
37
+ #metadata;
38
+ #platform;
39
+ /**
40
+ * Global map of all entities involved in the query.
41
+ * Populated during AST transformation and used for result transformation.
42
+ */
43
+ #entityMap = new Map();
44
+ #em;
45
+ #options;
46
+ constructor(em, options = {}) {
47
+ super();
48
+ this.#em = em;
49
+ this.#options = options;
50
+ this.#metadata = em.getMetadata();
51
+ this.#platform = em.getDriver().getPlatform();
52
+ }
53
+ reset() {
54
+ this.#subqueryAliasMap.clear();
55
+ this.#entityMap.clear();
56
+ }
57
+ getOutputEntityMap() {
58
+ return this.#entityMap;
59
+ }
60
+ /** @internal */
61
+ getContextStack() {
62
+ return this.#contextStack;
63
+ }
64
+ /** @internal */
65
+ getSubqueryAliasMap() {
66
+ return this.#subqueryAliasMap;
67
+ }
68
+ transformSelectQuery(node, queryId) {
69
+ // Push a new context for this query scope (starts with inherited parent context)
70
+ const currentContext = new Map();
71
+ this.#contextStack.push(currentContext);
72
+ try {
73
+ // Process WITH clause (CTEs) first - they define names available in this scope
74
+ if (node.with) {
75
+ this.processWithNode(node.with, currentContext);
76
+ }
77
+ // Process FROM clause - main tables in this scope
78
+ if (node.from?.froms) {
79
+ for (const from of node.from.froms) {
80
+ this.processFromItem(from, currentContext);
81
+ }
82
+ }
83
+ // Process JOINs - additional tables joined into this scope
84
+ if (node.joins) {
85
+ for (const join of node.joins) {
86
+ this.processJoinNode(join, currentContext);
87
+ }
88
+ }
89
+ const transformed = super.transformSelectQuery(node, queryId);
90
+ if (this.#options.convertValues && transformed.selections?.length) {
91
+ const selections = this.expandSelections(transformed.selections);
92
+ if (selections !== transformed.selections) {
93
+ return { ...transformed, selections };
94
+ }
95
+ }
96
+ return transformed;
97
+ } finally {
98
+ // Pop the context when exiting this query scope
99
+ this.#contextStack.pop();
100
+ }
101
+ }
102
+ transformInsertQuery(node, queryId) {
103
+ const currentContext = new Map();
104
+ this.#contextStack.push(currentContext);
105
+ try {
106
+ let entityMeta;
107
+ if (node.into) {
108
+ const tableName = this.getTableName(node.into);
109
+ if (tableName) {
110
+ const meta = this.findEntityMetadata(tableName);
111
+ if (meta) {
112
+ entityMeta = meta;
113
+ currentContext.set(meta.tableName, meta);
114
+ this.#entityMap.set(meta.tableName, meta);
115
+ }
116
+ }
117
+ }
118
+ const nodeWithHooks =
119
+ this.#options.processOnCreateHooks && entityMeta ? this.processOnCreateHooks(node, entityMeta) : node;
120
+ const nodeWithConvertedValues =
121
+ this.#options.convertValues && entityMeta ? this.processInsertValues(nodeWithHooks, entityMeta) : nodeWithHooks;
122
+ // Handle ON CONFLICT clause
123
+ let finalNode = nodeWithConvertedValues;
124
+ if (node.onConflict?.updates && entityMeta) {
125
+ // Create a temporary UpdateQueryNode to reuse processOnUpdateHooks and processUpdateValues
126
+ // We only care about the updates part
127
+ const tempUpdateNode = {
128
+ kind: 'UpdateQueryNode',
129
+ table: node.into, // Dummy table
130
+ updates: node.onConflict.updates,
334
131
  };
335
- }
336
- processOnUpdateHooks(node, meta) {
337
- if (!node.updates) {
338
- return node;
339
- }
340
- const existingProps = new Set();
341
- for (const update of node.updates) {
342
- if (ColumnNode.is(update.column)) {
343
- const prop = this.findProperty(meta, this.normalizeColumnName(update.column.column));
344
- if (prop) {
345
- existingProps.add(prop.name);
346
- }
347
- }
348
- }
349
- const missingProps = meta.props.filter(prop => prop.onUpdate && !existingProps.has(prop.name));
350
- if (missingProps.length === 0) {
351
- return node;
352
- }
353
- const newUpdates = [...node.updates];
354
- for (const prop of missingProps) {
355
- const val = prop.onUpdate(undefined, this.#em);
356
- newUpdates.push(ColumnUpdateNode.create(ColumnNode.create(prop.name), ValueNode.create(val)));
357
- }
358
- return {
359
- ...node,
360
- updates: Object.freeze(newUpdates),
132
+ const updatesWithHooks = this.#options.processOnUpdateHooks
133
+ ? this.processOnUpdateHooks(tempUpdateNode, entityMeta).updates
134
+ : node.onConflict.updates;
135
+ const tempUpdateNodeWithHooks = {
136
+ ...tempUpdateNode,
137
+ updates: updatesWithHooks,
361
138
  };
362
- }
363
- processInsertValues(node, meta) {
364
- if (!node.columns?.length || !node.values || !ValuesNode.is(node.values)) {
365
- return node;
366
- }
367
- const columnProps = this.mapColumnsToProperties(node.columns, meta);
368
- const shouldConvert = this.shouldConvertValues();
369
- let changed = false;
370
- const convertedRows = node.values.values.map(row => {
371
- if (ValueListNode.is(row)) {
372
- if (row.values.length !== columnProps.length) {
373
- return row;
374
- }
375
- const values = row.values.map((valueNode, idx) => {
376
- if (!ValueNode.is(valueNode)) {
377
- return valueNode;
378
- }
379
- const converted = this.prepareInputValue(columnProps[idx], valueNode.value, shouldConvert);
380
- if (converted === valueNode.value) {
381
- return valueNode;
382
- }
383
- changed = true;
384
- return valueNode.immediate ? ValueNode.createImmediate(converted) : ValueNode.create(converted);
385
- });
386
- return ValueListNode.create(values);
387
- }
388
- if (PrimitiveValueListNode.is(row)) {
389
- if (row.values.length !== columnProps.length) {
390
- return row;
391
- }
392
- const values = row.values.map((value, idx) => {
393
- const converted = this.prepareInputValue(columnProps[idx], value, shouldConvert);
394
- if (converted !== value) {
395
- changed = true;
396
- }
397
- return converted;
398
- });
399
- return PrimitiveValueListNode.create(values);
400
- }
401
- return row;
402
- });
403
- if (!changed) {
404
- return node;
405
- }
139
+ const updatesWithConvertedValues = this.#options.convertValues
140
+ ? this.processUpdateValues(tempUpdateNodeWithHooks, entityMeta).updates
141
+ : updatesWithHooks;
142
+ if (updatesWithConvertedValues && updatesWithConvertedValues !== node.onConflict.updates) {
143
+ // Construct the new OnConflictNode with updated values
144
+ finalNode = {
145
+ ...finalNode,
146
+ onConflict: {
147
+ ...node.onConflict,
148
+ updates: updatesWithConvertedValues,
149
+ },
150
+ };
151
+ }
152
+ }
153
+ return super.transformInsertQuery(finalNode, queryId);
154
+ } finally {
155
+ this.#contextStack.pop();
156
+ }
157
+ }
158
+ transformUpdateQuery(node, queryId) {
159
+ const currentContext = new Map();
160
+ this.#contextStack.push(currentContext);
161
+ try {
162
+ let entityMeta;
163
+ if (node.table && TableNode.is(node.table)) {
164
+ const tableName = this.getTableName(node.table);
165
+ if (tableName) {
166
+ const meta = this.findEntityMetadata(tableName);
167
+ if (meta) {
168
+ entityMeta = meta;
169
+ currentContext.set(meta.tableName, meta);
170
+ this.#entityMap.set(meta.tableName, meta);
171
+ }
172
+ }
173
+ }
174
+ // Process FROM clause in UPDATE queries (for UPDATE with JOIN)
175
+ if (node.from) {
176
+ for (const fromItem of node.from.froms) {
177
+ this.processFromItem(fromItem, currentContext);
178
+ }
179
+ }
180
+ // Also process JOINs in UPDATE queries
181
+ if (node.joins) {
182
+ for (const join of node.joins) {
183
+ this.processJoinNode(join, currentContext);
184
+ }
185
+ }
186
+ const nodeWithHooks =
187
+ this.#options.processOnUpdateHooks && entityMeta ? this.processOnUpdateHooks(node, entityMeta) : node;
188
+ const nodeWithConvertedValues =
189
+ this.#options.convertValues && entityMeta ? this.processUpdateValues(nodeWithHooks, entityMeta) : nodeWithHooks;
190
+ return super.transformUpdateQuery(nodeWithConvertedValues, queryId);
191
+ } finally {
192
+ this.#contextStack.pop();
193
+ }
194
+ }
195
+ transformDeleteQuery(node, queryId) {
196
+ const currentContext = new Map();
197
+ this.#contextStack.push(currentContext);
198
+ try {
199
+ const froms = node.from?.froms;
200
+ if (froms && froms.length > 0) {
201
+ const firstFrom = froms[0];
202
+ if (TableNode.is(firstFrom)) {
203
+ const tableName = this.getTableName(firstFrom);
204
+ if (tableName) {
205
+ const meta = this.findEntityMetadata(tableName);
206
+ if (meta) {
207
+ currentContext.set(meta.tableName, meta);
208
+ this.#entityMap.set(meta.tableName, meta);
209
+ }
210
+ }
211
+ }
212
+ }
213
+ // Also process JOINs in DELETE queries
214
+ if (node.joins) {
215
+ for (const join of node.joins) {
216
+ this.processJoinNode(join, currentContext);
217
+ }
218
+ }
219
+ return super.transformDeleteQuery(node, queryId);
220
+ } finally {
221
+ this.#contextStack.pop();
222
+ }
223
+ }
224
+ transformMergeQuery(node, queryId) {
225
+ const currentContext = new Map();
226
+ this.#contextStack.push(currentContext);
227
+ try {
228
+ return super.transformMergeQuery(node, queryId);
229
+ } finally {
230
+ this.#contextStack.pop();
231
+ }
232
+ }
233
+ transformIdentifier(node, queryId) {
234
+ node = super.transformIdentifier(node, queryId);
235
+ const parent = this.nodeStack[this.nodeStack.length - 2];
236
+ // Transform table names when tableNamingStrategy is 'entity'
237
+ if (this.#options.tableNamingStrategy === 'entity' && parent && SchemableIdentifierNode.is(parent)) {
238
+ const meta = this.findEntityMetadata(node.name);
239
+ if (meta) {
406
240
  return {
407
- ...node,
408
- values: ValuesNode.create(convertedRows),
241
+ ...node,
242
+ name: meta.tableName,
409
243
  };
410
- }
411
- processUpdateValues(node, meta) {
412
- if (!node.updates?.length) {
413
- return node;
414
- }
415
- const shouldConvert = this.shouldConvertValues();
416
- let changed = false;
417
- const updates = node.updates.map(updateNode => {
418
- if (!ValueNode.is(updateNode.value)) {
419
- return updateNode;
420
- }
421
- const columnName = ColumnNode.is(updateNode.column)
422
- ? this.normalizeColumnName(updateNode.column.column)
423
- : undefined;
424
- const property = this.findProperty(meta, columnName);
425
- const converted = this.prepareInputValue(property, updateNode.value.value, shouldConvert);
426
- if (converted === updateNode.value.value) {
427
- return updateNode;
428
- }
244
+ }
245
+ }
246
+ // Transform column names when columnNamingStrategy is 'property'
247
+ // Support ColumnNode, ColumnUpdateNode, and ReferenceNode (for JOIN conditions)
248
+ if (
249
+ this.#options.columnNamingStrategy === 'property' &&
250
+ parent &&
251
+ (ColumnNode.is(parent) || ColumnUpdateNode.is(parent) || ReferenceNode.is(parent))
252
+ ) {
253
+ const ownerMeta = this.findOwnerEntityInContext();
254
+ if (ownerMeta) {
255
+ const prop = ownerMeta.properties[node.name];
256
+ const fieldName = prop?.fieldNames?.[0];
257
+ if (fieldName) {
258
+ return {
259
+ ...node,
260
+ name: fieldName,
261
+ };
262
+ }
263
+ }
264
+ }
265
+ return node;
266
+ }
267
+ /**
268
+ * Find owner entity metadata for the current identifier in the context stack.
269
+ * Supports both aliased and non-aliased table references.
270
+ * Searches up the context stack to support correlated subqueries.
271
+ * Also checks subquery/CTE aliases to resolve to their source tables.
272
+ */
273
+ findOwnerEntityInContext() {
274
+ // Check if current column has a table reference (e.g., u.firstName)
275
+ const reference = this.nodeStack.find(it => ReferenceNode.is(it));
276
+ if (reference?.table && TableNode.is(reference.table)) {
277
+ const tableName = this.getTableName(reference.table);
278
+ if (tableName) {
279
+ // First, check in subquery alias map (for CTE/subquery columns)
280
+ if (this.#subqueryAliasMap.has(tableName)) {
281
+ return this.#subqueryAliasMap.get(tableName);
282
+ }
283
+ // Find entity metadata to get the actual table name
284
+ // Context uses table names (meta.tableName) as keys, not entity names
285
+ const entityMeta = this.findEntityMetadata(tableName);
286
+ if (entityMeta) {
287
+ // Search in context stack using the actual table name
288
+ const meta = this.lookupInContextStack(entityMeta.tableName);
289
+ if (meta) {
290
+ return meta;
291
+ }
292
+ // Also try with the entity name (for cases where context uses entity name)
293
+ const metaByEntityName = this.lookupInContextStack(tableName);
294
+ if (metaByEntityName) {
295
+ return metaByEntityName;
296
+ }
297
+ } else {
298
+ // If entity metadata not found, try direct lookup (for CTE/subquery cases)
299
+ const meta = this.lookupInContextStack(tableName);
300
+ if (meta) {
301
+ return meta;
302
+ }
303
+ }
304
+ }
305
+ }
306
+ // If no explicit table reference, use the first entity in current context
307
+ if (this.#contextStack.length > 0) {
308
+ const currentContext = this.#contextStack[this.#contextStack.length - 1];
309
+ for (const [alias, meta] of currentContext.entries()) {
310
+ if (meta) {
311
+ return meta;
312
+ }
313
+ // If the context value is undefined but the alias is in subqueryAliasMap,
314
+ // use the mapped metadata (for CTE/subquery cases)
315
+ if (!meta && this.#subqueryAliasMap.has(alias)) {
316
+ const mappedMeta = this.#subqueryAliasMap.get(alias);
317
+ if (mappedMeta) {
318
+ return mappedMeta;
319
+ }
320
+ }
321
+ }
322
+ }
323
+ return undefined;
324
+ }
325
+ processOnCreateHooks(node, meta) {
326
+ if (!node.columns || !node.values || !ValuesNode.is(node.values)) {
327
+ return node;
328
+ }
329
+ const existingProps = new Set();
330
+ for (const col of node.columns) {
331
+ const prop = this.findProperty(meta, this.normalizeColumnName(col.column));
332
+ if (prop) {
333
+ existingProps.add(prop.name);
334
+ }
335
+ }
336
+ const missingProps = meta.props.filter(prop => prop.onCreate && !existingProps.has(prop.name));
337
+ if (missingProps.length === 0) {
338
+ return node;
339
+ }
340
+ const newColumns = [...node.columns];
341
+ for (const prop of missingProps) {
342
+ newColumns.push(ColumnNode.create(prop.name));
343
+ }
344
+ const newRows = node.values.values.map(row => {
345
+ const valuesToAdd = missingProps.map(prop => {
346
+ const val = prop.onCreate(undefined, this.#em);
347
+ return val;
348
+ });
349
+ if (ValueListNode.is(row)) {
350
+ const newValues = [...row.values, ...valuesToAdd.map(v => ValueNode.create(v))];
351
+ return ValueListNode.create(newValues);
352
+ }
353
+ if (PrimitiveValueListNode.is(row)) {
354
+ const newValues = [...row.values, ...valuesToAdd];
355
+ return PrimitiveValueListNode.create(newValues);
356
+ }
357
+ return row;
358
+ });
359
+ return {
360
+ ...node,
361
+ columns: Object.freeze(newColumns),
362
+ values: ValuesNode.create(newRows),
363
+ };
364
+ }
365
+ processOnUpdateHooks(node, meta) {
366
+ if (!node.updates) {
367
+ return node;
368
+ }
369
+ const existingProps = new Set();
370
+ for (const update of node.updates) {
371
+ if (ColumnNode.is(update.column)) {
372
+ const prop = this.findProperty(meta, this.normalizeColumnName(update.column.column));
373
+ if (prop) {
374
+ existingProps.add(prop.name);
375
+ }
376
+ }
377
+ }
378
+ const missingProps = meta.props.filter(prop => prop.onUpdate && !existingProps.has(prop.name));
379
+ if (missingProps.length === 0) {
380
+ return node;
381
+ }
382
+ const newUpdates = [...node.updates];
383
+ for (const prop of missingProps) {
384
+ const val = prop.onUpdate(undefined, this.#em);
385
+ newUpdates.push(ColumnUpdateNode.create(ColumnNode.create(prop.name), ValueNode.create(val)));
386
+ }
387
+ return {
388
+ ...node,
389
+ updates: Object.freeze(newUpdates),
390
+ };
391
+ }
392
+ processInsertValues(node, meta) {
393
+ if (!node.columns?.length || !node.values || !ValuesNode.is(node.values)) {
394
+ return node;
395
+ }
396
+ const columnProps = this.mapColumnsToProperties(node.columns, meta);
397
+ const fieldNames = node.columns.map(c => this.normalizeColumnName(c.column));
398
+ // hasConvertToDatabaseValueSQL is set by MetadataDiscovery only when the SQL is non-trivial
399
+ // (i.e. it actually wraps `?`), so a no-op cast on sqlite won't force a row upgrade.
400
+ const needsSqlWrap = columnProps.some(p => p?.hasConvertToDatabaseValueSQL);
401
+ let changed = false;
402
+ const convertedRows = node.values.values.map(row => {
403
+ if (ValueListNode.is(row) && row.values.length === columnProps.length) {
404
+ const values = row.values.map((valueNode, idx) => {
405
+ if (!ValueNode.is(valueNode)) {
406
+ return valueNode;
407
+ }
408
+ const newNode = this.processInputValueNode(columnProps[idx], fieldNames[idx], valueNode);
409
+ if (newNode !== valueNode) {
429
410
  changed = true;
430
- const newValueNode = updateNode.value.immediate
431
- ? ValueNode.createImmediate(converted)
432
- : ValueNode.create(converted);
433
- return {
434
- ...updateNode,
435
- value: newValueNode,
436
- };
411
+ }
412
+ return newNode;
437
413
  });
438
- if (!changed) {
439
- return node;
440
- }
441
- return {
442
- ...node,
443
- updates,
444
- };
445
- }
446
- mapColumnsToProperties(columns, meta) {
447
- return columns.map(column => {
448
- const columnName = this.normalizeColumnName(column.column);
449
- return this.findProperty(meta, columnName);
414
+ return ValueListNode.create(values);
415
+ }
416
+ if (PrimitiveValueListNode.is(row) && row.values.length === columnProps.length) {
417
+ // upgrade to ValueListNode when any column needs SQL-side wrapping, since
418
+ // PrimitiveValueListNode can only hold primitives
419
+ if (needsSqlWrap) {
420
+ changed = true;
421
+ return ValueListNode.create(
422
+ row.values.map((value, idx) => {
423
+ const prop = columnProps[idx];
424
+ const converted = this.prepareInputValue(prop, value, true);
425
+ return this.wrapWrite(prop, fieldNames[idx], ValueNode.create(converted));
426
+ }),
427
+ );
428
+ }
429
+ const values = row.values.map((value, idx) => {
430
+ const converted = this.prepareInputValue(columnProps[idx], value, true);
431
+ if (converted !== value) {
432
+ changed = true;
433
+ }
434
+ return converted;
450
435
  });
451
- }
452
- normalizeColumnName(identifier) {
453
- const name = identifier.name;
454
- if (!name.includes('.')) {
455
- return name;
456
- }
457
- const parts = name.split('.');
458
- return parts[parts.length - 1] ?? name;
459
- }
460
- findProperty(meta, columnName) {
461
- if (!meta || !columnName) {
462
- return undefined;
463
- }
464
- if (meta.properties[columnName]) {
465
- return meta.properties[columnName];
466
- }
467
- return meta.props.find(prop => prop.fieldNames?.includes(columnName));
468
- }
469
- shouldConvertValues() {
470
- return !!this.#options.convertValues;
471
- }
472
- prepareInputValue(prop, value, enabled) {
473
- if (!enabled || !prop || value == null) {
474
- return value;
475
- }
476
- if (typeof value === 'object' && value !== null) {
477
- if (isRaw(value)) {
478
- return value;
479
- }
480
- if ('kind' in value) {
481
- return value;
482
- }
483
- }
484
- if (prop.customType && !isRaw(value)) {
485
- return prop.customType.convertToDatabaseValue(value, this.#platform, {
486
- fromQuery: true,
487
- key: prop.name,
488
- mode: 'query-data',
489
- });
490
- }
491
- if (value instanceof Date) {
492
- return this.#platform.processDateProperty(value);
493
- }
494
- return value;
495
- }
496
- /**
497
- * Look up a table name/alias in the context stack.
498
- * Searches from current scope (top of stack) to parent scopes (bottom).
499
- * This supports correlated subqueries and references to outer query tables.
500
- */
501
- lookupInContextStack(tableNameOrAlias) {
502
- // Search from top of stack (current scope) to bottom (parent scopes)
503
- for (let i = this.#contextStack.length - 1; i >= 0; i--) {
504
- const context = this.#contextStack[i];
505
- if (context.has(tableNameOrAlias)) {
506
- return context.get(tableNameOrAlias);
507
- }
508
- }
509
- return undefined;
510
- }
511
- /**
512
- * Process WITH node (CTE definitions)
513
- */
514
- processWithNode(withNode, context) {
515
- for (const cte of withNode.expressions) {
516
- const cteName = this.getCTEName(cte.name);
517
- if (cteName) {
518
- // CTEs are not entities, so map to undefined
519
- // They will be transformed recursively by transformSelectQuery
520
- context.set(cteName, undefined);
521
- // Also try to extract the source table from the CTE's expression
522
- // This helps resolve columns in subsequent queries that use the CTE
523
- if (cte.expression?.kind === 'SelectQueryNode') {
524
- const sourceMeta = this.extractSourceTableFromSelectQuery(cte.expression);
525
- if (sourceMeta) {
526
- this.#subqueryAliasMap.set(cteName, sourceMeta);
527
- // Add CTE to entityMap so it can be used for result transformation if needed
528
- // (though CTEs usually don't appear in result rows directly, but their columns might)
529
- this.#entityMap.set(cteName, sourceMeta);
530
- }
531
- }
532
- }
533
- }
534
- }
535
- /**
536
- * Extract CTE name from CommonTableExpressionNameNode
537
- */
538
- getCTEName(nameNode) {
539
- if (TableNode.is(nameNode.table)) {
540
- return this.getTableName(nameNode.table);
541
- }
542
- return undefined;
543
- }
544
- /**
545
- * Process a FROM item (can be TableNode or AliasNode)
546
- */
547
- processFromItem(from, // OperationNode type - can be TableNode, AliasNode, or SelectQueryNode
548
- context) {
549
- if (AliasNode.is(from)) {
550
- if (TableNode.is(from.node)) {
551
- // Regular table with alias
552
- const tableName = this.getTableName(from.node);
553
- if (tableName && from.alias) {
554
- const meta = this.findEntityMetadata(tableName);
555
- const aliasName = this.extractAliasName(from.alias);
556
- if (aliasName) {
557
- context.set(aliasName, meta);
558
- if (meta) {
559
- this.#entityMap.set(aliasName, meta);
560
- }
561
- // Also map the alias in subqueryAliasMap if the table name is a CTE
562
- if (this.#subqueryAliasMap.has(tableName)) {
563
- this.#subqueryAliasMap.set(aliasName, this.#subqueryAliasMap.get(tableName));
564
- }
565
- }
566
- }
567
- }
568
- else if (from.node?.kind === 'SelectQueryNode') {
569
- // Subquery with alias
570
- const aliasName = this.extractAliasName(from.alias);
571
- if (aliasName) {
572
- context.set(aliasName, undefined);
573
- // Try to extract the source table from the subquery
574
- const sourceMeta = this.extractSourceTableFromSelectQuery(from.node);
575
- if (sourceMeta) {
576
- this.#subqueryAliasMap.set(aliasName, sourceMeta);
577
- }
578
- }
579
- }
580
- else {
581
- // Other types with alias
582
- const aliasName = this.extractAliasName(from.alias);
583
- if (aliasName) {
584
- context.set(aliasName, undefined);
585
- }
586
- }
587
- }
588
- else if (TableNode.is(from)) {
589
- // Table without alias
590
- const tableName = this.getTableName(from);
591
- if (tableName) {
592
- const meta = this.findEntityMetadata(tableName);
593
- context.set(tableName, meta);
594
- if (meta) {
595
- this.#entityMap.set(tableName, meta);
596
- }
597
- }
598
- }
599
- }
600
- /**
601
- * Process a JOIN node
602
- */
603
- processJoinNode(join, context) {
604
- const joinTable = join.table;
605
- if (AliasNode.is(joinTable)) {
606
- if (TableNode.is(joinTable.node)) {
607
- // Regular table with alias in JOIN
608
- const tableName = this.getTableName(joinTable.node);
609
- if (tableName && joinTable.alias) {
610
- const meta = this.findEntityMetadata(tableName);
611
- const aliasName = this.extractAliasName(joinTable.alias);
612
- if (aliasName) {
613
- context.set(aliasName, meta);
614
- if (meta) {
615
- this.#entityMap.set(aliasName, meta);
616
- }
617
- // Also map the alias in subqueryAliasMap if the table name is a CTE
618
- if (this.#subqueryAliasMap.has(tableName)) {
619
- this.#subqueryAliasMap.set(aliasName, this.#subqueryAliasMap.get(tableName));
620
- }
621
- }
622
- }
623
- }
624
- else if (joinTable.node?.kind === 'SelectQueryNode') {
625
- // Subquery with alias in JOIN
626
- const aliasName = this.extractAliasName(joinTable.alias);
627
- if (aliasName) {
628
- context.set(aliasName, undefined);
629
- // Try to extract the source table from the subquery
630
- const sourceMeta = this.extractSourceTableFromSelectQuery(joinTable.node);
631
- if (sourceMeta) {
632
- this.#subqueryAliasMap.set(aliasName, sourceMeta);
633
- }
634
- }
635
- }
636
- else {
637
- // Other types with alias
638
- const aliasName = this.extractAliasName(joinTable.alias);
639
- if (aliasName) {
640
- context.set(aliasName, undefined);
641
- }
642
- }
643
- }
644
- else if (TableNode.is(joinTable)) {
645
- // Table without alias in JOIN
646
- const tableName = this.getTableName(joinTable);
647
- if (tableName) {
648
- const meta = this.findEntityMetadata(tableName);
649
- // Use table name (meta.tableName) as key to match transformUpdateQuery behavior
650
- if (meta) {
651
- context.set(meta.tableName, meta);
652
- this.#entityMap.set(meta.tableName, meta);
653
- // Also set with entity name for backward compatibility
654
- context.set(tableName, meta);
655
- }
656
- else {
657
- context.set(tableName, undefined);
658
- }
659
- }
660
- }
661
- }
662
- /**
663
- * Extract the primary source table from a SELECT query
664
- * This helps resolve columns from subqueries to their original entity tables
665
- */
666
- extractSourceTableFromSelectQuery(selectQuery) {
667
- if (!selectQuery.from?.froms || selectQuery.from.froms.length === 0) {
668
- return undefined;
669
- }
670
- // Get the first FROM table
671
- const firstFrom = selectQuery.from.froms[0];
672
- let sourceTable;
673
- if (AliasNode.is(firstFrom) && TableNode.is(firstFrom.node)) {
674
- sourceTable = firstFrom.node;
675
- }
676
- else if (TableNode.is(firstFrom)) {
677
- sourceTable = firstFrom;
678
- }
679
- if (sourceTable) {
680
- const tableName = this.getTableName(sourceTable);
681
- if (tableName) {
682
- return this.findEntityMetadata(tableName);
683
- }
684
- }
685
- return undefined;
686
- }
687
- /**
688
- * Extract alias name from an alias node
689
- */
690
- extractAliasName(alias) {
691
- if (typeof alias === 'object' && 'name' in alias) {
692
- return alias.name;
693
- }
436
+ return PrimitiveValueListNode.create(values);
437
+ }
438
+ return row;
439
+ });
440
+ if (!changed) {
441
+ return node;
442
+ }
443
+ return {
444
+ ...node,
445
+ values: ValuesNode.create(convertedRows),
446
+ };
447
+ }
448
+ processUpdateValues(node, meta) {
449
+ if (!node.updates?.length) {
450
+ return node;
451
+ }
452
+ let changed = false;
453
+ const updates = node.updates.map(updateNode => {
454
+ if (!ValueNode.is(updateNode.value)) {
455
+ return updateNode;
456
+ }
457
+ const columnName = ColumnNode.is(updateNode.column)
458
+ ? this.normalizeColumnName(updateNode.column.column)
459
+ : undefined;
460
+ const property = this.findProperty(meta, columnName);
461
+ const newValue = this.processInputValueNode(property, columnName, updateNode.value);
462
+ if (newValue === updateNode.value) {
463
+ return updateNode;
464
+ }
465
+ changed = true;
466
+ return { ...updateNode, value: newValue };
467
+ });
468
+ if (!changed) {
469
+ return node;
470
+ }
471
+ return {
472
+ ...node,
473
+ updates,
474
+ };
475
+ }
476
+ processInputValueNode(prop, fieldName, valueNode) {
477
+ const converted = this.prepareInputValue(prop, valueNode.value, true);
478
+ const newValueNode =
479
+ converted === valueNode.value
480
+ ? valueNode
481
+ : valueNode.immediate
482
+ ? ValueNode.createImmediate(converted)
483
+ : ValueNode.create(converted);
484
+ return this.wrapWrite(prop, fieldName, newValueNode);
485
+ }
486
+ expandSelections(selections) {
487
+ const out = [];
488
+ let changed = false;
489
+ for (const sel of selections) {
490
+ const replaced = this.expandSelection(sel);
491
+ if (replaced) {
492
+ out.push(...replaced);
493
+ changed = true;
494
+ } else {
495
+ out.push(sel);
496
+ }
497
+ }
498
+ return changed ? out : selections;
499
+ }
500
+ expandSelection(sel) {
501
+ const inner = sel.selection;
502
+ if (SelectAllNode.is(inner)) {
503
+ return this.expandStar(this.findOwnerMeta(undefined), undefined, undefined);
504
+ }
505
+ if (!ReferenceNode.is(inner)) {
506
+ return null;
507
+ }
508
+ const table = inner.table;
509
+ const tableName = table ? this.getTableName(table) : undefined;
510
+ const meta = this.findOwnerMeta(tableName);
511
+ if (!meta) {
512
+ return null;
513
+ }
514
+ if (SelectAllNode.is(inner.column)) {
515
+ return this.expandStar(meta, table, tableName);
516
+ }
517
+ const fieldName = inner.column.column.name;
518
+ const prop = this.findProperty(meta, fieldName);
519
+ const ct = prop && this.fieldType(prop, fieldName);
520
+ return ct?.convertToJSValueSQL ? [this.wrapRead(ct, fieldName, tableName)] : null;
521
+ }
522
+ expandStar(meta, table, tableName) {
523
+ if (!meta || !meta.props.some(p => p.hasConvertToJSValueSQL)) {
524
+ return null;
525
+ }
526
+ const out = [];
527
+ for (const prop of meta.props) {
528
+ if (prop.persist === false || !prop.fieldNames?.length || !EXPANDABLE_KINDS.has(prop.kind)) {
529
+ continue;
530
+ }
531
+ for (const fieldName of prop.fieldNames) {
532
+ const ct = this.fieldType(prop, fieldName);
533
+ out.push(
534
+ ct?.convertToJSValueSQL
535
+ ? this.wrapRead(ct, fieldName, tableName)
536
+ : SelectionNode.create(
537
+ table ? ReferenceNode.create(ColumnNode.create(fieldName), table) : ColumnNode.create(fieldName),
538
+ ),
539
+ );
540
+ }
541
+ }
542
+ return out;
543
+ }
544
+ wrapRead(customType, fieldName, tableName) {
545
+ const key = this.#platform.quoteIdentifier(tableName ? `${tableName}.${fieldName}` : fieldName);
546
+ const sql = customType.convertToJSValueSQL(key, this.#platform);
547
+ return SelectionNode.create(AliasNode.create(RawNode.createWithSql(sql), IdentifierNode.create(fieldName)));
548
+ }
549
+ wrapWrite(prop, fieldName, valueNode) {
550
+ if (!prop?.hasConvertToDatabaseValueSQL || !fieldName || valueNode.value == null || isRaw(valueNode.value)) {
551
+ return valueNode;
552
+ }
553
+ const customType = this.fieldType(prop, fieldName);
554
+ if (!customType?.convertToDatabaseValueSQL) {
555
+ return valueNode;
556
+ }
557
+ const fragments = customType.convertToDatabaseValueSQL('?', this.#platform).split('?');
558
+ return RawNode.create(
559
+ fragments,
560
+ fragments.slice(0, -1).map(() => valueNode),
561
+ );
562
+ }
563
+ /** Resolve the customType for a specific field name (handles composite-PK FK fan-out via prop.customTypes[]). */
564
+ fieldType(prop, fieldName) {
565
+ return prop.customType ?? prop.customTypes?.[prop.fieldNames.indexOf(fieldName)];
566
+ }
567
+ findOwnerMeta(name) {
568
+ if (name) {
569
+ return this.lookupInContextStack(name) ?? this.#subqueryAliasMap.get(name) ?? this.findEntityMetadata(name);
570
+ }
571
+ let single;
572
+ for (const meta of this.#contextStack[this.#contextStack.length - 1].values()) {
573
+ if (!meta) {
574
+ continue;
575
+ }
576
+ if (single && single !== meta) {
694
577
  return undefined;
695
- }
696
- /**
697
- * Extract table name from a TableNode
698
- */
699
- getTableName(node) {
700
- if (!node) {
701
- return undefined;
702
- }
703
- if (TableNode.is(node) && SchemableIdentifierNode.is(node.table)) {
704
- const identifier = node.table.identifier;
705
- if (typeof identifier === 'object' && 'name' in identifier) {
706
- return identifier.name;
707
- }
708
- }
709
- return undefined;
710
- }
711
- /**
712
- * Find entity metadata by table name or entity name
713
- */
714
- findEntityMetadata(name) {
715
- const byEntity = this.#metadata.getByClassName(name, false);
716
- if (byEntity) {
717
- return byEntity;
718
- }
719
- const allMetadata = Array.from(this.#metadata);
720
- const byTable = allMetadata.find(m => m.tableName === name);
721
- if (byTable) {
722
- return byTable;
723
- }
724
- return undefined;
725
- }
726
- /**
727
- * Transform result rows by mapping database column names to property names
728
- * This is called for SELECT queries when columnNamingStrategy is 'property'
729
- */
730
- transformResult(rows, entityMap) {
731
- // Only transform if columnNamingStrategy is 'property' or convertValues is true, and we have data
732
- if ((this.#options.columnNamingStrategy !== 'property' && !this.#options.convertValues) ||
733
- !rows ||
734
- rows.length === 0) {
735
- return rows;
736
- }
737
- // If no entities found (e.g. raw query without known tables), return rows as is
738
- if (entityMap.size === 0) {
739
- return rows;
740
- }
741
- // Build a global mapping from database field names to property objects
742
- const fieldToPropertyMap = this.buildGlobalFieldMap(entityMap);
743
- const relationFieldMap = this.buildGlobalRelationFieldMap(entityMap);
744
- // Transform each row
745
- return rows.map(row => this.transformRow(row, fieldToPropertyMap, relationFieldMap));
746
- }
747
- buildGlobalFieldMap(entityMap) {
748
- const map = {};
749
- for (const [alias, meta] of entityMap.entries()) {
750
- Object.assign(map, this.buildFieldToPropertyMap(meta, alias));
751
- }
752
- return map;
753
- }
754
- buildGlobalRelationFieldMap(entityMap) {
755
- const map = {};
756
- for (const [alias, meta] of entityMap.entries()) {
757
- Object.assign(map, this.buildRelationFieldMap(meta, alias));
758
- }
759
- return map;
760
- }
761
- /**
762
- * Build a mapping from database field names to property objects
763
- * Format: { 'field_name': EntityProperty }
764
- */
765
- buildFieldToPropertyMap(meta, alias) {
766
- const map = {};
767
- for (const prop of meta.props) {
768
- if (prop.fieldNames && prop.fieldNames.length > 0) {
769
- for (const fieldName of prop.fieldNames) {
770
- if (!(fieldName in map)) {
771
- map[fieldName] = prop;
772
- }
773
- if (alias) {
774
- const dotted = `${alias}.${fieldName}`;
775
- if (!(dotted in map)) {
776
- map[dotted] = prop;
777
- }
778
- const underscored = `${alias}_${fieldName}`;
779
- if (!(underscored in map)) {
780
- map[underscored] = prop;
781
- }
782
- const doubleUnderscored = `${alias}__${fieldName}`;
783
- if (!(doubleUnderscored in map)) {
784
- map[doubleUnderscored] = prop;
785
- }
786
- }
787
- }
788
- }
789
- if (!(prop.name in map)) {
790
- map[prop.name] = prop;
791
- }
792
- }
793
- return map;
794
- }
795
- /**
796
- * Build a mapping for relation fields
797
- * For ManyToOne relations, we need to map from the foreign key field to the relation property
798
- * Format: { 'foreign_key_field': 'relationPropertyName' }
799
- */
800
- buildRelationFieldMap(meta, alias) {
801
- const map = {};
802
- for (const prop of meta.props) {
803
- // For ManyToOne/OneToOne relations, find the foreign key field
804
- if (prop.kind === ReferenceKind.MANY_TO_ONE || prop.kind === ReferenceKind.ONE_TO_ONE) {
805
- if (prop.fieldNames && prop.fieldNames.length > 0) {
806
- const fieldName = prop.fieldNames[0];
807
- map[fieldName] = prop.name;
808
- if (alias) {
809
- map[`${alias}.${fieldName}`] = prop.name;
810
- map[`${alias}_${fieldName}`] = prop.name;
811
- map[`${alias}__${fieldName}`] = prop.name;
812
- }
813
- }
814
- }
815
- }
816
- return map;
817
- }
818
- /**
819
- * Transform a single row by mapping column names to property names
820
- */
821
- transformRow(row, fieldToPropertyMap, relationFieldMap) {
822
- const transformed = { ...row };
823
- // First pass: map regular fields from fieldName to propertyName and convert values
824
- for (const [fieldName, prop] of Object.entries(fieldToPropertyMap)) {
825
- if (!(fieldName in transformed)) {
826
- continue;
827
- }
828
- const converted = this.prepareOutputValue(prop, transformed[fieldName]);
829
- if (this.#options.columnNamingStrategy === 'property' && prop.name !== fieldName) {
830
- if (!(prop.name in transformed)) {
831
- transformed[prop.name] = converted;
832
- }
833
- else {
834
- transformed[prop.name] = converted;
835
- }
836
- delete transformed[fieldName];
837
- continue;
838
- }
839
- if (this.#options.convertValues) {
840
- transformed[fieldName] = converted;
841
- }
842
- }
843
- // Second pass: handle relation fields
844
- // Only run if columnNamingStrategy is 'property', as we don't want to rename FKs otherwise
845
- if (this.#options.columnNamingStrategy === 'property') {
846
- for (const [fieldName, relationPropertyName] of Object.entries(relationFieldMap)) {
847
- if (fieldName in transformed && !(relationPropertyName in transformed)) {
848
- // Move the foreign key value to the relation property name
849
- transformed[relationPropertyName] = transformed[fieldName];
850
- delete transformed[fieldName];
851
- }
852
- }
853
- }
854
- return transformed;
855
- }
856
- prepareOutputValue(prop, value) {
857
- if (!this.#options.convertValues || !prop || value == null) {
858
- return value;
859
- }
860
- if (prop.customType) {
861
- return prop.customType.convertToJSValue(value, this.#platform);
862
- }
863
- // Aligned with EntityComparator.getResultMapper logic
864
- if (prop.runtimeType === 'boolean') {
865
- // Use !! conversion like EntityComparator: value == null ? value : !!value
866
- return value == null ? value : !!value;
867
- }
868
- if (prop.runtimeType === 'Date' && !this.#platform.isNumericProperty(prop)) {
869
- // Aligned with EntityComparator: exclude numeric timestamp properties
870
- // If already Date instance or null, return as is
871
- if (value == null || value instanceof Date) {
872
- return value;
873
- }
874
- // Handle timezone like EntityComparator.parseDate
875
- const tz = this.#platform.getTimezone();
876
- if (!tz || tz === 'local') {
877
- return this.#platform.parseDate(value);
878
- }
879
- // For non-local timezone, check if value already has timezone info
880
- // Number (timestamp) doesn't need timezone handling, string needs check
881
- if (typeof value === 'number' ||
882
- (typeof value === 'string' && (value.includes('+') || value.lastIndexOf('-') > 10 || value.endsWith('Z')))) {
883
- return this.#platform.parseDate(value);
884
- }
885
- // Append timezone if not present (only for string values)
886
- return this.#platform.parseDate(value + tz);
887
- }
888
- // For all other runtimeTypes (number, string, bigint, Buffer, object, any, etc.)
889
- // EntityComparator just assigns directly without conversion
578
+ }
579
+ single = meta;
580
+ }
581
+ return single;
582
+ }
583
+ mapColumnsToProperties(columns, meta) {
584
+ return columns.map(column => {
585
+ const columnName = this.normalizeColumnName(column.column);
586
+ return this.findProperty(meta, columnName);
587
+ });
588
+ }
589
+ normalizeColumnName(identifier) {
590
+ const name = identifier.name;
591
+ if (!name.includes('.')) {
592
+ return name;
593
+ }
594
+ const parts = name.split('.');
595
+ return parts[parts.length - 1] ?? name;
596
+ }
597
+ findProperty(meta, columnName) {
598
+ if (!meta || !columnName) {
599
+ return undefined;
600
+ }
601
+ if (meta.properties[columnName]) {
602
+ return meta.properties[columnName];
603
+ }
604
+ return meta.props.find(prop => prop.fieldNames?.includes(columnName));
605
+ }
606
+ prepareInputValue(prop, value, enabled) {
607
+ if (!enabled || !prop || value == null) {
608
+ return value;
609
+ }
610
+ if (typeof value === 'object' && value !== null) {
611
+ if (isRaw(value)) {
890
612
  return value;
891
- }
613
+ }
614
+ if ('kind' in value) {
615
+ return value;
616
+ }
617
+ }
618
+ if (prop.customType && !isRaw(value)) {
619
+ return prop.customType.convertToDatabaseValue(value, this.#platform, {
620
+ fromQuery: true,
621
+ key: prop.name,
622
+ mode: 'query-data',
623
+ });
624
+ }
625
+ if (value instanceof Date) {
626
+ return this.#platform.processDateProperty(value);
627
+ }
628
+ return value;
629
+ }
630
+ /**
631
+ * Look up a table name/alias in the context stack.
632
+ * Searches from current scope (top of stack) to parent scopes (bottom).
633
+ * This supports correlated subqueries and references to outer query tables.
634
+ */
635
+ lookupInContextStack(tableNameOrAlias) {
636
+ // Search from top of stack (current scope) to bottom (parent scopes)
637
+ for (let i = this.#contextStack.length - 1; i >= 0; i--) {
638
+ const context = this.#contextStack[i];
639
+ if (context.has(tableNameOrAlias)) {
640
+ return context.get(tableNameOrAlias);
641
+ }
642
+ }
643
+ return undefined;
644
+ }
645
+ /**
646
+ * Process WITH node (CTE definitions)
647
+ */
648
+ processWithNode(withNode, context) {
649
+ for (const cte of withNode.expressions) {
650
+ const cteName = this.getCTEName(cte.name);
651
+ if (cteName) {
652
+ // CTEs are not entities, so map to undefined
653
+ // They will be transformed recursively by transformSelectQuery
654
+ context.set(cteName, undefined);
655
+ // Also try to extract the source table from the CTE's expression
656
+ // This helps resolve columns in subsequent queries that use the CTE
657
+ if (cte.expression?.kind === 'SelectQueryNode') {
658
+ const sourceMeta = this.extractSourceTableFromSelectQuery(cte.expression);
659
+ if (sourceMeta) {
660
+ this.#subqueryAliasMap.set(cteName, sourceMeta);
661
+ // Add CTE to entityMap so it can be used for result transformation if needed
662
+ // (though CTEs usually don't appear in result rows directly, but their columns might)
663
+ this.#entityMap.set(cteName, sourceMeta);
664
+ }
665
+ }
666
+ }
667
+ }
668
+ }
669
+ /**
670
+ * Extract CTE name from CommonTableExpressionNameNode
671
+ */
672
+ getCTEName(nameNode) {
673
+ if (TableNode.is(nameNode.table)) {
674
+ return this.getTableName(nameNode.table);
675
+ }
676
+ return undefined;
677
+ }
678
+ /**
679
+ * Process a FROM item (can be TableNode or AliasNode)
680
+ */
681
+ processFromItem(
682
+ from, // OperationNode type - can be TableNode, AliasNode, or SelectQueryNode
683
+ context,
684
+ ) {
685
+ if (AliasNode.is(from)) {
686
+ if (TableNode.is(from.node)) {
687
+ // Regular table with alias
688
+ const tableName = this.getTableName(from.node);
689
+ if (tableName && from.alias) {
690
+ const meta = this.findEntityMetadata(tableName);
691
+ const aliasName = this.extractAliasName(from.alias);
692
+ if (aliasName) {
693
+ context.set(aliasName, meta);
694
+ if (meta) {
695
+ this.#entityMap.set(aliasName, meta);
696
+ }
697
+ // Also map the alias in subqueryAliasMap if the table name is a CTE
698
+ if (this.#subqueryAliasMap.has(tableName)) {
699
+ this.#subqueryAliasMap.set(aliasName, this.#subqueryAliasMap.get(tableName));
700
+ }
701
+ }
702
+ }
703
+ } else if (from.node?.kind === 'SelectQueryNode') {
704
+ // Subquery with alias
705
+ const aliasName = this.extractAliasName(from.alias);
706
+ if (aliasName) {
707
+ context.set(aliasName, undefined);
708
+ // Try to extract the source table from the subquery
709
+ const sourceMeta = this.extractSourceTableFromSelectQuery(from.node);
710
+ if (sourceMeta) {
711
+ this.#subqueryAliasMap.set(aliasName, sourceMeta);
712
+ }
713
+ }
714
+ } else {
715
+ // Other types with alias
716
+ const aliasName = this.extractAliasName(from.alias);
717
+ if (aliasName) {
718
+ context.set(aliasName, undefined);
719
+ }
720
+ }
721
+ } else if (TableNode.is(from)) {
722
+ // Table without alias
723
+ const tableName = this.getTableName(from);
724
+ if (tableName) {
725
+ const meta = this.findEntityMetadata(tableName);
726
+ context.set(tableName, meta);
727
+ if (meta) {
728
+ this.#entityMap.set(tableName, meta);
729
+ }
730
+ }
731
+ }
732
+ }
733
+ /**
734
+ * Process a JOIN node
735
+ */
736
+ processJoinNode(join, context) {
737
+ const joinTable = join.table;
738
+ if (AliasNode.is(joinTable)) {
739
+ if (TableNode.is(joinTable.node)) {
740
+ // Regular table with alias in JOIN
741
+ const tableName = this.getTableName(joinTable.node);
742
+ if (tableName && joinTable.alias) {
743
+ const meta = this.findEntityMetadata(tableName);
744
+ const aliasName = this.extractAliasName(joinTable.alias);
745
+ if (aliasName) {
746
+ context.set(aliasName, meta);
747
+ if (meta) {
748
+ this.#entityMap.set(aliasName, meta);
749
+ }
750
+ // Also map the alias in subqueryAliasMap if the table name is a CTE
751
+ if (this.#subqueryAliasMap.has(tableName)) {
752
+ this.#subqueryAliasMap.set(aliasName, this.#subqueryAliasMap.get(tableName));
753
+ }
754
+ }
755
+ }
756
+ } else if (joinTable.node?.kind === 'SelectQueryNode') {
757
+ // Subquery with alias in JOIN
758
+ const aliasName = this.extractAliasName(joinTable.alias);
759
+ if (aliasName) {
760
+ context.set(aliasName, undefined);
761
+ // Try to extract the source table from the subquery
762
+ const sourceMeta = this.extractSourceTableFromSelectQuery(joinTable.node);
763
+ if (sourceMeta) {
764
+ this.#subqueryAliasMap.set(aliasName, sourceMeta);
765
+ }
766
+ }
767
+ } else {
768
+ // Other types with alias
769
+ const aliasName = this.extractAliasName(joinTable.alias);
770
+ if (aliasName) {
771
+ context.set(aliasName, undefined);
772
+ }
773
+ }
774
+ } else if (TableNode.is(joinTable)) {
775
+ // Table without alias in JOIN
776
+ const tableName = this.getTableName(joinTable);
777
+ if (tableName) {
778
+ const meta = this.findEntityMetadata(tableName);
779
+ // Use table name (meta.tableName) as key to match transformUpdateQuery behavior
780
+ if (meta) {
781
+ context.set(meta.tableName, meta);
782
+ this.#entityMap.set(meta.tableName, meta);
783
+ // Also set with entity name for backward compatibility
784
+ context.set(tableName, meta);
785
+ } else {
786
+ context.set(tableName, undefined);
787
+ }
788
+ }
789
+ }
790
+ }
791
+ /**
792
+ * Extract the primary source table from a SELECT query
793
+ * This helps resolve columns from subqueries to their original entity tables
794
+ */
795
+ extractSourceTableFromSelectQuery(selectQuery) {
796
+ if (!selectQuery.from?.froms || selectQuery.from.froms.length === 0) {
797
+ return undefined;
798
+ }
799
+ // Get the first FROM table
800
+ const firstFrom = selectQuery.from.froms[0];
801
+ let sourceTable;
802
+ if (AliasNode.is(firstFrom) && TableNode.is(firstFrom.node)) {
803
+ sourceTable = firstFrom.node;
804
+ } else if (TableNode.is(firstFrom)) {
805
+ sourceTable = firstFrom;
806
+ }
807
+ if (sourceTable) {
808
+ const tableName = this.getTableName(sourceTable);
809
+ if (tableName) {
810
+ return this.findEntityMetadata(tableName);
811
+ }
812
+ }
813
+ return undefined;
814
+ }
815
+ /**
816
+ * Extract alias name from an alias node
817
+ */
818
+ extractAliasName(alias) {
819
+ if (typeof alias === 'object' && 'name' in alias) {
820
+ return alias.name;
821
+ }
822
+ return undefined;
823
+ }
824
+ /**
825
+ * Extract table name from a TableNode
826
+ */
827
+ getTableName(node) {
828
+ if (!node) {
829
+ return undefined;
830
+ }
831
+ if (TableNode.is(node) && SchemableIdentifierNode.is(node.table)) {
832
+ const identifier = node.table.identifier;
833
+ if (typeof identifier === 'object' && 'name' in identifier) {
834
+ return identifier.name;
835
+ }
836
+ }
837
+ return undefined;
838
+ }
839
+ /**
840
+ * Find entity metadata by table name or entity name
841
+ */
842
+ findEntityMetadata(name) {
843
+ const byEntity = this.#metadata.getByClassName(name, false);
844
+ if (byEntity) {
845
+ return byEntity;
846
+ }
847
+ const allMetadata = Array.from(this.#metadata);
848
+ const byTable = allMetadata.find(m => m.tableName === name);
849
+ if (byTable) {
850
+ return byTable;
851
+ }
852
+ return undefined;
853
+ }
854
+ /**
855
+ * Transform result rows by mapping database column names to property names
856
+ * This is called for SELECT queries when columnNamingStrategy is 'property'
857
+ */
858
+ transformResult(rows, entityMap) {
859
+ // Only transform if columnNamingStrategy is 'property' or convertValues is true, and we have data
860
+ if (
861
+ (this.#options.columnNamingStrategy !== 'property' && !this.#options.convertValues) ||
862
+ !rows ||
863
+ rows.length === 0
864
+ ) {
865
+ return rows;
866
+ }
867
+ // If no entities found (e.g. raw query without known tables), return rows as is
868
+ if (entityMap.size === 0) {
869
+ return rows;
870
+ }
871
+ // Build a global mapping from database field names to property objects
872
+ const fieldToPropertyMap = this.buildGlobalFieldMap(entityMap);
873
+ const relationFieldMap = this.buildGlobalRelationFieldMap(entityMap);
874
+ // Transform each row
875
+ return rows.map(row => this.transformRow(row, fieldToPropertyMap, relationFieldMap));
876
+ }
877
+ buildGlobalFieldMap(entityMap) {
878
+ const map = {};
879
+ for (const [alias, meta] of entityMap.entries()) {
880
+ Object.assign(map, this.buildFieldToPropertyMap(meta, alias));
881
+ }
882
+ return map;
883
+ }
884
+ buildGlobalRelationFieldMap(entityMap) {
885
+ const map = {};
886
+ for (const [alias, meta] of entityMap.entries()) {
887
+ Object.assign(map, this.buildRelationFieldMap(meta, alias));
888
+ }
889
+ return map;
890
+ }
891
+ /**
892
+ * Build a mapping from database field names to property objects
893
+ * Format: { 'field_name': EntityProperty }
894
+ */
895
+ buildFieldToPropertyMap(meta, alias) {
896
+ const map = {};
897
+ for (const prop of meta.props) {
898
+ if (prop.fieldNames && prop.fieldNames.length > 0) {
899
+ for (const fieldName of prop.fieldNames) {
900
+ if (!(fieldName in map)) {
901
+ map[fieldName] = prop;
902
+ }
903
+ if (alias) {
904
+ const dotted = `${alias}.${fieldName}`;
905
+ if (!(dotted in map)) {
906
+ map[dotted] = prop;
907
+ }
908
+ const underscored = `${alias}_${fieldName}`;
909
+ if (!(underscored in map)) {
910
+ map[underscored] = prop;
911
+ }
912
+ const doubleUnderscored = `${alias}__${fieldName}`;
913
+ if (!(doubleUnderscored in map)) {
914
+ map[doubleUnderscored] = prop;
915
+ }
916
+ }
917
+ }
918
+ }
919
+ if (!(prop.name in map)) {
920
+ map[prop.name] = prop;
921
+ }
922
+ }
923
+ return map;
924
+ }
925
+ /**
926
+ * Build a mapping for relation fields
927
+ * For ManyToOne relations, we need to map from the foreign key field to the relation property
928
+ * Format: { 'foreign_key_field': 'relationPropertyName' }
929
+ */
930
+ buildRelationFieldMap(meta, alias) {
931
+ const map = {};
932
+ for (const prop of meta.props) {
933
+ // For ManyToOne/OneToOne relations, find the foreign key field
934
+ if (prop.kind === ReferenceKind.MANY_TO_ONE || prop.kind === ReferenceKind.ONE_TO_ONE) {
935
+ if (prop.fieldNames && prop.fieldNames.length > 0) {
936
+ const fieldName = prop.fieldNames[0];
937
+ map[fieldName] = prop.name;
938
+ if (alias) {
939
+ map[`${alias}.${fieldName}`] = prop.name;
940
+ map[`${alias}_${fieldName}`] = prop.name;
941
+ map[`${alias}__${fieldName}`] = prop.name;
942
+ }
943
+ }
944
+ }
945
+ }
946
+ return map;
947
+ }
948
+ /**
949
+ * Transform a single row by mapping column names to property names
950
+ */
951
+ transformRow(row, fieldToPropertyMap, relationFieldMap) {
952
+ const transformed = { ...row };
953
+ // First pass: map regular fields from fieldName to propertyName and convert values
954
+ for (const [fieldName, prop] of Object.entries(fieldToPropertyMap)) {
955
+ if (!(fieldName in transformed)) {
956
+ continue;
957
+ }
958
+ const converted = this.prepareOutputValue(prop, transformed[fieldName]);
959
+ if (this.#options.columnNamingStrategy === 'property' && prop.name !== fieldName) {
960
+ if (!(prop.name in transformed)) {
961
+ transformed[prop.name] = converted;
962
+ } else {
963
+ transformed[prop.name] = converted;
964
+ }
965
+ delete transformed[fieldName];
966
+ continue;
967
+ }
968
+ if (this.#options.convertValues) {
969
+ transformed[fieldName] = converted;
970
+ }
971
+ }
972
+ // Second pass: handle relation fields
973
+ // Only run if columnNamingStrategy is 'property', as we don't want to rename FKs otherwise
974
+ if (this.#options.columnNamingStrategy === 'property') {
975
+ for (const [fieldName, relationPropertyName] of Object.entries(relationFieldMap)) {
976
+ if (fieldName in transformed && !(relationPropertyName in transformed)) {
977
+ // Move the foreign key value to the relation property name
978
+ transformed[relationPropertyName] = transformed[fieldName];
979
+ delete transformed[fieldName];
980
+ }
981
+ }
982
+ }
983
+ return transformed;
984
+ }
985
+ prepareOutputValue(prop, value) {
986
+ if (!this.#options.convertValues || !prop || value == null) {
987
+ return value;
988
+ }
989
+ if (prop.customType) {
990
+ return prop.customType.convertToJSValue(value, this.#platform);
991
+ }
992
+ // Aligned with EntityComparator.getResultMapper logic
993
+ if (prop.runtimeType === 'boolean') {
994
+ // Use !! conversion like EntityComparator: value == null ? value : !!value
995
+ return value == null ? value : !!value;
996
+ }
997
+ if (prop.runtimeType === 'Date' && !this.#platform.isNumericProperty(prop)) {
998
+ // Aligned with EntityComparator: exclude numeric timestamp properties
999
+ // If already Date instance or null, return as is
1000
+ if (value == null || value instanceof Date) {
1001
+ return value;
1002
+ }
1003
+ // Handle timezone like EntityComparator.parseDate
1004
+ const tz = this.#platform.getTimezone();
1005
+ if (!tz || tz === 'local') {
1006
+ return this.#platform.parseDate(value);
1007
+ }
1008
+ // For non-local timezone, check if value already has timezone info
1009
+ // Number (timestamp) doesn't need timezone handling, string needs check
1010
+ if (
1011
+ typeof value === 'number' ||
1012
+ (typeof value === 'string' && (value.includes('+') || value.lastIndexOf('-') > 10 || value.endsWith('Z')))
1013
+ ) {
1014
+ return this.#platform.parseDate(value);
1015
+ }
1016
+ // Append timezone if not present (only for string values)
1017
+ return this.#platform.parseDate(value + tz);
1018
+ }
1019
+ // For all other runtimeTypes (number, string, bigint, Buffer, object, any, etc.)
1020
+ // EntityComparator just assigns directly without conversion
1021
+ return value;
1022
+ }
892
1023
  }