linkgress-orm 0.1.5 → 0.1.6

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 (44) 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 +126 -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.js +2 -2
  12. package/dist/query/conditions.js.map +1 -1
  13. package/dist/query/grouped-query.d.ts.map +1 -1
  14. package/dist/query/grouped-query.js +62 -26
  15. package/dist/query/grouped-query.js.map +1 -1
  16. package/dist/query/query-builder.d.ts +5 -0
  17. package/dist/query/query-builder.d.ts.map +1 -1
  18. package/dist/query/query-builder.js +570 -275
  19. package/dist/query/query-builder.js.map +1 -1
  20. package/dist/query/strategies/cte-collection-strategy.d.ts +5 -0
  21. package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
  22. package/dist/query/strategies/cte-collection-strategy.js +23 -2
  23. package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
  24. package/dist/query/strategies/lateral-collection-strategy.d.ts +5 -0
  25. package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
  26. package/dist/query/strategies/lateral-collection-strategy.js +20 -0
  27. package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
  28. package/dist/query/strategies/temptable-collection-strategy.d.ts +6 -0
  29. package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
  30. package/dist/query/strategies/temptable-collection-strategy.js +42 -2
  31. package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
  32. package/dist/query/subquery.d.ts +19 -2
  33. package/dist/query/subquery.d.ts.map +1 -1
  34. package/dist/query/subquery.js +12 -0
  35. package/dist/query/subquery.js.map +1 -1
  36. package/dist/schema/table-builder.d.ts +20 -1
  37. package/dist/schema/table-builder.d.ts.map +1 -1
  38. package/dist/schema/table-builder.js +11 -2
  39. package/dist/schema/table-builder.js.map +1 -1
  40. package/dist/types/custom-types.d.ts +4 -2
  41. package/dist/types/custom-types.d.ts.map +1 -1
  42. package/dist/types/custom-types.js +6 -4
  43. package/dist/types/custom-types.js.map +1 -1
  44. 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,25 @@ 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 = {};
189
209
  // Add columns as FieldRef objects - type-safe with property name and database column name
