linkgress-orm 0.1.5 → 0.1.7

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 (46) hide show
  1. package/dist/entity/db-context.d.ts +79 -0
  2. package/dist/entity/db-context.d.ts.map +1 -1
  3. package/dist/entity/db-context.js +128 -1
  4. package/dist/entity/db-context.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +9 -6
  8. package/dist/index.js.map +1 -1
  9. package/dist/query/collection-strategy.interface.d.ts +8 -0
  10. package/dist/query/collection-strategy.interface.d.ts.map +1 -1
  11. package/dist/query/conditions.d.ts +4 -1
  12. package/dist/query/conditions.d.ts.map +1 -1
  13. package/dist/query/conditions.js +40 -11
  14. package/dist/query/conditions.js.map +1 -1
  15. package/dist/query/grouped-query.d.ts.map +1 -1
  16. package/dist/query/grouped-query.js +62 -26
  17. package/dist/query/grouped-query.js.map +1 -1
  18. package/dist/query/query-builder.d.ts +5 -0
  19. package/dist/query/query-builder.d.ts.map +1 -1
  20. package/dist/query/query-builder.js +595 -277
  21. package/dist/query/query-builder.js.map +1 -1
  22. package/dist/query/strategies/cte-collection-strategy.d.ts +5 -0
  23. package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
  24. package/dist/query/strategies/cte-collection-strategy.js +23 -2
  25. package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
  26. package/dist/query/strategies/lateral-collection-strategy.d.ts +5 -0
  27. package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
  28. package/dist/query/strategies/lateral-collection-strategy.js +20 -0
  29. package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
  30. package/dist/query/strategies/temptable-collection-strategy.d.ts +6 -0
  31. package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
  32. package/dist/query/strategies/temptable-collection-strategy.js +42 -2
  33. package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
  34. package/dist/query/subquery.d.ts +19 -2
  35. package/dist/query/subquery.d.ts.map +1 -1
  36. package/dist/query/subquery.js +12 -0
  37. package/dist/query/subquery.js.map +1 -1
  38. package/dist/schema/table-builder.d.ts +20 -1
  39. package/dist/schema/table-builder.d.ts.map +1 -1
  40. package/dist/schema/table-builder.js +11 -2
  41. package/dist/schema/table-builder.js.map +1 -1
  42. package/dist/types/custom-types.d.ts +4 -2
  43. package/dist/types/custom-types.d.ts.map +1 -1
  44. package/dist/types/custom-types.js +6 -4
  45. package/dist/types/custom-types.js.map +1 -1
  46. package/package.json +1 -1
@@ -6,11 +6,28 @@ exports.getRelationEntriesForSchema = getRelationEntriesForSchema;
6
6
  exports.getTargetSchemaForRelation = getTargetSchemaForRelation;
7
7
  exports.createNestedFieldRefProxy = createNestedFieldRefProxy;
8
8
  const conditions_1 = require("./conditions");
9
+ const db_context_1 = require("../entity/db-context");
9
10
  const query_utils_1 = require("./query-utils");
10
11
  const subquery_1 = require("./subquery");
11
12
  const grouped_query_1 = require("./grouped-query");
12
13
  const cte_builder_1 = require("./cte-builder");
13
14
  const collection_strategy_factory_1 = require("./collection-strategy.factory");
15
+ /**
16
+ * Field type categories for optimized result transformation
17
+ * const enum is inlined at compile time for zero runtime overhead
18
+ */
19
+ var FieldType;
20
+ (function (FieldType) {
21
+ FieldType[FieldType["NAVIGATION"] = 0] = "NAVIGATION";
22
+ FieldType[FieldType["COLLECTION_SCALAR"] = 1] = "COLLECTION_SCALAR";
23
+ FieldType[FieldType["COLLECTION_ARRAY"] = 2] = "COLLECTION_ARRAY";
24
+ FieldType[FieldType["COLLECTION_JSON"] = 3] = "COLLECTION_JSON";
25
+ FieldType[FieldType["CTE_AGGREGATION"] = 4] = "CTE_AGGREGATION";
26
+ FieldType[FieldType["SQL_FRAGMENT_MAPPER"] = 5] = "SQL_FRAGMENT_MAPPER";
27
+ FieldType[FieldType["FIELD_REF_MAPPER"] = 6] = "FIELD_REF_MAPPER";
28
+ FieldType[FieldType["FIELD_REF_NO_MAPPER"] = 7] = "FIELD_REF_NO_MAPPER";
29
+ FieldType[FieldType["SIMPLE"] = 8] = "SIMPLE";
30
+ })(FieldType || (FieldType = {}));
14
31
  /**
15
32
  * Performance utility: Get column name map from schema, using cached version if available
16
33
  */
@@ -184,16 +201,36 @@ class QueryBuilder {
184
201
  return this._cachedMockRow;
185
202
  }
186
203
  const mock = {};
204
+ const tableAlias = this.schema.name;
187
205
  // Performance: Use pre-computed column name map if available
188
206
  const columnNameMap = getColumnNameMapForSchema(this.schema);
207
+ // Performance: Lazy-cache FieldRef objects - only create when first accessed
208
+ const fieldRefCache = {};
209
+ // Build a mapper lookup for columns (only when needed)
210
+ const columnMappers = {};
211
+ for (const [colName, colBuilder] of Object.entries(this.schema.columns)) {
212
+ const config = colBuilder.build();
213
+ if (config.mapper) {
214
+ columnMappers[colName] = config.mapper;
215
+ }
216
+ }
189
217
  // Add columns as FieldRef objects - type-safe with property name and database column name