190
210
  for (const [colName, dbColumnName] of columnNameMap) {
191
211
  Object.defineProperty(mock, colName, {
192
- get: () => ({
193
- __fieldName: colName,
194
- __dbColumnName: dbColumnName,
195
- __tableAlias: this.schema.name,
196
- }),
212
+ get() {
213
+ let cached = fieldRefCache[colName];
214
+ if (!cached) {
215
+ cached = fieldRefCache[colName] = {
216
+ __fieldName: colName,
217
+ __dbColumnName: dbColumnName,
218
+ __tableAlias: tableAlias,
219
+ };
220
+ }
221
+ return cached;
222
+ },
197
223
  enumerable: true,
198
224
  configurable: true,
199
225
  });
@@ -339,14 +365,22 @@ class QueryBuilder {
339
365
  const mock = {};
340
366
  // Performance: Use pre-computed column name map if available
341
367
  const columnNameMap = getColumnNameMapForSchema(schema);
368
+ // Performance: Lazy-cache FieldRef objects
369
+ const fieldRefCache = {};
342
370
  // Add columns as FieldRef objects with table alias
343
371
  for (const [colName, dbColumnName] of columnNameMap) {
344
372
  Object.defineProperty(mock, colName, {
345
- get: () => ({
346
- __fieldName: colName,
347
- __dbColumnName: dbColumnName,
348
- __tableAlias: alias,
349
- }),
373
+ get() {
374
+ let cached = fieldRefCache[colName];
375
+ if (!cached) {
376
+ cached = fieldRefCache[colName] = {
377
+ __fieldName: colName,
378
+ __dbColumnName: dbColumnName,
379
+ __tableAlias: alias,
380
+ };
381
+ }
382
+ return cached;
383
+ },
350
384
  enumerable: true,
351
385
  configurable: true,
352
386
  });
@@ -711,14 +745,22 @@ class SelectQueryBuilder {
711
745
  const mock = {};
712
746
  // Performance: Use pre-computed column name map if available
713
747
  const columnNameMap = getColumnNameMapForSchema(schema);
748
+ // Performance: Lazy-cache FieldRef objects
749
+ const fieldRefCache = {};
714
750
  // Add columns as FieldRef objects with table alias
715
751
  for (const [colName, dbColumnName] of columnNameMap) {
716
752
  Object.defineProperty(mock, colName, {
717
- get: () => ({
718
- __fieldName: colName,
719
- __dbColumnName: dbColumnName,
720
- __tableAlias: alias,
721
- }),
753
+ get() {
754
+ let cached = fieldRefCache[colName];
755
+ if (!cached) {
756
+ cached = fieldRefCache[colName] = {
757
+ __fieldName: colName,
758
+ __dbColumnName: dbColumnName,
759
+ __tableAlias: alias,
760
+ };
761
+ }
762
+ return cached;
763
+ },
722
764
  enumerable: true,
723
765
  configurable: true,
724
766
  });
@@ -986,65 +1028,94 @@ class SelectQueryBuilder {
986
1028
  * Collection results are automatically resolved to arrays
987
1029
  */
988
1030
  async toList() {
989
- const context = {
1031
+ const options = this.executor?.getOptions();
1032
+ const tracer = new db_context_1.TimeTracer(options?.traceTime ?? false, options?.logger);
1033
+ // Query Build Phase
1034
+ tracer.startPhase('queryBuild');
1035
+ const context = tracer.trace('createContext', () => ({
990
1036
  ctes: new Map(),
991
1037
  cteCounter: 0,
992
1038
  paramCounter: 1,
993
1039
  allParams: [],
994
1040
  collectionStrategy: this.collectionStrategy,
995
1041
  executor: this.executor,
996
- };
1042
+ }));
997
1043
  // Analyze the selector to extract nested queries
998
- const mockRow = this.createMockRow();
999
- const selectionResult = this.selector(mockRow);
1044
+ const mockRow = tracer.trace('createMockRow', () => this.createMockRow());
1045
+ const selectionResult = tracer.trace('evaluateSelector', () => this.selector(mockRow));
1000
1046
  // Check if we're using temp table strategy and have collections
1001
- const collections = this.detectCollections(selectionResult);
1047
+ const collections = tracer.trace('detectCollections', () => this.detectCollections(selectionResult));
1002
1048
  const useTempTableStrategy = this.collectionStrategy === 'temptable' && collections.length > 0;
1049
+ tracer.endPhase();
1050
+ let results;
1003
1051
  if (useTempTableStrategy) {
1004
1052
  // Two-phase execution for temp table strategy
1005
- return this.executeWithTempTables(selectionResult, context, collections);
1053
+ results = await this.executeWithTempTables(selectionResult, context, collections, tracer);
1006
1054
  }
1007
1055
  else {
1008
1056
  // Single-phase execution for JSONB strategy (current behavior)
1009
- return this.executeSinglePhase(selectionResult, context);
1057
+ results = await this.executeSinglePhase(selectionResult, context, tracer);
1010
1058
  }
1059
+ // Log trace summary if tracing is enabled
1060
+ tracer.logSummary(results.length);
1061
+ return results;
1011
1062
  }
1012
1063
  /**
1013
1064
  * Execute query using single-phase approach (JSONB/CTE strategy)
1014
1065
  */
1015
- async executeSinglePhase(selectionResult, context) {
1066
+ async executeSinglePhase(selectionResult, context, tracer) {
1016
1067
  // Build the query
1017
- const { sql, params } = this.buildQuery(selectionResult, context);
1068
+ tracer.startPhase('queryBuild');
1069
+ const { sql, params } = tracer.trace('buildQuery', () => this.buildQuery(selectionResult, context));
1070
+ tracer.endPhase();
1018
1071
  // Execute using executor if available, otherwise use client directly
1019
- const result = this.executor
1072
+ tracer.startPhase('queryExecution');
1073
+ const result = await tracer.traceAsync('executeQuery', async () => this.executor
1020
1074
  ? await this.executor.query(sql, params)
1021
- : await this.client.query(sql, params);
1075
+ : await this.client.query(sql, params), { rowCount: 'pending' });
1076
+ tracer.endPhase();
1077
+ // If rawResult is enabled, return raw rows without any processing
1078
+ if (this.executor?.getOptions().rawResult) {
1079
+ return result.rows;
1080
+ }
1022
1081
  // Transform results
1023
- return this.transformResults(result.rows, selectionResult);
1082
+ tracer.startPhase('resultProcessing');
1083
+ const transformed = tracer.trace('transformResults', () => this.transformResults(result.rows, selectionResult), { rowCount: result.rows.length });
1084
+ tracer.endPhase();
1085
+ return transformed;
1024
1086
  }
1025
1087
  /**
1026
1088
  * Execute query using two-phase approach (temp table strategy)
1027
1089
  */
1028
- async executeWithTempTables(selectionResult, context, collections) {
1090
+ async executeWithTempTables(selectionResult, context, collections, tracer) {
1029
1091
  // Build base selection (excludes collections, includes foreign keys)
1030
- const baseSelection = this.buildBaseSelection(selectionResult, collections);
1031
- const { sql: baseSql, params: baseParams } = this.buildQuery(baseSelection, {
1092
+ tracer.startPhase('queryBuild');
1093
+ const baseSelection = tracer.trace('buildBaseSelection', () => this.buildBaseSelection(selectionResult, collections));
1094
+ const { sql: baseSql, params: baseParams } = tracer.trace('buildBaseQuery', () => this.buildQuery(baseSelection, {
1032
1095
  ...context,
1033
1096
  ctes: new Map(), // Clear CTEs since we're not using them for collections
1034
- });
1097
+ }));
1098
+ tracer.endPhase();
1035
1099
  // Check if we can use fully optimized single-query approach
1036
1100
  // Requirements: PostgresClient with querySimpleMulti support AND no parameters in base query
1037
1101
  const canUseFullOptimization = this.client.supportsMultiStatementQueries() &&
1038
1102
  baseParams.length === 0 &&
1039
1103
  collections.length > 0;
1040
1104
  if (canUseFullOptimization) {
1041
- return this.executeFullyOptimized(baseSql, baseSelection, selectionResult, context, collections);
1105
+ return this.executeFullyOptimized(baseSql, baseSelection, selectionResult, context, collections, tracer);
1042
1106
  }
1043
1107
  // Legacy two-phase approach: execute base query first
1044
- const baseResult = this.executor
1108
+ tracer.startPhase('queryExecution');
1109
+ const baseResult = await tracer.traceAsync('executeBaseQuery', async () => this.executor
1045
1110
  ? await this.executor.query(baseSql, baseParams)
1046
- : await this.client.query(baseSql, baseParams);
1111
+ : await this.client.query(baseSql, baseParams));
1112
+ // If rawResult is enabled, return raw rows without any processing
1113
+ if (this.executor?.getOptions().rawResult) {
1114
+ tracer.endPhase();
1115
+ return baseResult.rows;
1116
+ }
1047
1117
  if (baseResult.rows.length === 0) {
1118
+ tracer.endPhase();
1048
1119
  return [];
1049
1120
  }
1050
1121
  // Extract parent IDs from base results (using the known alias we added in buildBaseSelection)
@@ -1055,7 +1126,7 @@ class SelectQueryBuilder {
1055
1126
  for (const collection of collections) {
1056
1127
  const builder = collection.builder;
1057
1128
  // Call buildCTE with parent IDs - this will use the temp table strategy
1058
- const aggResult = await builder.buildCTE(context, this.client, parentIds);
1129
+ const aggResult = await tracer.traceAsync(`buildCTE:${collection.name}`, async () => builder.buildCTE(context, this.client, parentIds));
1059
1130
  // aggResult is a Promise<CollectionAggregationResult> for temp table strategy
1060
1131
  const result = await aggResult;
1061
1132
  // If the result has a tableName, it means temp tables were created and we need to query them
@@ -1068,9 +1139,9 @@ class SelectQueryBuilder {
1068
1139
  else {
1069
1140
  // Temp table strategy (legacy) - query the aggregation table
1070
1141
  const aggQuery = `SELECT parent_id, data FROM ${result.tableName}`;
1071
- const aggQueryResult = this.executor
1142
+ const aggQueryResult = await tracer.traceAsync(`queryCollection:${collection.name}`, async () => this.executor
1072
1143
  ? await this.executor.query(aggQuery, [])
1073
- : await this.client.query(aggQuery, []);
1144
+ : await this.client.query(aggQuery, []));
1074
1145
  // Cleanup temp tables if needed
1075
1146
  if (result.cleanupSql) {
1076
1147
  await this.client.query(result.cleanupSql);
@@ -1088,8 +1159,10 @@ class SelectQueryBuilder {
1088
1159
  throw new Error('Expected temp table result but got CTE');
1089
1160
  }
1090
1161
  }
1162
+ tracer.endPhase();
1091
1163
  // Phase 3: Merge base results with collection results
1092
- const mergedRows = baseResult.rows.map(baseRow => {
1164
+ tracer.startPhase('resultProcessing');
1165
+ const mergedRows = tracer.trace('mergeResults', () => baseResult.rows.map(baseRow => {
1093
1166
  const merged = { ...baseRow };
1094
1167
  for (const collection of collections) {
1095
1168
  const resultMap = collectionResults.get(collection.name);
@@ -1099,84 +1172,104 @@ class SelectQueryBuilder {
1099
1172
  // Remove the internal __pk_id field before returning
1100
1173
  delete merged.__pk_id;
1101
1174
  return merged;
1102
- });
1175
+ }), { rowCount: baseResult.rows.length });
1103
1176
  // Transform results using the original selection
1104
- return this.transformResults(mergedRows, selectionResult);
1177
+ const transformed = tracer.trace('transformResults', () => this.transformResults(mergedRows, selectionResult), { rowCount: mergedRows.length });
1178
+ tracer.endPhase();
1179
+ return transformed;
1105
1180
  }
1106
1181
  /**
1107
1182
  * Execute using fully optimized single-query approach (PostgresClient only)
1108
1183
  * Combines base query + all collections into ONE multi-statement query
1109
1184
  */
1110
- async executeFullyOptimized(baseSql, baseSelection, selectionResult, context, collections) {
1185
+ async executeFullyOptimized(baseSql, baseSelection, selectionResult, context, collections, tracer) {
1186
+ tracer.startPhase('queryBuild');
1111
1187
  const baseTempTable = `tmp_base_${context.cteCounter++}`;
1112
1188
  // 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}"`);
1189
+ const collectionSQLs = tracer.trace('buildCollectionSQLs', () => {
1190
+ const sqls = [];
1191
+ for (const collection of collections) {
1192
+ const builderAny = collection.builder;
1193
+ const targetTable = builderAny.targetTable;
1194
+ const foreignKey = builderAny.foreignKey;
1195
+ const selector = builderAny.selector;
1196
+ const orderByFields = builderAny.orderByFields || [];
1197
+ // Build selected fields
1198
+ let selectedFieldsSQL = '';
1199
+ if (selector) {
1200
+ const mockItem = builderAny.createMockItem();
1201
+ const selectedFields = selector(mockItem);
1202
+ const fieldParts = [];
1203
+ for (const [alias, field] of Object.entries(selectedFields)) {
1204
+ if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
1205
+ const dbColumnName = field.__dbColumnName;
1206
+ fieldParts.push(`"${dbColumnName}" as "${alias}"`);
1207
+ }
1130
1208
  }
1209
+ selectedFieldsSQL = fieldParts.join(', ');
1131
1210
  }
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
- }
1211
+ // Build ORDER BY
1212
+ let orderBySQL = orderByFields.length > 0
1213
+ ? ` ORDER BY ${orderByFields.map(({ field, direction }) => `"${field}" ${direction}`).join(', ')}`
1214
+ : ` ORDER BY "id" DESC`;
1215
+ const collectionSQL = `SELECT "${foreignKey}" as parent_id, ${selectedFieldsSQL} FROM "${targetTable}" WHERE "${foreignKey}" IN (SELECT "__pk_id" FROM ${baseTempTable})${orderBySQL}`;
1216
+ sqls.push(collectionSQL);
1217
+ }
1218
+ return sqls;
1219
+ });
1141
1220
  // 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');
1221
+ const multiStatementSQL = tracer.trace('buildMultiStatement', () => {
1222
+ const statements = [
1223
+ `CREATE TEMP TABLE ${baseTempTable} AS ${baseSql}`,
1224
+ `SELECT * FROM ${baseTempTable}`,
1225
+ ...collectionSQLs,
1226
+ `DROP TABLE IF EXISTS ${baseTempTable}`
1227
+ ];
1228
+ return statements.join(';\n');
1229
+ });
1230
+ tracer.endPhase();
1149
1231
  // Execute via querySimpleMulti
1232
+ tracer.startPhase('queryExecution');
1150
1233
  const executor = this.executor || this.client;
1151
1234
  let resultSets;
1152
1235
  if ('querySimpleMulti' in executor && typeof executor.querySimpleMulti === 'function') {
1153
- resultSets = await executor.querySimpleMulti(multiStatementSQL);
1236
+ resultSets = await tracer.traceAsync('executeMultiStatement', async () => executor.querySimpleMulti(multiStatementSQL));
1154
1237
  }
1155
1238
  else {
1156
1239
  throw new Error('Fully optimized mode requires querySimpleMulti support');
1157
1240
  }
1241
+ tracer.endPhase();
1158
1242
  // Parse result sets: [0]=CREATE, [1]=base, [2..N]=collections, [N+1]=DROP
1159
1243
  const baseResult = resultSets[1];
1244
+ // If rawResult is enabled, return raw rows without any processing
1245
+ if (this.executor?.getOptions().rawResult) {
1246
+ return baseResult?.rows || [];
1247
+ }
1160
1248
  if (!baseResult || baseResult.rows.length === 0) {
1161
1249
  return [];
1162
1250
  }
1251
+ // Result processing phase
1252
+ tracer.startPhase('resultProcessing');
1163
1253
  // 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);
1254
+ const collectionResults = tracer.trace('groupCollectionResults', () => {
1255
+ const results = new Map();
1256
+ collections.forEach((collection, idx) => {
1257
+ const collectionResultSet = resultSets[2 + idx];
1258
+ const dataMap = new Map();
1259
+ for (const row of collectionResultSet.rows) {
1260
+ const parentId = row.parent_id;
1261
+ if (!dataMap.has(parentId)) {
1262
+ dataMap.set(parentId, []);
1263
+ }
1264
+ const { parent_id, ...rowData } = row;
1265
+ dataMap.get(parentId).push(rowData);
1266
+ }
1267
+ results.set(collection.name, dataMap);
1268
+ });
1269
+ return results;
1177
1270
  });
1178
1271
  // Merge base results with collection results
1179
- const mergedRows = baseResult.rows.map((baseRow) => {
1272
+ const mergedRows = tracer.trace('mergeResults', () => baseResult.rows.map((baseRow) => {
1180
1273
  const merged = { ...baseRow };
1181
1274
  for (const collection of collections) {
1182
1275
  const resultMap = collectionResults.get(collection.name);
@@ -1185,8 +1278,10 @@ class SelectQueryBuilder {
1185
1278
  }
1186
1279
  delete merged.__pk_id;
1187
1280
  return merged;
1188
- });
1189
- return this.transformResults(mergedRows, selectionResult);
1281
+ }), { rowCount: baseResult.rows.length });
1282
+ const transformed = tracer.trace('transformResults', () => this.transformResults(mergedRows, selectionResult), { rowCount: mergedRows.length });
1283
+ tracer.endPhase();
1284
+ return transformed;
1190
1285
  }
1191
1286
  /**
1192
1287
  * Detect collections in the selection result
@@ -1340,16 +1435,25 @@ class SelectQueryBuilder {
1340
1435
  */
1341
1436
  createMockRow() {
1342
1437
  const mock = {};
1438
+ const tableAlias = this.schema.name;
1343
1439
  // Performance: Use pre-computed column name map if available
1344
1440
  const columnNameMap = getColumnNameMapForSchema(this.schema);
1441
+ // Performance: Lazy-cache FieldRef objects
1442
+ const fieldRefCache = {};
1345
1443
  // Add columns as FieldRef objects - type-safe with property name and database column name
1346
1444
  for (const [colName, dbColumnName] of columnNameMap) {
1347
1445
  Object.defineProperty(mock, colName, {
1348
- get: () => ({
1349
- __fieldName: colName,
1350
- __dbColumnName: dbColumnName,
1351
- __tableAlias: this.schema.name,
1352
- }),
1446
+ get() {
1447
+ let cached = fieldRefCache[colName];
1448
+ if (!cached) {
1449
+ cached = fieldRefCache[colName] = {
1450
+ __fieldName: colName,
1451
+ __dbColumnName: dbColumnName,
1452
+ __tableAlias: tableAlias,
1453
+ };
1454
+ }
1455
+ return cached;
1456
+ },
1353
1457
  enumerable: true,
1354
1458
  configurable: true,
1355
1459
  });
@@ -1365,13 +1469,22 @@ class SelectQueryBuilder {
1365
1469
  if (!mock[join.alias]) {
1366
1470
  mock[join.alias] = {};
1367
1471
  }
1472
+ // Lazy-cache for joined table
1473
+ const joinFieldRefCache = {};
1474
+ const joinAlias = join.alias;
1368
1475
  for (const [colName, dbColumnName] of joinColumnNameMap) {
1369
1476
  Object.defineProperty(mock[join.alias], colName, {
1370
- get: () => ({
1371
- __fieldName: colName,
1372
- __dbColumnName: dbColumnName,
1373
- __tableAlias: join.alias,
1374
- }),
1477
+ get() {
1478
+ let cached = joinFieldRefCache[colName];
1479
+ if (!cached) {
1480
+ cached = joinFieldRefCache[colName] = {
1481
+ __fieldName: colName,
1482
+ __dbColumnName: dbColumnName,
1483
+ __tableAlias: joinAlias,
1484
+ };
1485
+ }
1486
+ return cached;
1487
+ },
1375
1488
  enumerable: true,
1376
1489
  configurable: true,
1377
1490
  });
@@ -2046,181 +2159,199 @@ class SelectQueryBuilder {
2046
2159
  * Transform database results
2047
2160
  */
2048
2161
  transformResults(rows, selection) {
2162
+ if (rows.length === 0) {
2163
+ return [];
2164
+ }
2049
2165
  // Check if mappers are disabled for performance
2050
2166
  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) {
2167
+ // Pre-analyze selection structure ONCE and categorize each field
2168
+ // This moves all type checks out of the per-row loop
2169
+ const schemaColumnCache = this.schema.columnMetadataCache;
2170
+ const fieldConfigs = [];
2171
+ // Single pass to categorize all fields
2172
+ for (const key in selection) {
2173
+ const value = selection[key];
2174
+ // Check for navigation placeholders first (most common early exit)
2059
2175
  if (Array.isArray(value) && value.length === 0) {
2060
- navigationPlaceholders[key] = [];
2176
+ fieldConfigs.push({ key, type: 0 /* FieldType.NAVIGATION */, value: [] });
2177
+ continue;
2061
2178
  }
2062
- else if (value === undefined) {
2063
- navigationPlaceholders[key] = undefined;
2179
+ if (value === undefined) {
2180
+ fieldConfigs.push({ key, type: 0 /* FieldType.NAVIGATION */, value: undefined });
2181
+ continue;
2064
2182
  }
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
2183
+ // Check for navigation property mocks (objects with getters)
2184
+ // These are treated as SIMPLE because the actual value comes from json_build_object in the row
2185
+ // The navigation mock is just a placeholder - actual data processing happens via FieldType.SIMPLE
2186
+ if (value && typeof value === 'object' && !('__dbColumnName' in value) && !('__fieldName' in value) && !('__collectionResult' in value) && !('__isAggregationArray' in value)) {
2068
2187
  const props = Object.getOwnPropertyNames(value);
2069
2188
  if (props.length > 0) {
2070
- const firstProp = props[0];
2071
- const descriptor = Object.getOwnPropertyDescriptor(value, firstProp);
2189
+ const descriptor = Object.getOwnPropertyDescriptor(value, props[0]);
2072
2190
  if (descriptor && descriptor.get) {
2073
- navigationPlaceholders[key] = undefined;
2191
+ // Navigation mock - treat as simple, data will come from row via json_build_object
2192
+ // If row has no data, convertValue will return undefined
2193
+ fieldConfigs.push({ key, type: 8 /* FieldType.SIMPLE */, value });
2194
+ continue;
2074
2195
  }
2075
2196
  }
2076
2197
  }
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) {
2198
+ // Collection types
2199
+ if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
2200
+ const isScalarAgg = value instanceof CollectionQueryBuilder && value.isScalarAggregation();
2201
+ if (isScalarAgg) {
2202
+ const aggregationType = value.getAggregationType();
2203
+ fieldConfigs.push({
2204
+ key,
2205
+ type: 1 /* FieldType.COLLECTION_SCALAR */,
2206
+ value,
2207
+ aggregationType
2208
+ });
2209
+ }
2210
+ else {
2211
+ const isArrayAgg = value && typeof value === 'object' && 'isArrayAggregation' in value && value.isArrayAggregation();
2212
+ if (isArrayAgg) {
2213
+ fieldConfigs.push({ key, type: 2 /* FieldType.COLLECTION_ARRAY */, value });
2214
+ }
2215
+ else {
2216
+ fieldConfigs.push({
2217
+ key,
2218
+ type: 3 /* FieldType.COLLECTION_JSON */,
2219
+ value,
2220
+ collectionBuilder: value instanceof CollectionQueryBuilder ? value : undefined
2221
+ });
2222
+ }
2223
+ }
2224
+ continue;
2225
+ }
2226
+ // CTE aggregation array
2227
+ if (typeof value === 'object' && value !== null && '__isAggregationArray' in value && value.__isAggregationArray) {
2228
+ fieldConfigs.push({
2229
+ key,
2230
+ type: 4 /* FieldType.CTE_AGGREGATION */,
2231
+ value,
2232
+ innerMetadata: value.__innerSelectionMetadata
2233
+ });
2234
+ continue;
2235
+ }
2236
+ // SqlFragment with mapper
2237
+ if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
2238
+ let mapper = disableMappers ? null : value.getMapper();
2239
+ if (mapper && typeof mapper.getType === 'function') {
2240
+ mapper = mapper.getType();
2241
+ }
2242
+ if (mapper && typeof mapper.fromDriver === 'function') {
2243
+ fieldConfigs.push({ key, type: 5 /* FieldType.SQL_FRAGMENT_MAPPER */, value, mapper });
2244
+ }
2245
+ else {
2246
+ fieldConfigs.push({ key, type: 8 /* FieldType.SIMPLE */, value });
2247
+ }
2248
+ continue;
2249
+ }
2250
+ // FieldRef with potential mapper
2251
+ if (typeof value === 'object' && value !== null && '__fieldName' in value) {
2252
+ if (disableMappers) {
2253
+ fieldConfigs.push({ key, type: 7 /* FieldType.FIELD_REF_NO_MAPPER */, value });
2254
+ }
2255
+ else {
2083
2256
  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
- };
2257
+ const cached = schemaColumnCache?.get(fieldName);
2258
+ if (cached && cached.hasMapper) {
2259
+ fieldConfigs.push({ key, type: 6 /* FieldType.FIELD_REF_MAPPER */, value, mapper: cached.mapper });
2260
+ }
2261
+ else if (cached) {
2262
+ fieldConfigs.push({ key, type: 7 /* FieldType.FIELD_REF_NO_MAPPER */, value });
2263
+ }
2264
+ else {
2265
+ // Not in schema - treat as simple value
2266
+ fieldConfigs.push({ key, type: 8 /* FieldType.SIMPLE */, value });
2092
2267
  }
2093
2268
  }
2269
+ continue;
2094
2270
  }
2271
+ // Default: simple value
2272
+ fieldConfigs.push({ key, type: 8 /* FieldType.SIMPLE */, value });
2095
2273
  }
2096
- return rows.map(row => {
2274
+ // Transform each row using pre-analyzed field configs
2275
+ // Using while(i--) for maximum performance - decrement and compare to 0 is faster
2276
+ const results = new Array(rows.length);
2277
+ const configCount = fieldConfigs.length;
2278
+ let rowIdx = rows.length;
2279
+ while (rowIdx--) {
2280
+ const row = rows[rowIdx];
2097
2281
  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)) {
2282
+ let i = configCount;
2283
+ // Process all fields using pre-computed types
2284
+ while (i--) {
2285
+ const config = fieldConfigs[i];
2286
+ const key = config.key;
2287
+ // Handle navigation placeholders separately
2288
+ if (config.type === 0 /* FieldType.NAVIGATION */) {
2289
+ result[key] = config.value;
2105
2290
  continue;
2106
2291
  }
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]);
2292
+ const rawValue = row[key];
2293
+ switch (config.type) {
2294
+ case 1 /* FieldType.COLLECTION_SCALAR */: {
2295
+ if (config.aggregationType === 'COUNT') {
2296
+ result[key] = this.convertValue(rawValue);
2117
2297
  }
2118
2298
  else {
2119
- // For MAX/MIN/SUM, preserve NULL and convert numeric strings to numbers
2120
- const rawValue = row[key];
2299
+ // MAX/MIN/SUM: preserve NULL, convert numeric strings
2121
2300
  if (rawValue === null) {
2122
2301
  result[key] = null;
2123
2302
  }
2124
2303
  else if (typeof rawValue === 'string' && NUMERIC_REGEX.test(rawValue)) {
2125
- result[key] = Number(rawValue);
2304
+ result[key] = +rawValue;
2126
2305
  }
2127
2306
  else {
2128
2307
  result[key] = rawValue;
2129
2308
  }
2130
2309
  }
2310
+ break;
2131
2311
  }
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] || [];
2312
+ case 2 /* FieldType.COLLECTION_ARRAY */:
2313
+ result[key] = rawValue || [];
2314
+ break;
2315
+ case 3 /* FieldType.COLLECTION_JSON */: {
2316
+ const items = rawValue || [];
2317
+ if (config.collectionBuilder) {
2318
+ result[key] = this.transformCollectionItems(items, config.collectionBuilder);
2138
2319
  }
2139
2320
  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
- }
2321
+ result[key] = items;
2149
2322
  }
2323
+ break;
2150
2324
  }
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
- }
2185
- }
2186
- else {
2187
- // No mapper or null value - convert normally
2188
- result[key] = this.convertValue(rawValue);
2189
- }
2190
- }
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);
2325
+ case 4 /* FieldType.CTE_AGGREGATION */: {
2326
+ const items = rawValue || [];
2327
+ if (config.innerMetadata && !disableMappers) {
2328
+ result[key] = this.transformCteAggregationItems(items, config.innerMetadata);
2207
2329
  }
2208
2330
  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]);
2331
+ result[key] = items;
2212
2332
  }
2333
+ break;
2213
2334
  }
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;
2335
+ case 5 /* FieldType.SQL_FRAGMENT_MAPPER */:
2336
+ // mapWith wraps user functions to handle null
2337
+ result[key] = config.mapper.fromDriver(rawValue);
2338
+ break;
2339
+ case 6 /* FieldType.FIELD_REF_MAPPER */:
2340
+ // Column mappers (customType) - null check done here
2341
+ result[key] = config.mapper.fromDriver(rawValue);
2342
+ break;
2343
+ case 7 /* FieldType.FIELD_REF_NO_MAPPER */:
2344
+ result[key] = rawValue;
2345
+ break;
2346
+ case 8 /* FieldType.SIMPLE */:
2347
+ default:
2348
+ result[key] = this.convertValue(rawValue);
2349
+ break;
2220
2350
  }
2221
2351
  }
2222
- return result;
2223
- });
2352
+ results[rowIdx] = result;
2353
+ }
2354
+ return results;
2224
2355
  }
2225
2356
  /**
2226
2357
  * Convert database values: null to undefined, numeric strings to numbers
@@ -2231,11 +2362,9 @@ class SelectQueryBuilder {
2231
2362
  }
2232
2363
  // Check if it's a numeric string (PostgreSQL NUMERIC type)
2233
2364
  // This handles scalar subqueries with aggregates like AVG, SUM, etc.
2365
+ // The regex validates format, so Number() is guaranteed to produce a valid number
2234
2366
  if (typeof value === 'string' && NUMERIC_REGEX.test(value)) {
2235
- const num = Number(value);
2236
- if (!isNaN(num)) {
2237
- return num;
2238
- }
2367
+ return +value; // Faster than Number(value)
2239
2368
  }
2240
2369
  return value;
2241
2370
  }
@@ -2253,24 +2382,47 @@ class SelectQueryBuilder {
2253
2382
  // Skip mapper transformation for performance - return items as-is
2254
2383
  return items;
2255
2384
  }
2256
- return items.map(item => {
2385
+ // Use pre-cached column metadata from target schema
2386
+ // This avoids repeated column.build() calls for each item
2387
+ const columnCache = targetSchema.columnMetadataCache;
2388
+ if (!columnCache || columnCache.size === 0) {
2389
+ // Fallback for schemas without cache (shouldn't happen, but be safe)
2390
+ return items.map(item => {
2391
+ const transformedItem = {};
2392
+ for (const [key, value] of Object.entries(item)) {
2393
+ const column = targetSchema.columns[key];
2394
+ if (column) {
2395
+ const config = column.build();
2396
+ transformedItem[key] = config.mapper
2397
+ ? config.mapper.fromDriver(value)
2398
+ : value;
2399
+ }
2400
+ else {
2401
+ transformedItem[key] = value;
2402
+ }
2403
+ }
2404
+ return transformedItem;
2405
+ });
2406
+ }
2407
+ // Optimized path using cached metadata and while(i--) loop
2408
+ const results = new Array(items.length);
2409
+ let i = items.length;
2410
+ while (i--) {
2411
+ const item = items[i];
2257
2412
  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;
2413
+ for (const key in item) {
2414
+ const value = item[key];
2415
+ const cached = columnCache.get(key);
2416
+ if (cached && cached.hasMapper) {
2417
+ transformedItem[key] = cached.mapper.fromDriver(value);
2267
2418
  }
2268
2419
  else {
2269
2420
  transformedItem[key] = value;
2270
2421
  }
2271
2422
  }
2272
- return transformedItem;
2273
- });
2423
+ results[i] = transformedItem;
2424
+ }
2425
+ return results;
2274
2426
  }
2275
2427
  /**
2276
2428
  * Transform CTE aggregation items applying fromDriver mappers from selection metadata
@@ -2279,9 +2431,12 @@ class SelectQueryBuilder {
2279
2431
  if (!items || items.length === 0) {
2280
2432
  return [];
2281
2433
  }
2434
+ // Use pre-cached column metadata from schema
2435
+ const schemaColumnCache = this.schema.columnMetadataCache;
2282
2436
  // Build mapper cache from selection metadata
2283
2437
  const mapperCache = {};
2284
- for (const [key, value] of Object.entries(selectionMetadata)) {
2438
+ for (const key in selectionMetadata) {
2439
+ const value = selectionMetadata[key];
2285
2440
  // Check if value has getMapper (SqlFragment or field with mapper)
2286
2441
  if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
2287
2442
  let mapper = value.getMapper();
@@ -2296,29 +2451,45 @@ class SelectQueryBuilder {
2296
2451
  // Check if it's a FieldRef with schema column mapper
2297
2452
  else if (typeof value === 'object' && value !== null && '__fieldName' in value) {
2298
2453
  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;
2454
+ // Use cached column metadata instead of column.build()
2455
+ if (schemaColumnCache) {
2456
+ const cached = schemaColumnCache.get(fieldName);
2457
+ if (cached && cached.hasMapper && typeof cached.mapper.fromDriver === 'function') {
2458
+ mapperCache[key] = cached.mapper;
2459
+ }
2460
+ }
2461
+ else {
2462
+ // Fallback for schemas without cache
2463
+ const column = this.schema.columns[fieldName];
2464
+ if (column) {
2465
+ const config = column.build();
2466
+ if (config.mapper && typeof config.mapper.fromDriver === 'function') {
2467
+ mapperCache[key] = config.mapper;
2468
+ }
2304
2469
  }
2305
2470
  }
2306
2471
  }
2307
2472
  }
2308
- // Transform items
2309
- return items.map(item => {
2473
+ // Transform items using while(i--) loop - decrement and compare to 0 is fastest
2474
+ const results = new Array(items.length);
2475
+ let i = items.length;
2476
+ while (i--) {
2477
+ const item = items[i];
2310
2478
  const transformedItem = {};
2311
- for (const [key, value] of Object.entries(item)) {
2479
+ for (const key in item) {
2480
+ const value = item[key];
2312
2481
  const mapper = mapperCache[key];
2313
- if (mapper && value !== null && value !== undefined) {
2482
+ // Mappers handle null internally (mapWith wraps user functions)
2483
+ if (mapper) {
2314
2484
  transformedItem[key] = mapper.fromDriver(value);
2315
2485
  }
2316
2486
  else {
2317
2487
  transformedItem[key] = value;
2318
2488
  }
2319
2489
  }
2320
- return transformedItem;
2321
- });
2490
+ results[i] = transformedItem;
2491
+ }
2492
+ return results;
2322
2493
  }
2323
2494
  /**
2324
2495
  * Build aggregation query (MIN, MAX, SUM)
@@ -2564,13 +2735,22 @@ class ReferenceQueryBuilder {
2564
2735
  const mock = {};
2565
2736
  // Add columns - use pre-computed column name map if available
2566
2737
  const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
2738
+ // Performance: Lazy-cache FieldRef objects
2739
+ const fieldRefCache = {};
2740
+ const tableAlias = this.relationName;
2567
2741
  for (const [colName, dbColumnName] of columnNameMap) {
2568
2742
  Object.defineProperty(mock, colName, {
2569
- get: () => ({
2570
- __fieldName: colName,
2571
- __dbColumnName: dbColumnName,
2572
- __tableAlias: this.relationName, // Mark which table this belongs to
2573
- }),
2743
+ get() {
2744
+ let cached = fieldRefCache[colName];
2745
+ if (!cached) {
2746
+ cached = fieldRefCache[colName] = {
2747
+ __fieldName: colName,
2748
+ __dbColumnName: dbColumnName,
2749
+ __tableAlias: tableAlias, // Mark which table this belongs to
2750
+ };
2751
+ }
2752
+ return cached;
2753
+ },
2574
2754
  enumerable: true,
2575
2755
  configurable: true,
2576
2756
  });
@@ -2701,13 +2881,21 @@ class CollectionQueryBuilder {
2701
2881
  const mock = {};
2702
2882
  // Performance: Use pre-computed column name map if available
2703
2883
  const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
2884
+ // Performance: Lazy-cache FieldRef objects
2885
+ const fieldRefCache = {};
2704
2886
  // Add columns
2705
2887
  for (const [colName, dbColumnName] of columnNameMap) {
2706
2888
  Object.defineProperty(mock, colName, {
2707
- get: () => ({
2708
- __fieldName: colName,
2709
- __dbColumnName: dbColumnName,
2710
- }),
2889
+ get() {
2890
+ let cached = fieldRefCache[colName];
2891
+ if (!cached) {
2892
+ cached = fieldRefCache[colName] = {
2893
+ __fieldName: colName,
2894
+ __dbColumnName: dbColumnName,
2895
+ };
2896
+ }
2897
+ return cached;
2898
+ },
2711
2899
  enumerable: true,
2712
2900
  configurable: true,
2713
2901
  });
@@ -2998,8 +3186,10 @@ class CollectionQueryBuilder {
2998
3186
  resolveNavigationJoins(allTableAliases, joins, startSchema) {
2999
3187
  // Keep resolving until we've resolved all aliases or can't make progress
3000
3188
  let resolved = new Set();
3001
- let maxIterations = allTableAliases.size * 2; // Prevent infinite loops
3002
- while (resolved.size < allTableAliases.size && maxIterations-- > 0) {
3189
+ let lastResolvedCount = -1;
3190
+ let maxIterations = 100; // Prevent infinite loops
3191
+ while (resolved.size < allTableAliases.size && resolved.size !== lastResolvedCount && maxIterations-- > 0) {
3192
+ lastResolvedCount = resolved.size;
3003
3193
  // Build a map of already joined schemas for path resolution
3004
3194
  const joinedSchemas = new Map();
3005
3195
  joinedSchemas.set(this.targetTable, startSchema);
@@ -3018,19 +3208,80 @@ class CollectionQueryBuilder {
3018
3208
  resolved.add(alias);
3019
3209
  continue;
3020
3210
  }
3021
- // Look for this alias in any of the already joined schemas
3211
+ // First, look for this alias in any of the already joined schemas (direct lookup)
3212
+ let found = false;
3022
3213
  for (const [schemaAlias, schema] of joinedSchemas) {
3023
3214
  if (schema.relations && schema.relations[alias]) {
3024
3215
  const relation = schema.relations[alias];
3025
3216
  if (relation.type === 'one') {
3026
3217
  this.addNavigationJoin(alias, relation, joins, schemaAlias);
3027
3218
  resolved.add(alias);
3219
+ found = true;
3028
3220
  break;
3029
3221
  }
3030
3222
  }
3031
3223
  }
3224
+ // If not found directly, search transitively through all schemas in registry
3225
+ // to find an intermediate path
3226
+ if (!found && this.schemaRegistry) {
3227
+ const path = this.findNavigationPath(alias, joinedSchemas, startSchema);
3228
+ if (path.length > 0) {
3229
+ // Add all intermediate joins
3230
+ for (const step of path) {
3231
+ if (!joins.some(j => j.alias === step.alias)) {
3232
+ this.addNavigationJoin(step.alias, step.relation, joins, step.sourceAlias);
3233
+ }
3234
+ }
3235
+ resolved.add(alias);
3236
+ }
3237
+ }
3238
+ }
3239
+ }
3240
+ }
3241
+ /**
3242
+ * Find a path from already-joined schemas to the target alias
3243
+ * Uses BFS to find the shortest path through the schema graph
3244
+ */
3245
+ findNavigationPath(targetAlias, joinedSchemas, _startSchema) {
3246
+ if (!this.schemaRegistry) {
3247
+ return [];
3248
+ }
3249
+ // BFS to find path from any joined schema to the target alias
3250
+ const queue = [];
3251
+ // Start from all currently joined schemas
3252
+ for (const [schemaAlias, schema] of joinedSchemas) {
3253
+ queue.push({ schemaAlias, schema, path: [] });
3254
+ }
3255
+ const visited = new Set();
3256
+ for (const [alias] of joinedSchemas) {
3257
+ visited.add(alias);
3258
+ }
3259
+ while (queue.length > 0) {
3260
+ const { schemaAlias, schema, path } = queue.shift();
3261
+ if (!schema.relations) {
3262
+ continue;
3263
+ }
3264
+ for (const [relName, relConfig] of Object.entries(schema.relations)) {
3265
+ if (relConfig.type !== 'one') {
3266
+ continue; // Only follow reference (one-to-one/many-to-one) relations
3267
+ }
3268
+ if (visited.has(relName)) {
3269
+ continue;
3270
+ }
3271
+ const newPath = [...path, { alias: relName, relation: relConfig, sourceAlias: schemaAlias }];
3272
+ // Found the target!
3273
+ if (relName === targetAlias) {
3274
+ return newPath;
3275
+ }
3276
+ // Continue searching through this relation's schema
3277
+ visited.add(relName);
3278
+ const nextSchema = this.schemaRegistry.get(relConfig.targetTable);
3279
+ if (nextSchema) {
3280
+ queue.push({ schemaAlias: relName, schema: nextSchema, path: newPath });
3281
+ }
3032
3282
  }
3033
3283
  }
3284
+ return []; // No path found
3034
3285
  }
3035
3286
  /**
3036
3287
  * Build CTE for this collection query
@@ -3065,6 +3316,50 @@ class CollectionQueryBuilder {
3065
3316
  context.paramCounter = sqlBuildContext.paramCounter;
3066
3317
  return { alias, expression: fragmentSql };
3067
3318
  }
3319
+ else if (field instanceof CollectionQueryBuilder) {
3320
+ // Nested collection query builder
3321
+ // For temptable strategy, nested collections are not supported - need lateral/CTE
3322
+ if (strategyType === 'temptable') {
3323
+ throw new Error(`Nested collections in temptable strategy are not supported. ` +
3324
+ `The field "${alias}" contains a nested collection query. ` +
3325
+ `Use collectionStrategy: 'lateral' or 'cte' for queries with nested collections.`);
3326
+ }
3327
+ // For lateral/CTE strategies, build the nested collection as a subquery
3328
+ const nestedCtx = {
3329
+ ...context,
3330
+ cteCounter: context.cteCounter,
3331
+ };
3332
+ const nestedResult = field.buildCTE(nestedCtx, client);
3333
+ context.cteCounter = nestedCtx.cteCounter;
3334
+ // For CTE/LATERAL strategy, we need to track the nested join
3335
+ // The nested aggregation needs to be joined in the outer collection's subquery
3336
+ if (nestedResult.tableName) {
3337
+ let nestedJoinClause;
3338
+ if (nestedResult.isCTE) {
3339
+ // CTE strategy: join by parent_id
3340
+ // The join should be: this.targetTable.id = nestedCte.parent_id
3341
+ nestedJoinClause = `LEFT JOIN "${nestedResult.tableName}" ON "${this.targetTable}"."id" = "${nestedResult.tableName}".parent_id`;
3342
+ }
3343
+ else if (nestedResult.joinClause) {
3344
+ // LATERAL strategy: use the provided join clause (contains full LATERAL subquery)
3345
+ nestedJoinClause = nestedResult.joinClause;
3346
+ }
3347
+ else {
3348
+ // Fallback for other strategies
3349
+ nestedJoinClause = `LEFT JOIN "${nestedResult.tableName}" ON "${this.targetTable}"."id" = "${nestedResult.tableName}".parent_id`;
3350
+ }
3351
+ return {
3352
+ alias,
3353
+ expression: nestedResult.selectExpression || nestedResult.sql,
3354
+ nestedCteJoin: {
3355
+ cteName: nestedResult.tableName,
3356
+ joinClause: nestedJoinClause,
3357
+ },
3358
+ };
3359
+ }
3360
+ // The nested collection becomes a correlated subquery in SELECT
3361
+ return { alias, expression: nestedResult.selectExpression || nestedResult.sql };
3362
+ }
3068
3363
  else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
3069
3364
  // FieldRef object - use database column name with optional table alias
3070
3365
  const dbColumnName = field.__dbColumnName;