190
218
  for (const [colName, dbColumnName] of columnNameMap) {
219
+ const mapper = columnMappers[colName];
191
220
  Object.defineProperty(mock, colName, {
192
- get: () => ({
193
- __fieldName: colName,
194
- __dbColumnName: dbColumnName,
195
- __tableAlias: this.schema.name,
196
- }),
221
+ get() {
222
+ let cached = fieldRefCache[colName];
223
+ if (!cached) {
224
+ cached = fieldRefCache[colName] = {
225
+ __fieldName: colName,
226
+ __dbColumnName: dbColumnName,
227
+ __tableAlias: tableAlias,
228
+ // Include mapper for toDriver transformation in conditions
229
+ __mapper: mapper,
230
+ };
231
+ }
232
+ return cached;
233
+ },
197
234
  enumerable: true,
198
235
  configurable: true,
199
236
  });
@@ -339,14 +376,22 @@ class QueryBuilder {
339
376
  const mock = {};
340
377
  // Performance: Use pre-computed column name map if available
341
378
  const columnNameMap = getColumnNameMapForSchema(schema);
379
+ // Performance: Lazy-cache FieldRef objects
380
+ const fieldRefCache = {};
342
381
  // Add columns as FieldRef objects with table alias
343
382
  for (const [colName, dbColumnName] of columnNameMap) {
344
383
  Object.defineProperty(mock, colName, {
345
- get: () => ({
346
- __fieldName: colName,
347
- __dbColumnName: dbColumnName,
348
- __tableAlias: alias,
349
- }),
384
+ get() {
385
+ let cached = fieldRefCache[colName];
386
+ if (!cached) {
387
+ cached = fieldRefCache[colName] = {
388
+ __fieldName: colName,
389
+ __dbColumnName: dbColumnName,
390
+ __tableAlias: alias,
391
+ };
392
+ }
393
+ return cached;
394
+ },
350
395
  enumerable: true,
351
396
  configurable: true,
352
397
  });
@@ -711,14 +756,22 @@ class SelectQueryBuilder {
711
756
  const mock = {};
712
757
  // Performance: Use pre-computed column name map if available
713
758
  const columnNameMap = getColumnNameMapForSchema(schema);
759
+ // Performance: Lazy-cache FieldRef objects
760
+ const fieldRefCache = {};
714
761
  // Add columns as FieldRef objects with table alias
715
762
  for (const [colName, dbColumnName] of columnNameMap) {
716
763
  Object.defineProperty(mock, colName, {
717
- get: () => ({
718
- __fieldName: colName,
719
- __dbColumnName: dbColumnName,
720
- __tableAlias: alias,
721
- }),
764
+ get() {
765
+ let cached = fieldRefCache[colName];
766
+ if (!cached) {
767
+ cached = fieldRefCache[colName] = {
768
+ __fieldName: colName,
769
+ __dbColumnName: dbColumnName,
770
+ __tableAlias: alias,
771
+ };
772
+ }
773
+ return cached;
774
+ },
722
775
  enumerable: true,
723
776
  configurable: true,
724
777
  });
@@ -986,65 +1039,94 @@ class SelectQueryBuilder {
986
1039
  * Collection results are automatically resolved to arrays
987
1040
  */
988
1041
  async toList() {
989
- const context = {
1042
+ const options = this.executor?.getOptions();
1043
+ const tracer = new db_context_1.TimeTracer(options?.traceTime ?? false, options?.logger);
1044
+ // Query Build Phase
1045
+ tracer.startPhase('queryBuild');
1046
+ const context = tracer.trace('createContext', () => ({
990
1047
  ctes: new Map(),
991
1048
  cteCounter: 0,
992
1049
  paramCounter: 1,
993
1050
  allParams: [],
994
1051
  collectionStrategy: this.collectionStrategy,
995
1052
  executor: this.executor,
996
- };
1053
+ }));
997
1054
  // Analyze the selector to extract nested queries
998
- const mockRow = this.createMockRow();
999
- const selectionResult = this.selector(mockRow);
1055
+ const mockRow = tracer.trace('createMockRow', () => this.createMockRow());
1056
+ const selectionResult = tracer.trace('evaluateSelector', () => this.selector(mockRow));
1000
1057
  // Check if we're using temp table strategy and have collections
1001
- const collections = this.detectCollections(selectionResult);
1058
+ const collections = tracer.trace('detectCollections', () => this.detectCollections(selectionResult));
1002
1059
  const useTempTableStrategy = this.collectionStrategy === 'temptable' && collections.length > 0;
1060
+ tracer.endPhase();
1061
+ let results;
1003
1062
  if (useTempTableStrategy) {
1004
1063
  // Two-phase execution for temp table strategy
1005
- return this.executeWithTempTables(selectionResult, context, collections);
1064
+ results = await this.executeWithTempTables(selectionResult, context, collections, tracer);
1006
1065
  }
1007
1066
  else {
1008
1067
  // Single-phase execution for JSONB strategy (current behavior)
1009
- return this.executeSinglePhase(selectionResult, context);
1068
+ results = await this.executeSinglePhase(selectionResult, context, tracer);
1010
1069
  }
1070
+ // Log trace summary if tracing is enabled
1071
+ tracer.logSummary(results.length);
1072
+ return results;
1011
1073
  }
1012
1074
  /**
1013
1075
  * Execute query using single-phase approach (JSONB/CTE strategy)
1014
1076
  */
1015
- async executeSinglePhase(selectionResult, context) {
1077
+ async executeSinglePhase(selectionResult, context, tracer) {
1016
1078
  // Build the query
1017
- const { sql, params } = this.buildQuery(selectionResult, context);
1079
+ tracer.startPhase('queryBuild');
1080
+ const { sql, params } = tracer.trace('buildQuery', () => this.buildQuery(selectionResult, context));
1081
+ tracer.endPhase();
1018
1082
  // Execute using executor if available, otherwise use client directly
1019
- const result = this.executor
1083
+ tracer.startPhase('queryExecution');
1084
+ const result = await tracer.traceAsync('executeQuery', async () => this.executor
1020
1085
  ? await this.executor.query(sql, params)
1021
- : await this.client.query(sql, params);
1086
+ : await this.client.query(sql, params), { rowCount: 'pending' });
1087
+ tracer.endPhase();
1088
+ // If rawResult is enabled, return raw rows without any processing
1089
+ if (this.executor?.getOptions().rawResult) {
1090
+ return result.rows;
1091
+ }
1022
1092
  // Transform results
1023
- return this.transformResults(result.rows, selectionResult);
1093
+ tracer.startPhase('resultProcessing');
1094
+ const transformed = tracer.trace('transformResults', () => this.transformResults(result.rows, selectionResult), { rowCount: result.rows.length });
1095
+ tracer.endPhase();
1096
+ return transformed;
1024
1097
  }
1025
1098
  /**
1026
1099
  * Execute query using two-phase approach (temp table strategy)
1027
1100
  */
1028
- async executeWithTempTables(selectionResult, context, collections) {
1101
+ async executeWithTempTables(selectionResult, context, collections, tracer) {
1029
1102
  // Build base selection (excludes collections, includes foreign keys)
1030
- const baseSelection = this.buildBaseSelection(selectionResult, collections);
1031
- const { sql: baseSql, params: baseParams } = this.buildQuery(baseSelection, {
1103
+ tracer.startPhase('queryBuild');
1104
+ const baseSelection = tracer.trace('buildBaseSelection', () => this.buildBaseSelection(selectionResult, collections));
1105
+ const { sql: baseSql, params: baseParams } = tracer.trace('buildBaseQuery', () => this.buildQuery(baseSelection, {
1032
1106
  ...context,
1033
1107
  ctes: new Map(), // Clear CTEs since we're not using them for collections
1034
- });
1108
+ }));
1109
+ tracer.endPhase();
1035
1110
  // Check if we can use fully optimized single-query approach
1036
1111
  // Requirements: PostgresClient with querySimpleMulti support AND no parameters in base query
1037
1112
  const canUseFullOptimization = this.client.supportsMultiStatementQueries() &&
1038
1113
  baseParams.length === 0 &&
1039
1114
  collections.length > 0;
1040
1115
  if (canUseFullOptimization) {
1041
- return this.executeFullyOptimized(baseSql, baseSelection, selectionResult, context, collections);
1116
+ return this.executeFullyOptimized(baseSql, baseSelection, selectionResult, context, collections, tracer);
1042
1117
  }
1043
1118
  // Legacy two-phase approach: execute base query first
1044
- const baseResult = this.executor
1119
+ tracer.startPhase('queryExecution');
1120
+ const baseResult = await tracer.traceAsync('executeBaseQuery', async () => this.executor
1045
1121
  ? await this.executor.query(baseSql, baseParams)
1046
- : await this.client.query(baseSql, baseParams);
1122
+ : await this.client.query(baseSql, baseParams));
1123
+ // If rawResult is enabled, return raw rows without any processing
1124
+ if (this.executor?.getOptions().rawResult) {
1125
+ tracer.endPhase();
1126
+ return baseResult.rows;
1127
+ }
1047
1128
  if (baseResult.rows.length === 0) {
1129
+ tracer.endPhase();
1048
1130
  return [];
1049
1131
  }
1050
1132
  // Extract parent IDs from base results (using the known alias we added in buildBaseSelection)
@@ -1055,7 +1137,7 @@ class SelectQueryBuilder {
1055
1137
  for (const collection of collections) {
1056
1138
  const builder = collection.builder;
1057
1139
  // Call buildCTE with parent IDs - this will use the temp table strategy
1058
- const aggResult = await builder.buildCTE(context, this.client, parentIds);
1140
+ const aggResult = await tracer.traceAsync(`buildCTE:${collection.name}`, async () => builder.buildCTE(context, this.client, parentIds));
1059
1141
  // aggResult is a Promise<CollectionAggregationResult> for temp table strategy
1060
1142
  const result = await aggResult;
1061
1143
  // If the result has a tableName, it means temp tables were created and we need to query them
@@ -1068,9 +1150,9 @@ class SelectQueryBuilder {
1068
1150
  else {
1069
1151
  // Temp table strategy (legacy) - query the aggregation table
1070
1152
  const aggQuery = `SELECT parent_id, data FROM ${result.tableName}`;
1071
- const aggQueryResult = this.executor
1153
+ const aggQueryResult = await tracer.traceAsync(`queryCollection:${collection.name}`, async () => this.executor
1072
1154
  ? await this.executor.query(aggQuery, [])
1073
- : await this.client.query(aggQuery, []);
1155
+ : await this.client.query(aggQuery, []));
1074
1156
  // Cleanup temp tables if needed
1075
1157
  if (result.cleanupSql) {
1076
1158
  await this.client.query(result.cleanupSql);
@@ -1088,8 +1170,10 @@ class SelectQueryBuilder {
1088
1170
  throw new Error('Expected temp table result but got CTE');
1089
1171
  }
1090
1172
  }
1173
+ tracer.endPhase();
1091
1174
  // Phase 3: Merge base results with collection results
1092
- const mergedRows = baseResult.rows.map(baseRow => {
1175
+ tracer.startPhase('resultProcessing');
1176
+ const mergedRows = tracer.trace('mergeResults', () => baseResult.rows.map(baseRow => {
1093
1177
  const merged = { ...baseRow };
1094
1178
  for (const collection of collections) {
1095
1179
  const resultMap = collectionResults.get(collection.name);
@@ -1099,84 +1183,104 @@ class SelectQueryBuilder {
1099
1183
  // Remove the internal __pk_id field before returning
1100
1184
  delete merged.__pk_id;
1101
1185
  return merged;
1102
- });
1186
+ }), { rowCount: baseResult.rows.length });
1103
1187
  // Transform results using the original selection
1104
- return this.transformResults(mergedRows, selectionResult);
1188
+ const transformed = tracer.trace('transformResults', () => this.transformResults(mergedRows, selectionResult), { rowCount: mergedRows.length });
1189
+ tracer.endPhase();
1190
+ return transformed;
1105
1191
  }
1106
1192
  /**
1107
1193
  * Execute using fully optimized single-query approach (PostgresClient only)
1108
1194
  * Combines base query + all collections into ONE multi-statement query
1109
1195
  */
1110
- async executeFullyOptimized(baseSql, baseSelection, selectionResult, context, collections) {
1196
+ async executeFullyOptimized(baseSql, baseSelection, selectionResult, context, collections, tracer) {
1197
+ tracer.startPhase('queryBuild');
1111
1198
  const baseTempTable = `tmp_base_${context.cteCounter++}`;
1112
1199
  // Build SQL for each collection
1113
- const collectionSQLs = [];
1114
- for (const collection of collections) {
1115
- const builderAny = collection.builder;
1116
- const targetTable = builderAny.targetTable;
1117
- const foreignKey = builderAny.foreignKey;
1118
- const selector = builderAny.selector;
1119
- const orderByFields = builderAny.orderByFields || [];
1120
- // Build selected fields
1121
- let selectedFieldsSQL = '';
1122
- if (selector) {
1123
- const mockItem = builderAny.createMockItem();
1124
- const selectedFields = selector(mockItem);
1125
- const fieldParts = [];
1126
- for (const [alias, field] of Object.entries(selectedFields)) {
1127
- if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
1128
- const dbColumnName = field.__dbColumnName;
1129
- fieldParts.push(`"${dbColumnName}" as "${alias}"`);
1200
+ const collectionSQLs = tracer.trace('buildCollectionSQLs', () => {
1201
+ const sqls = [];
1202
+ for (const collection of collections) {
1203
+ const builderAny = collection.builder;
1204
+ const targetTable = builderAny.targetTable;
1205
+ const foreignKey = builderAny.foreignKey;
1206
+ const selector = builderAny.selector;
1207
+ const orderByFields = builderAny.orderByFields || [];
1208
+ // Build selected fields
1209
+ let selectedFieldsSQL = '';
1210
+ if (selector) {
1211
+ const mockItem = builderAny.createMockItem();
1212
+ const selectedFields = selector(mockItem);
1213
+ const fieldParts = [];
1214
+ for (const [alias, field] of Object.entries(selectedFields)) {
1215
+ if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
1216
+ const dbColumnName = field.__dbColumnName;
1217
+ fieldParts.push(`"${dbColumnName}" as "${alias}"`);
1218
+ }
1130
1219
  }
1220
+ selectedFieldsSQL = fieldParts.join(', ');
1131
1221
  }
1132
- selectedFieldsSQL = fieldParts.join(', ');
1133
- }
1134
- // Build ORDER BY
1135
- let orderBySQL = orderByFields.length > 0
1136
- ? ` ORDER BY ${orderByFields.map(({ field, direction }) => `"${field}" ${direction}`).join(', ')}`
1137
- : ` ORDER BY "id" DESC`;
1138
- const collectionSQL = `SELECT "${foreignKey}" as parent_id, ${selectedFieldsSQL} FROM "${targetTable}" WHERE "${foreignKey}" IN (SELECT "__pk_id" FROM ${baseTempTable})${orderBySQL}`;
1139
- collectionSQLs.push(collectionSQL);
1140
- }
1222
+ // Build ORDER BY
1223
+ let orderBySQL = orderByFields.length > 0
1224
+ ? ` ORDER BY ${orderByFields.map(({ field, direction }) => `"${field}" ${direction}`).join(', ')}`
1225
+ : ` ORDER BY "id" DESC`;
1226
+ const collectionSQL = `SELECT "${foreignKey}" as parent_id, ${selectedFieldsSQL} FROM "${targetTable}" WHERE "${foreignKey}" IN (SELECT "__pk_id" FROM ${baseTempTable})${orderBySQL}`;
1227
+ sqls.push(collectionSQL);
1228
+ }
1229
+ return sqls;
1230
+ });
1141
1231
  // Build mega multi-statement SQL
1142
- const statements = [
1143
- `CREATE TEMP TABLE ${baseTempTable} AS ${baseSql}`,
1144
- `SELECT * FROM ${baseTempTable}`,
1145
- ...collectionSQLs,
1146
- `DROP TABLE IF EXISTS ${baseTempTable}`
1147
- ];
1148
- const multiStatementSQL = statements.join(';\n');
1232
+ const multiStatementSQL = tracer.trace('buildMultiStatement', () => {
1233
+ const statements = [
1234
+ `CREATE TEMP TABLE ${baseTempTable} AS ${baseSql}`,
1235
+ `SELECT * FROM ${baseTempTable}`,
1236
+ ...collectionSQLs,
1237
+ `DROP TABLE IF EXISTS ${baseTempTable}`
1238
+ ];
1239
+ return statements.join(';\n');
1240
+ });
1241
+ tracer.endPhase();
1149
1242
  // Execute via querySimpleMulti
1243
+ tracer.startPhase('queryExecution');
1150
1244
  const executor = this.executor || this.client;
1151
1245
  let resultSets;
1152
1246
  if ('querySimpleMulti' in executor && typeof executor.querySimpleMulti === 'function') {
1153
- resultSets = await executor.querySimpleMulti(multiStatementSQL);
1247
+ resultSets = await tracer.traceAsync('executeMultiStatement', async () => executor.querySimpleMulti(multiStatementSQL));
1154
1248
  }
1155
1249
  else {
1156
1250
  throw new Error('Fully optimized mode requires querySimpleMulti support');
1157
1251
  }
1252
+ tracer.endPhase();
1158
1253
  // Parse result sets: [0]=CREATE, [1]=base, [2..N]=collections, [N+1]=DROP
1159
1254
  const baseResult = resultSets[1];
1255
+ // If rawResult is enabled, return raw rows without any processing
1256
+ if (this.executor?.getOptions().rawResult) {
1257
+ return baseResult?.rows || [];
1258
+ }
1160
1259
  if (!baseResult || baseResult.rows.length === 0) {
1161
1260
  return [];
1162
1261
  }
1262
+ // Result processing phase
1263
+ tracer.startPhase('resultProcessing');
1163
1264
  // Group collection results by parent_id
1164
- const collectionResults = new Map();
1165
- collections.forEach((collection, idx) => {
1166
- const collectionResultSet = resultSets[2 + idx];
1167
- const dataMap = new Map();
1168
- for (const row of collectionResultSet.rows) {
1169
- const parentId = row.parent_id;
1170
- if (!dataMap.has(parentId)) {
1171
- dataMap.set(parentId, []);
1172
- }
1173
- const { parent_id, ...rowData } = row;
1174
- dataMap.get(parentId).push(rowData);
1175
- }
1176
- collectionResults.set(collection.name, dataMap);
1265
+ const collectionResults = tracer.trace('groupCollectionResults', () => {
1266
+ const results = new Map();
1267
+ collections.forEach((collection, idx) => {
1268
+ const collectionResultSet = resultSets[2 + idx];
1269
+ const dataMap = new Map();
1270
+ for (const row of collectionResultSet.rows) {
1271
+ const parentId = row.parent_id;
1272
+ if (!dataMap.has(parentId)) {
1273
+ dataMap.set(parentId, []);
1274
+ }
1275
+ const { parent_id, ...rowData } = row;
1276
+ dataMap.get(parentId).push(rowData);
1277
+ }
1278
+ results.set(collection.name, dataMap);
1279
+ });
1280
+ return results;
1177
1281
  });
1178
1282
  // Merge base results with collection results
1179
- const mergedRows = baseResult.rows.map((baseRow) => {
1283
+ const mergedRows = tracer.trace('mergeResults', () => baseResult.rows.map((baseRow) => {
1180
1284
  const merged = { ...baseRow };
1181
1285
  for (const collection of collections) {
1182
1286
  const resultMap = collectionResults.get(collection.name);
@@ -1185,8 +1289,10 @@ class SelectQueryBuilder {
1185
1289
  }
1186
1290
  delete merged.__pk_id;
1187
1291
  return merged;
1188
- });
1189
- return this.transformResults(mergedRows, selectionResult);
1292
+ }), { rowCount: baseResult.rows.length });
1293
+ const transformed = tracer.trace('transformResults', () => this.transformResults(mergedRows, selectionResult), { rowCount: mergedRows.length });
1294
+ tracer.endPhase();
1295
+ return transformed;
1190
1296
  }
1191
1297
  /**
1192
1298
  * Detect collections in the selection result
@@ -1340,16 +1446,36 @@ class SelectQueryBuilder {
1340
1446
  */
1341
1447
  createMockRow() {
1342
1448
  const mock = {};
1449
+ const tableAlias = this.schema.name;
1343
1450
  // Performance: Use pre-computed column name map if available
1344
1451
  const columnNameMap = getColumnNameMapForSchema(this.schema);
1452
+ // Performance: Lazy-cache FieldRef objects
1453
+ const fieldRefCache = {};
1454
+ // Build a mapper lookup for columns (only when needed)
1455
+ const columnMappers = {};
1456
+ for (const [colName, colBuilder] of Object.entries(this.schema.columns)) {
1457
+ const config = colBuilder.build();
1458
+ if (config.mapper) {
1459
+ columnMappers[colName] = config.mapper;
1460
+ }
1461
+ }
1345
1462
  // Add columns as FieldRef objects - type-safe with property name and database column name
1346
1463
  for (const [colName, dbColumnName] of columnNameMap) {
1464
+ const mapper = columnMappers[colName];
1347
1465
  Object.defineProperty(mock, colName, {
1348
- get: () => ({
1349
- __fieldName: colName,
1350
- __dbColumnName: dbColumnName,
1351
- __tableAlias: this.schema.name,
1352
- }),
1466
+ get() {
1467
+ let cached = fieldRefCache[colName];
1468
+ if (!cached) {
1469
+ cached = fieldRefCache[colName] = {
1470
+ __fieldName: colName,
1471
+ __dbColumnName: dbColumnName,
1472
+ __tableAlias: tableAlias,
1473
+ // Include mapper for toDriver transformation in conditions
1474
+ __mapper: mapper,
1475
+ };
1476
+ }
1477
+ return cached;
1478
+ },
1353
1479
  enumerable: true,
1354
1480
  configurable: true,
1355
1481
  });
@@ -1365,13 +1491,22 @@ class SelectQueryBuilder {
1365
1491
  if (!mock[join.alias]) {
1366
1492
  mock[join.alias] = {};
1367
1493
  }
1494
+ // Lazy-cache for joined table
1495
+ const joinFieldRefCache = {};
1496
+ const joinAlias = join.alias;
1368
1497
  for (const [colName, dbColumnName] of joinColumnNameMap) {
1369
1498
  Object.defineProperty(mock[join.alias], colName, {
1370
- get: () => ({
1371
- __fieldName: colName,
1372
- __dbColumnName: dbColumnName,
1373
- __tableAlias: join.alias,
1374
- }),
1499
+ get() {
1500
+ let cached = joinFieldRefCache[colName];
1501
+ if (!cached) {
1502
+ cached = joinFieldRefCache[colName] = {
1503
+ __fieldName: colName,
1504
+ __dbColumnName: dbColumnName,
1505
+ __tableAlias: joinAlias,
1506
+ };
1507
+ }
1508
+ return cached;
1509
+ },
1375
1510
  enumerable: true,
1376
1511
  configurable: true,
1377
1512
  });
@@ -1441,12 +1576,13 @@ class SelectQueryBuilder {
1441
1576
  // If the value is already a FieldRef
1442
1577
  if (value && typeof value === 'object' && '__fieldName' in value && '__dbColumnName' in value) {
1443
1578
  if (preserveOriginal) {
1444
- // For WHERE: preserve original column name and table alias
1445
- // This ensures WHERE references the actual database column
1579
+ // For WHERE: preserve original column name, table alias, and mapper
1580
+ // This ensures WHERE references the actual database column with proper type conversion
1446
1581
  return {
1447
1582
  __fieldName: prop,
1448
1583
  __dbColumnName: value.__dbColumnName,
1449
1584
  __tableAlias: value.__tableAlias,
1585
+ __mapper: value.__mapper, // Preserve mapper for toDriver in conditions
1450
1586
  };
1451
1587
  }
1452
1588
  else {
@@ -2046,181 +2182,199 @@ class SelectQueryBuilder {
2046
2182
  * Transform database results
2047
2183
  */
2048
2184
  transformResults(rows, selection) {
2185
+ if (rows.length === 0) {
2186
+ return [];
2187
+ }
2049
2188
  // Check if mappers are disabled for performance
2050
2189
  const disableMappers = this.executor?.getOptions().disableMappers ?? false;
2051
- // Pre-analyze selection structure once instead of per-row
2052
- // This avoids repeated Object.entries() calls and type checks
2053
- const selectionKeys = Object.keys(selection);
2054
- const selectionEntries = Object.entries(selection);
2055
- // Pre-cache navigation placeholders to avoid repeated checks
2056
- // Only cache actual navigation properties (arrays and getter-based navigation)
2057
- const navigationPlaceholders = {};
2058
- for (const [key, value] of selectionEntries) {
2190
+ // Pre-analyze selection structure ONCE and categorize each field
2191
+ // This moves all type checks out of the per-row loop
2192
+ const schemaColumnCache = this.schema.columnMetadataCache;
2193
+ const fieldConfigs = [];
2194
+ // Single pass to categorize all fields
2195
+ for (const key in selection) {
2196
+ const value = selection[key];
2197
+ // Check for navigation placeholders first (most common early exit)
2059
2198
  if (Array.isArray(value) && value.length === 0) {
2060
- navigationPlaceholders[key] = [];
2199
+ fieldConfigs.push({ key, type: 0 /* FieldType.NAVIGATION */, value: [] });
2200
+ continue;
2061
2201
  }
2062
- else if (value === undefined) {
2063
- navigationPlaceholders[key] = undefined;
2202
+ if (value === undefined) {
2203
+ fieldConfigs.push({ key, type: 0 /* FieldType.NAVIGATION */, value: undefined });
2204
+ continue;
2064
2205
  }
2065
- else if (value && typeof value === 'object' && !('__dbColumnName' in value) && !('__fieldName' in value)) {
2066
- // Check if it's a navigation property mock (object with getters)
2067
- // Exclude FieldRef objects by checking for __fieldName
2206
+ // Check for navigation property mocks (objects with getters)
2207
+ // These are treated as SIMPLE because the actual value comes from json_build_object in the row
2208
+ // The navigation mock is just a placeholder - actual data processing happens via FieldType.SIMPLE
2209
+ if (value && typeof value === 'object' && !('__dbColumnName' in value) && !('__fieldName' in value) && !('__collectionResult' in value) && !('__isAggregationArray' in value)) {
2068
2210
  const props = Object.getOwnPropertyNames(value);
2069
2211
  if (props.length > 0) {
2070
- const firstProp = props[0];
2071
- const descriptor = Object.getOwnPropertyDescriptor(value, firstProp);
2212
+ const descriptor = Object.getOwnPropertyDescriptor(value, props[0]);
2072
2213
  if (descriptor && descriptor.get) {
2073
- navigationPlaceholders[key] = undefined;
2214
+ // Navigation mock - treat as simple, data will come from row via json_build_object
2215
+ // If row has no data, convertValue will return undefined
2216
+ fieldConfigs.push({ key, type: 8 /* FieldType.SIMPLE */, value });
2217
+ continue;
2074
2218
  }
2075
2219
  }
2076
2220
  }
2077
- }
2078
- // Pre-build column metadata cache to avoid repeated schema lookups
2079
- const columnMetadataCache = {};
2080
- if (!disableMappers) {
2081
- for (const [key, value] of selectionEntries) {
2082
- if (typeof value === 'object' && value !== null && '__fieldName' in value) {
2221
+ // Collection types
2222
+ if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
2223
+ const isScalarAgg = value instanceof CollectionQueryBuilder && value.isScalarAggregation();
2224
+ if (isScalarAgg) {
2225
+ const aggregationType = value.getAggregationType();
2226
+ fieldConfigs.push({
2227
+ key,
2228
+ type: 1 /* FieldType.COLLECTION_SCALAR */,
2229
+ value,
2230
+ aggregationType
2231
+ });
2232
+ }
2233
+ else {
2234
+ const isArrayAgg = value && typeof value === 'object' && 'isArrayAggregation' in value && value.isArrayAggregation();
2235
+ if (isArrayAgg) {
2236
+ fieldConfigs.push({ key, type: 2 /* FieldType.COLLECTION_ARRAY */, value });
2237
+ }
2238
+ else {
2239
+ fieldConfigs.push({
2240
+ key,
2241
+ type: 3 /* FieldType.COLLECTION_JSON */,
2242
+ value,
2243
+ collectionBuilder: value instanceof CollectionQueryBuilder ? value : undefined
2244
+ });
2245
+ }
2246
+ }
2247
+ continue;
2248
+ }
2249
+ // CTE aggregation array
2250
+ if (typeof value === 'object' && value !== null && '__isAggregationArray' in value && value.__isAggregationArray) {
2251
+ fieldConfigs.push({
2252
+ key,
2253
+ type: 4 /* FieldType.CTE_AGGREGATION */,
2254
+ value,
2255
+ innerMetadata: value.__innerSelectionMetadata
2256
+ });
2257
+ continue;
2258
+ }
2259
+ // SqlFragment with mapper
2260
+ if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
2261
+ let mapper = disableMappers ? null : value.getMapper();
2262
+ if (mapper && typeof mapper.getType === 'function') {
2263
+ mapper = mapper.getType();
2264
+ }
2265
+ if (mapper && typeof mapper.fromDriver === 'function') {
2266
+ fieldConfigs.push({ key, type: 5 /* FieldType.SQL_FRAGMENT_MAPPER */, value, mapper });
2267
+ }
2268
+ else {
2269
+ fieldConfigs.push({ key, type: 8 /* FieldType.SIMPLE */, value });
2270
+ }
2271
+ continue;
2272
+ }
2273
+ // FieldRef with potential mapper
2274
+ if (typeof value === 'object' && value !== null && '__fieldName' in value) {
2275
+ if (disableMappers) {
2276
+ fieldConfigs.push({ key, type: 7 /* FieldType.FIELD_REF_NO_MAPPER */, value });
2277
+ }
2278
+ else {
2083
2279
  const fieldName = value.__fieldName;
2084
- const column = this.schema.columns[fieldName];
2085
- if (column) {
2086
- const config = column.build();
2087
- columnMetadataCache[key] = {
2088
- hasMapper: !!config.mapper,
2089
- mapper: config.mapper,
2090
- config: config,
2091
- };
2280
+ const cached = schemaColumnCache?.get(fieldName);
2281
+ if (cached && cached.hasMapper) {
2282
+ fieldConfigs.push({ key, type: 6 /* FieldType.FIELD_REF_MAPPER */, value, mapper: cached.mapper });
2283
+ }
2284
+ else if (cached) {
2285
+ fieldConfigs.push({ key, type: 7 /* FieldType.FIELD_REF_NO_MAPPER */, value });
2286
+ }
2287
+ else {
2288
+ // Not in schema - treat as simple value
2289
+ fieldConfigs.push({ key, type: 8 /* FieldType.SIMPLE */, value });
2092
2290
  }
2093
2291
  }
2292
+ continue;
2094
2293
  }
2294
+ // Default: simple value
2295
+ fieldConfigs.push({ key, type: 8 /* FieldType.SIMPLE */, value });
2095
2296
  }
2096
- return rows.map(row => {
2297
+ // Transform each row using pre-analyzed field configs
2298
+ // Using while(i--) for maximum performance - decrement and compare to 0 is faster
2299
+ const results = new Array(rows.length);
2300
+ const configCount = fieldConfigs.length;
2301
+ let rowIdx = rows.length;
2302
+ while (rowIdx--) {
2303
+ const row = rows[rowIdx];
2097
2304
  const result = {};
2098
- // Copy navigation placeholders without iteration
2099
- Object.assign(result, navigationPlaceholders);
2100
- // Then process actual data fields
2101
- for (const [key, value] of selectionEntries) {
2102
- // Skip if we already set this key as a navigation placeholder
2103
- // UNLESS there's actual data for this key in the row (e.g., from json_build_object)
2104
- if (key in result && (result[key] === undefined || Array.isArray(result[key])) && !(key in row && row[key] !== undefined && row[key] !== null)) {
2305
+ let i = configCount;
2306
+ // Process all fields using pre-computed types
2307
+ while (i--) {
2308
+ const config = fieldConfigs[i];
2309
+ const key = config.key;
2310
+ // Handle navigation placeholders separately
2311
+ if (config.type === 0 /* FieldType.NAVIGATION */) {
2312
+ result[key] = config.value;
2105
2313
  continue;
2106
2314
  }
2107
- if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
2108
- // Check if this is a scalar aggregation (count, sum, max, min)
2109
- const isScalarAgg = value instanceof CollectionQueryBuilder && value.isScalarAggregation();
2110
- if (isScalarAgg) {
2111
- // For scalar aggregations, return the value directly
2112
- // For COUNT, convertValue will handle numeric conversion (NULL is already COALESCE'd to 0 in SQL)
2113
- // For MAX/MIN/SUM, we want to keep NULL as null (not undefined)
2114
- const aggregationType = value.getAggregationType();
2115
- if (aggregationType === 'COUNT') {
2116
- result[key] = this.convertValue(row[key]);
2315
+ const rawValue = row[key];
2316
+ switch (config.type) {
2317
+ case 1 /* FieldType.COLLECTION_SCALAR */: {
2318
+ if (config.aggregationType === 'COUNT') {
2319
+ result[key] = this.convertValue(rawValue);
2117
2320
  }
2118
2321
  else {
2119
- // For MAX/MIN/SUM, preserve NULL and convert numeric strings to numbers
2120
- const rawValue = row[key];
2322
+ // MAX/MIN/SUM: preserve NULL, convert numeric strings
2121
2323
  if (rawValue === null) {
2122
2324
  result[key] = null;
2123
2325
  }
2124
2326
  else if (typeof rawValue === 'string' && NUMERIC_REGEX.test(rawValue)) {
2125
- result[key] = Number(rawValue);
2327
+ result[key] = +rawValue;
2126
2328
  }
2127
2329
  else {
2128
2330
  result[key] = rawValue;
2129
2331
  }
2130
2332
  }
2333
+ break;
2131
2334
  }
2132
- else {
2133
- // Check if this is a flattened array result (toNumberList/toStringList)
2134
- const isArrayAgg = value && typeof value === 'object' && 'isArrayAggregation' in value && value.isArrayAggregation();
2135
- if (isArrayAgg) {
2136
- // For flattened arrays, PostgreSQL returns a native array - use it directly
2137
- result[key] = row[key] || [];
2335
+ case 2 /* FieldType.COLLECTION_ARRAY */:
2336
+ result[key] = rawValue || [];
2337
+ break;
2338
+ case 3 /* FieldType.COLLECTION_JSON */: {
2339
+ const items = rawValue || [];
2340
+ if (config.collectionBuilder) {
2341
+ result[key] = this.transformCollectionItems(items, config.collectionBuilder);
2138
2342
  }
2139
2343
  else {
2140
- // Parse JSON array from CTE (both CollectionQueryBuilder and CollectionResult are treated the same at runtime)
2141
- const collectionItems = row[key] || [];
2142
- // Apply fromDriver mappers to collection items if needed
2143
- if (value instanceof CollectionQueryBuilder) {
2144
- result[key] = this.transformCollectionItems(collectionItems, value);
2145
- }
2146
- else {
2147
- result[key] = collectionItems;
2148
- }
2344
+ result[key] = items;
2149
2345
  }
2346
+ break;
2150
2347
  }
2151
- }
2152
- else if (typeof value === 'object' && value !== null && '__isAggregationArray' in value && value.__isAggregationArray) {
2153
- // CTE withAggregation array - apply mappers to items inside
2154
- const collectionItems = row[key] || [];
2155
- const innerMetadata = value.__innerSelectionMetadata;
2156
- if (innerMetadata && !disableMappers) {
2157
- result[key] = this.transformCteAggregationItems(collectionItems, innerMetadata);
2158
- }
2159
- else {
2160
- result[key] = collectionItems;
2161
- }
2162
- }
2163
- else if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
2164
- // SqlFragment with custom mapper (check this BEFORE FieldRef to handle subquery/CTE fields with mappers)
2165
- const rawValue = row[key];
2166
- if (disableMappers) {
2167
- // Skip mapper transformation for performance
2168
- result[key] = this.convertValue(rawValue);
2169
- }
2170
- else {
2171
- let mapper = value.getMapper();
2172
- if (mapper && rawValue !== null && rawValue !== undefined) {
2173
- // If mapper is a CustomTypeBuilder, get the actual type
2174
- if (typeof mapper.getType === 'function') {
2175
- mapper = mapper.getType();
2176
- }
2177
- // Apply the fromDriver transformation
2178
- if (typeof mapper.fromDriver === 'function') {
2179
- result[key] = mapper.fromDriver(rawValue);
2180
- }
2181
- else {
2182
- // Fallback if fromDriver doesn't exist
2183
- result[key] = this.convertValue(rawValue);
2184
- }
2348
+ case 4 /* FieldType.CTE_AGGREGATION */: {
2349
+ const items = rawValue || [];
2350
+ if (config.innerMetadata && !disableMappers) {
2351
+ result[key] = this.transformCteAggregationItems(items, config.innerMetadata);
2185
2352
  }
2186
2353
  else {
2187
- // No mapper or null value - convert normally
2188
- result[key] = this.convertValue(rawValue);
2354
+ result[key] = items;
2189
2355
  }
2356
+ break;
2190
2357
  }
2191
- }
2192
- else if (typeof value === 'object' && value !== null && '__fieldName' in value) {
2193
- // FieldRef object - check if it has a custom mapper
2194
- const rawValue = row[key];
2195
- if (disableMappers) {
2196
- // Skip mapper transformation for performance
2197
- result[key] = rawValue === null ? undefined : rawValue;
2198
- }
2199
- else {
2200
- // Use pre-cached column metadata instead of repeated lookups
2201
- const cached = columnMetadataCache[key];
2202
- if (cached) {
2203
- // Field is in our schema - use cached mapper info
2204
- result[key] = rawValue === null
2205
- ? undefined
2206
- : (cached.hasMapper ? cached.mapper.fromDriver(rawValue) : rawValue);
2207
- }
2208
- else {
2209
- // Field not in schema (e.g., CTE field, joined table field)
2210
- // Always call convertValue to handle numeric string conversion
2211
- result[key] = this.convertValue(row[key]);
2212
- }
2213
- }
2214
- }
2215
- else {
2216
- // Convert null to undefined for all other values
2217
- // Also convert numeric strings to numbers for scalar subqueries (PostgreSQL returns NUMERIC as string)
2218
- const converted = this.convertValue(row[key]);
2219
- result[key] = converted;
2358
+ case 5 /* FieldType.SQL_FRAGMENT_MAPPER */:
2359
+ // mapWith wraps user functions to handle null
2360
+ result[key] = config.mapper.fromDriver(rawValue);
2361
+ break;
2362
+ case 6 /* FieldType.FIELD_REF_MAPPER */:
2363
+ // Column mappers (customType) - null check done here
2364
+ result[key] = config.mapper.fromDriver(rawValue);
2365
+ break;
2366
+ case 7 /* FieldType.FIELD_REF_NO_MAPPER */:
2367
+ result[key] = rawValue;
2368
+ break;
2369
+ case 8 /* FieldType.SIMPLE */:
2370
+ default:
2371
+ result[key] = this.convertValue(rawValue);
2372
+ break;
2220
2373
  }
2221
2374
  }
2222
- return result;
2223
- });
2375
+ results[rowIdx] = result;
2376
+ }
2377
+ return results;
2224
2378
  }
2225
2379
  /**
2226
2380
  * Convert database values: null to undefined, numeric strings to numbers
@@ -2231,11 +2385,9 @@ class SelectQueryBuilder {
2231
2385
  }
2232
2386
  // Check if it's a numeric string (PostgreSQL NUMERIC type)
2233
2387
  // This handles scalar subqueries with aggregates like AVG, SUM, etc.
2388
+ // The regex validates format, so Number() is guaranteed to produce a valid number
2234
2389
  if (typeof value === 'string' && NUMERIC_REGEX.test(value)) {
2235
- const num = Number(value);
2236
- if (!isNaN(num)) {
2237
- return num;
2238
- }
2390
+ return +value; // Faster than Number(value)
2239
2391
  }
2240
2392
  return value;
2241
2393
  }
@@ -2253,24 +2405,47 @@ class SelectQueryBuilder {
2253
2405
  // Skip mapper transformation for performance - return items as-is
2254
2406
  return items;
2255
2407
  }
2256
- return items.map(item => {
2408
+ // Use pre-cached column metadata from target schema
2409
+ // This avoids repeated column.build() calls for each item
2410
+ const columnCache = targetSchema.columnMetadataCache;
2411
+ if (!columnCache || columnCache.size === 0) {
2412
+ // Fallback for schemas without cache (shouldn't happen, but be safe)
2413
+ return items.map(item => {
2414
+ const transformedItem = {};
2415
+ for (const [key, value] of Object.entries(item)) {
2416
+ const column = targetSchema.columns[key];
2417
+ if (column) {
2418
+ const config = column.build();
2419
+ transformedItem[key] = config.mapper
2420
+ ? config.mapper.fromDriver(value)
2421
+ : value;
2422
+ }
2423
+ else {
2424
+ transformedItem[key] = value;
2425
+ }
2426
+ }
2427
+ return transformedItem;
2428
+ });
2429
+ }
2430
+ // Optimized path using cached metadata and while(i--) loop
2431
+ const results = new Array(items.length);
2432
+ let i = items.length;
2433
+ while (i--) {
2434
+ const item = items[i];
2257
2435
  const transformedItem = {};
2258
- for (const [key, value] of Object.entries(item)) {
2259
- // Find the column in target schema
2260
- const column = targetSchema.columns[key];
2261
- if (column) {
2262
- const config = column.build();
2263
- // Apply fromDriver mapper if present
2264
- transformedItem[key] = config.mapper
2265
- ? config.mapper.fromDriver(value)
2266
- : value;
2436
+ for (const key in item) {
2437
+ const value = item[key];
2438
+ const cached = columnCache.get(key);
2439
+ if (cached && cached.hasMapper) {
2440
+ transformedItem[key] = cached.mapper.fromDriver(value);
2267
2441
  }
2268
2442
  else {
2269
2443
  transformedItem[key] = value;
2270
2444
  }
2271
2445
  }
2272
- return transformedItem;
2273
- });
2446
+ results[i] = transformedItem;
2447
+ }
2448
+ return results;
2274
2449
  }
2275
2450
  /**
2276
2451
  * Transform CTE aggregation items applying fromDriver mappers from selection metadata
@@ -2279,9 +2454,12 @@ class SelectQueryBuilder {
2279
2454
  if (!items || items.length === 0) {
2280
2455
  return [];
2281
2456
  }
2457
+ // Use pre-cached column metadata from schema
2458
+ const schemaColumnCache = this.schema.columnMetadataCache;
2282
2459
  // Build mapper cache from selection metadata
2283
2460
  const mapperCache = {};
2284
- for (const [key, value] of Object.entries(selectionMetadata)) {
2461
+ for (const key in selectionMetadata) {
2462
+ const value = selectionMetadata[key];
2285
2463
  // Check if value has getMapper (SqlFragment or field with mapper)
2286
2464
  if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
2287
2465
  let mapper = value.getMapper();
@@ -2296,29 +2474,45 @@ class SelectQueryBuilder {
2296
2474
  // Check if it's a FieldRef with schema column mapper
2297
2475
  else if (typeof value === 'object' && value !== null && '__fieldName' in value) {
2298
2476
  const fieldName = value.__fieldName;
2299
- const column = this.schema.columns[fieldName];
2300
- if (column) {
2301
- const config = column.build();
2302
- if (config.mapper && typeof config.mapper.fromDriver === 'function') {
2303
- mapperCache[key] = config.mapper;
2477
+ // Use cached column metadata instead of column.build()
2478
+ if (schemaColumnCache) {
2479
+ const cached = schemaColumnCache.get(fieldName);
2480
+ if (cached && cached.hasMapper && typeof cached.mapper.fromDriver === 'function') {
2481
+ mapperCache[key] = cached.mapper;
2482
+ }
2483
+ }
2484
+ else {
2485
+ // Fallback for schemas without cache
2486
+ const column = this.schema.columns[fieldName];
2487
+ if (column) {
2488
+ const config = column.build();
2489
+ if (config.mapper && typeof config.mapper.fromDriver === 'function') {
2490
+ mapperCache[key] = config.mapper;
2491
+ }
2304
2492
  }
2305
2493
  }
2306
2494
  }
2307
2495
  }
2308
- // Transform items
2309
- return items.map(item => {
2496
+ // Transform items using while(i--) loop - decrement and compare to 0 is fastest
2497
+ const results = new Array(items.length);
2498
+ let i = items.length;
2499
+ while (i--) {
2500
+ const item = items[i];
2310
2501
  const transformedItem = {};
2311
- for (const [key, value] of Object.entries(item)) {
2502
+ for (const key in item) {
2503
+ const value = item[key];
2312
2504
  const mapper = mapperCache[key];
2313
- if (mapper && value !== null && value !== undefined) {
2505
+ // Mappers handle null internally (mapWith wraps user functions)
2506
+ if (mapper) {
2314
2507
  transformedItem[key] = mapper.fromDriver(value);
2315
2508
  }
2316
2509
  else {
2317
2510
  transformedItem[key] = value;
2318
2511
  }
2319
2512
  }
2320
- return transformedItem;
2321
- });
2513
+ results[i] = transformedItem;
2514
+ }
2515
+ return results;
2322
2516
  }
2323
2517
  /**
2324
2518
  * Build aggregation query (MIN, MAX, SUM)
@@ -2564,13 +2758,22 @@ class ReferenceQueryBuilder {
2564
2758
  const mock = {};
2565
2759
  // Add columns - use pre-computed column name map if available
2566
2760
  const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
2761
+ // Performance: Lazy-cache FieldRef objects
2762
+ const fieldRefCache = {};
2763
+ const tableAlias = this.relationName;
2567
2764
  for (const [colName, dbColumnName] of columnNameMap) {
2568
2765
  Object.defineProperty(mock, colName, {
2569
- get: () => ({
2570
- __fieldName: colName,
2571
- __dbColumnName: dbColumnName,
2572
- __tableAlias: this.relationName, // Mark which table this belongs to
2573
- }),
2766
+ get() {
2767
+ let cached = fieldRefCache[colName];
2768
+ if (!cached) {
2769
+ cached = fieldRefCache[colName] = {
2770
+ __fieldName: colName,
2771
+ __dbColumnName: dbColumnName,
2772
+ __tableAlias: tableAlias, // Mark which table this belongs to
2773
+ };
2774
+ }
2775
+ return cached;
2776
+ },
2574
2777
  enumerable: true,
2575
2778
  configurable: true,
2576
2779
  });
@@ -2701,13 +2904,21 @@ class CollectionQueryBuilder {
2701
2904
  const mock = {};
2702
2905
  // Performance: Use pre-computed column name map if available
2703
2906
  const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
2907
+ // Performance: Lazy-cache FieldRef objects
2908
+ const fieldRefCache = {};
2704
2909
  // Add columns
2705
2910
  for (const [colName, dbColumnName] of columnNameMap) {
2706
2911
  Object.defineProperty(mock, colName, {
2707
- get: () => ({
2708
- __fieldName: colName,
2709
- __dbColumnName: dbColumnName,
2710
- }),
2912
+ get() {
2913
+ let cached = fieldRefCache[colName];
2914
+ if (!cached) {
2915
+ cached = fieldRefCache[colName] = {
2916
+ __fieldName: colName,
2917
+ __dbColumnName: dbColumnName,
2918
+ };
2919
+ }
2920
+ return cached;
2921
+ },
2711
2922
  enumerable: true,
2712
2923
  configurable: true,
2713
2924
  });
@@ -2998,8 +3209,10 @@ class CollectionQueryBuilder {
2998
3209
  resolveNavigationJoins(allTableAliases, joins, startSchema) {
2999
3210
  // Keep resolving until we've resolved all aliases or can't make progress
3000
3211
  let resolved = new Set();
3001
- let maxIterations = allTableAliases.size * 2; // Prevent infinite loops
3002
- while (resolved.size < allTableAliases.size && maxIterations-- > 0) {
3212
+ let lastResolvedCount = -1;
3213
+ let maxIterations = 100; // Prevent infinite loops
3214
+ while (resolved.size < allTableAliases.size && resolved.size !== lastResolvedCount && maxIterations-- > 0) {
3215
+ lastResolvedCount = resolved.size;
3003
3216
  // Build a map of already joined schemas for path resolution
3004
3217
  const joinedSchemas = new Map();
3005
3218
  joinedSchemas.set(this.targetTable, startSchema);
@@ -3018,20 +3231,81 @@ class CollectionQueryBuilder {
3018
3231
  resolved.add(alias);
3019
3232
  continue;
3020
3233
  }
3021
- // Look for this alias in any of the already joined schemas
3234
+ // First, look for this alias in any of the already joined schemas (direct lookup)
3235
+ let found = false;
3022
3236
  for (const [schemaAlias, schema] of joinedSchemas) {
3023
3237
  if (schema.relations && schema.relations[alias]) {
3024
3238
  const relation = schema.relations[alias];
3025
3239
  if (relation.type === 'one') {
3026
3240
  this.addNavigationJoin(alias, relation, joins, schemaAlias);
3027
3241
  resolved.add(alias);
3242
+ found = true;
3028
3243
  break;
3029
3244
  }
3030
3245
  }
3031
3246
  }
3247
+ // If not found directly, search transitively through all schemas in registry
3248
+ // to find an intermediate path
3249
+ if (!found && this.schemaRegistry) {
3250
+ const path = this.findNavigationPath(alias, joinedSchemas, startSchema);
3251
+ if (path.length > 0) {
3252
+ // Add all intermediate joins
3253
+ for (const step of path) {
3254
+ if (!joins.some(j => j.alias === step.alias)) {
3255
+ this.addNavigationJoin(step.alias, step.relation, joins, step.sourceAlias);
3256
+ }
3257
+ }
3258
+ resolved.add(alias);
3259
+ }
3260
+ }
3032
3261
  }
3033
3262
  }
3034
3263
  }
3264
+ /**
3265
+ * Find a path from already-joined schemas to the target alias
3266
+ * Uses BFS to find the shortest path through the schema graph
3267
+ */
3268
+ findNavigationPath(targetAlias, joinedSchemas, _startSchema) {
3269
+ if (!this.schemaRegistry) {
3270
+ return [];
3271
+ }
3272
+ // BFS to find path from any joined schema to the target alias
3273
+ const queue = [];
3274
+ // Start from all currently joined schemas
3275
+ for (const [schemaAlias, schema] of joinedSchemas) {
3276
+ queue.push({ schemaAlias, schema, path: [] });
3277
+ }
3278
+ const visited = new Set();
3279
+ for (const [alias] of joinedSchemas) {
3280
+ visited.add(alias);
3281
+ }
3282
+ while (queue.length > 0) {
3283
+ const { schemaAlias, schema, path } = queue.shift();
3284
+ if (!schema.relations) {
3285
+ continue;
3286
+ }
3287
+ for (const [relName, relConfig] of Object.entries(schema.relations)) {
3288
+ if (relConfig.type !== 'one') {
3289
+ continue; // Only follow reference (one-to-one/many-to-one) relations
3290
+ }
3291
+ if (visited.has(relName)) {
3292
+ continue;
3293
+ }
3294
+ const newPath = [...path, { alias: relName, relation: relConfig, sourceAlias: schemaAlias }];
3295
+ // Found the target!
3296
+ if (relName === targetAlias) {
3297
+ return newPath;
3298
+ }
3299
+ // Continue searching through this relation's schema
3300
+ visited.add(relName);
3301
+ const nextSchema = this.schemaRegistry.get(relConfig.targetTable);
3302
+ if (nextSchema) {
3303
+ queue.push({ schemaAlias: relName, schema: nextSchema, path: newPath });
3304
+ }
3305
+ }
3306
+ }
3307
+ return []; // No path found
3308
+ }
3035
3309
  /**
3036
3310
  * Build CTE for this collection query
3037
3311
  * Now delegates to collection strategy pattern
@@ -3065,6 +3339,50 @@ class CollectionQueryBuilder {
3065
3339
  context.paramCounter = sqlBuildContext.paramCounter;
3066
3340
  return { alias, expression: fragmentSql };
3067
3341
  }
3342
+ else if (field instanceof CollectionQueryBuilder) {
3343
+ // Nested collection query builder
3344
+ // For temptable strategy, nested collections are not supported - need lateral/CTE
3345
+ if (strategyType === 'temptable') {
3346
+ throw new Error(`Nested collections in temptable strategy are not supported. ` +
3347
+ `The field "${alias}" contains a nested collection query. ` +
3348
+ `Use collectionStrategy: 'lateral' or 'cte' for queries with nested collections.`);
3349
+ }
3350
+ // For lateral/CTE strategies, build the nested collection as a subquery
3351
+ const nestedCtx = {
3352
+ ...context,
3353
+ cteCounter: context.cteCounter,
3354
+ };
3355
+ const nestedResult = field.buildCTE(nestedCtx, client);
3356
+ context.cteCounter = nestedCtx.cteCounter;
3357
+ // For CTE/LATERAL strategy, we need to track the nested join
3358
+ // The nested aggregation needs to be joined in the outer collection's subquery
3359
+ if (nestedResult.tableName) {
3360
+ let nestedJoinClause;
3361
+ if (nestedResult.isCTE) {
3362
+ // CTE strategy: join by parent_id
3363
+ // The join should be: this.targetTable.id = nestedCte.parent_id
3364
+ nestedJoinClause = `LEFT JOIN "${nestedResult.tableName}" ON "${this.targetTable}"."id" = "${nestedResult.tableName}".parent_id`;
3365
+ }
3366
+ else if (nestedResult.joinClause) {
3367
+ // LATERAL strategy: use the provided join clause (contains full LATERAL subquery)
3368
+ nestedJoinClause = nestedResult.joinClause;
3369
+ }
3370
+ else {
3371
+ // Fallback for other strategies
3372
+ nestedJoinClause = `LEFT JOIN "${nestedResult.tableName}" ON "${this.targetTable}"."id" = "${nestedResult.tableName}".parent_id`;
3373
+ }
3374
+ return {
3375
+ alias,
3376
+ expression: nestedResult.selectExpression || nestedResult.sql,
3377
+ nestedCteJoin: {
3378
+ cteName: nestedResult.tableName,
3379
+ joinClause: nestedJoinClause,
3380
+ },
3381
+ };
3382
+ }
3383
+ // The nested collection becomes a correlated subquery in SELECT
3384
+ return { alias, expression: nestedResult.selectExpression || nestedResult.sql };
3385
+ }
3068
3386
  else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
3069
3387
  // FieldRef object - use database column name with optional table alias
3070
3388
  const dbColumnName = field.__dbColumnName;