linkgress-orm 0.1.4 → 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 (48) 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/cte-builder.d.ts.map +1 -1
  14. package/dist/query/cte-builder.js +27 -6
  15. package/dist/query/cte-builder.js.map +1 -1
  16. package/dist/query/grouped-query.d.ts +23 -0
  17. package/dist/query/grouped-query.d.ts.map +1 -1
  18. package/dist/query/grouped-query.js +337 -129
  19. package/dist/query/grouped-query.js.map +1 -1
  20. package/dist/query/query-builder.d.ts +5 -0
  21. package/dist/query/query-builder.d.ts.map +1 -1
  22. package/dist/query/query-builder.js +579 -282
  23. package/dist/query/query-builder.js.map +1 -1
  24. package/dist/query/strategies/cte-collection-strategy.d.ts +11 -5
  25. package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
  26. package/dist/query/strategies/cte-collection-strategy.js +36 -14
  27. package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
  28. package/dist/query/strategies/lateral-collection-strategy.d.ts +18 -2
  29. package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
  30. package/dist/query/strategies/lateral-collection-strategy.js +140 -7
  31. package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
  32. package/dist/query/strategies/temptable-collection-strategy.d.ts +9 -3
  33. package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
  34. package/dist/query/strategies/temptable-collection-strategy.js +53 -13
  35. package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
  36. package/dist/query/subquery.d.ts +19 -2
  37. package/dist/query/subquery.d.ts.map +1 -1
  38. package/dist/query/subquery.js +12 -0
  39. package/dist/query/subquery.js.map +1 -1
  40. package/dist/schema/table-builder.d.ts +20 -1
  41. package/dist/schema/table-builder.d.ts.map +1 -1
  42. package/dist/schema/table-builder.js +11 -2
  43. package/dist/schema/table-builder.js.map +1 -1
  44. package/dist/types/custom-types.d.ts +4 -2
  45. package/dist/types/custom-types.d.ts.map +1 -1
  46. package/dist/types/custom-types.js +6 -4
  47. package/dist/types/custom-types.js.map +1 -1
  48. 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
@@ -1280,8 +1375,10 @@ class SelectQueryBuilder {
1280
1375
  getDefaultValueString(aggregationType) {
1281
1376
  switch (aggregationType) {
1282
1377
  case 'jsonb':
1378
+ // Use JSON instead of JSONB for better aggregation performance
1379
+ return "'[]'::json";
1283
1380
  case 'array':
1284
- return "'[]'::jsonb";
1381
+ return "'{}'";
1285
1382
  case 'count':
1286
1383
  return '0';
1287
1384
  case 'min':
@@ -1289,7 +1386,7 @@ class SelectQueryBuilder {
1289
1386
  case 'sum':
1290
1387
  return 'null';
1291
1388
  default:
1292
- return "'[]'::jsonb";
1389
+ return "'[]'::json";
1293
1390
  }
1294
1391
  }
1295
1392
  /**
@@ -1338,16 +1435,25 @@ class SelectQueryBuilder {
1338
1435
  */
1339
1436
  createMockRow() {
1340
1437
  const mock = {};
1438
+ const tableAlias = this.schema.name;
1341
1439
  // Performance: Use pre-computed column name map if available
1342
1440
  const columnNameMap = getColumnNameMapForSchema(this.schema);
1441
+ // Performance: Lazy-cache FieldRef objects
1442
+ const fieldRefCache = {};
1343
1443
  // Add columns as FieldRef objects - type-safe with property name and database column name
1344
1444
  for (const [colName, dbColumnName] of columnNameMap) {
1345
1445
  Object.defineProperty(mock, colName, {
1346
- get: () => ({
1347
- __fieldName: colName,
1348
- __dbColumnName: dbColumnName,
1349
- __tableAlias: this.schema.name,
1350
- }),
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
+ },
1351
1457
  enumerable: true,
1352
1458
  configurable: true,
1353
1459
  });
@@ -1363,13 +1469,22 @@ class SelectQueryBuilder {
1363
1469
  if (!mock[join.alias]) {
1364
1470
  mock[join.alias] = {};
1365
1471
  }
1472
+ // Lazy-cache for joined table
1473
+ const joinFieldRefCache = {};
1474
+ const joinAlias = join.alias;
1366
1475
  for (const [colName, dbColumnName] of joinColumnNameMap) {
1367
1476
  Object.defineProperty(mock[join.alias], colName, {
1368
- get: () => ({
1369
- __fieldName: colName,
1370
- __dbColumnName: dbColumnName,
1371
- __tableAlias: join.alias,
1372
- }),
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
+ },
1373
1488
  enumerable: true,
1374
1489
  configurable: true,
1375
1490
  });
@@ -1745,7 +1860,7 @@ class SelectQueryBuilder {
1745
1860
  const cteJoin = this.manualJoins.find(j => j.cte && j.cte.name === tableAlias);
1746
1861
  if (cteJoin && cteJoin.cte && cteJoin.cte.isAggregationColumn(columnName)) {
1747
1862
  // CTE aggregation column - wrap with COALESCE to return empty array instead of null
1748
- selectParts.push(`COALESCE("${tableAlias}"."${columnName}", '[]'::jsonb) as "${key}"`);
1863
+ selectParts.push(`COALESCE("${tableAlias}"."${columnName}", '[]'::json) as "${key}"`);
1749
1864
  }
1750
1865
  else {
1751
1866
  selectParts.push(`"${tableAlias}"."${columnName}" as "${key}"`);
@@ -1918,8 +2033,8 @@ class SelectQueryBuilder {
1918
2033
  selectParts.push(`COALESCE("${cteName}".data, ARRAY[]::${arrayType}) as "${name}"`);
1919
2034
  }
1920
2035
  else {
1921
- // For JSON aggregation, use jsonb type
1922
- selectParts.push(`COALESCE("${cteName}".data, '[]'::jsonb) as "${name}"`);
2036
+ // For JSON aggregation, use json type for better performance
2037
+ selectParts.push(`COALESCE("${cteName}".data, '[]'::json) as "${name}"`);
1923
2038
  }
1924
2039
  }
1925
2040
  // Build WHERE clause
@@ -2044,181 +2159,199 @@ class SelectQueryBuilder {
2044
2159
  * Transform database results
2045
2160
  */
2046
2161
  transformResults(rows, selection) {
2162
+ if (rows.length === 0) {
2163
+ return [];
2164
+ }
2047
2165
  // Check if mappers are disabled for performance
2048
2166
  const disableMappers = this.executor?.getOptions().disableMappers ?? false;
2049
- // Pre-analyze selection structure once instead of per-row
2050
- // This avoids repeated Object.entries() calls and type checks
2051
- const selectionKeys = Object.keys(selection);
2052
- const selectionEntries = Object.entries(selection);
2053
- // Pre-cache navigation placeholders to avoid repeated checks
2054
- // Only cache actual navigation properties (arrays and getter-based navigation)
2055
- const navigationPlaceholders = {};
2056
- 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)
2057
2175
  if (Array.isArray(value) && value.length === 0) {
2058
- navigationPlaceholders[key] = [];
2176
+ fieldConfigs.push({ key, type: 0 /* FieldType.NAVIGATION */, value: [] });
2177
+ continue;
2059
2178
  }
2060
- else if (value === undefined) {
2061
- navigationPlaceholders[key] = undefined;
2179
+ if (value === undefined) {
2180
+ fieldConfigs.push({ key, type: 0 /* FieldType.NAVIGATION */, value: undefined });
2181
+ continue;
2062
2182
  }
2063
- else if (value && typeof value === 'object' && !('__dbColumnName' in value) && !('__fieldName' in value)) {
2064
- // Check if it's a navigation property mock (object with getters)
2065
- // 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)) {
2066
2187
  const props = Object.getOwnPropertyNames(value);
2067
2188
  if (props.length > 0) {
2068
- const firstProp = props[0];
2069
- const descriptor = Object.getOwnPropertyDescriptor(value, firstProp);
2189
+ const descriptor = Object.getOwnPropertyDescriptor(value, props[0]);
2070
2190
  if (descriptor && descriptor.get) {
2071
- 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;
2072
2195
  }
2073
2196
  }
2074
2197
  }
2075
- }
2076
- // Pre-build column metadata cache to avoid repeated schema lookups
2077
- const columnMetadataCache = {};
2078
- if (!disableMappers) {
2079
- for (const [key, value] of selectionEntries) {
2080
- 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 {
2081
2256
  const fieldName = value.__fieldName;
2082
- const column = this.schema.columns[fieldName];
2083
- if (column) {
2084
- const config = column.build();
2085
- columnMetadataCache[key] = {
2086
- hasMapper: !!config.mapper,
2087
- mapper: config.mapper,
2088
- config: config,
2089
- };
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 });
2090
2267
  }
2091
2268
  }
2269
+ continue;
2092
2270
  }
2271
+ // Default: simple value
2272
+ fieldConfigs.push({ key, type: 8 /* FieldType.SIMPLE */, value });
2093
2273
  }
2094
- 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];
2095
2281
  const result = {};
2096
- // Copy navigation placeholders without iteration
2097
- Object.assign(result, navigationPlaceholders);
2098
- // Then process actual data fields
2099
- for (const [key, value] of selectionEntries) {
2100
- // Skip if we already set this key as a navigation placeholder
2101
- // UNLESS there's actual data for this key in the row (e.g., from json_build_object)
2102
- 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;
2103
2290
  continue;
2104
2291
  }
2105
- if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
2106
- // Check if this is a scalar aggregation (count, sum, max, min)
2107
- const isScalarAgg = value instanceof CollectionQueryBuilder && value.isScalarAggregation();
2108
- if (isScalarAgg) {
2109
- // For scalar aggregations, return the value directly
2110
- // For COUNT, convertValue will handle numeric conversion (NULL is already COALESCE'd to 0 in SQL)
2111
- // For MAX/MIN/SUM, we want to keep NULL as null (not undefined)
2112
- const aggregationType = value.getAggregationType();
2113
- if (aggregationType === 'COUNT') {
2114
- 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);
2115
2297
  }
2116
2298
  else {
2117
- // For MAX/MIN/SUM, preserve NULL and convert numeric strings to numbers
2118
- const rawValue = row[key];
2299
+ // MAX/MIN/SUM: preserve NULL, convert numeric strings
2119
2300
  if (rawValue === null) {
2120
2301
  result[key] = null;
2121
2302
  }
2122
2303
  else if (typeof rawValue === 'string' && NUMERIC_REGEX.test(rawValue)) {
2123
- result[key] = Number(rawValue);
2304
+ result[key] = +rawValue;
2124
2305
  }
2125
2306
  else {
2126
2307
  result[key] = rawValue;
2127
2308
  }
2128
2309
  }
2310
+ break;
2129
2311
  }
2130
- else {
2131
- // Check if this is a flattened array result (toNumberList/toStringList)
2132
- const isArrayAgg = value && typeof value === 'object' && 'isArrayAggregation' in value && value.isArrayAggregation();
2133
- if (isArrayAgg) {
2134
- // For flattened arrays, PostgreSQL returns a native array - use it directly
2135
- 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);
2136
2319
  }
2137
2320
  else {
2138
- // Parse JSON array from CTE (both CollectionQueryBuilder and CollectionResult are treated the same at runtime)
2139
- const collectionItems = row[key] || [];
2140
- // Apply fromDriver mappers to collection items if needed
2141
- if (value instanceof CollectionQueryBuilder) {
2142
- result[key] = this.transformCollectionItems(collectionItems, value);
2143
- }
2144
- else {
2145
- result[key] = collectionItems;
2146
- }
2321
+ result[key] = items;
2147
2322
  }
2323
+ break;
2148
2324
  }
2149
- }
2150
- else if (typeof value === 'object' && value !== null && '__isAggregationArray' in value && value.__isAggregationArray) {
2151
- // CTE withAggregation array - apply mappers to items inside
2152
- const collectionItems = row[key] || [];
2153
- const innerMetadata = value.__innerSelectionMetadata;
2154
- if (innerMetadata && !disableMappers) {
2155
- result[key] = this.transformCteAggregationItems(collectionItems, innerMetadata);
2156
- }
2157
- else {
2158
- result[key] = collectionItems;
2159
- }
2160
- }
2161
- else if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
2162
- // SqlFragment with custom mapper (check this BEFORE FieldRef to handle subquery/CTE fields with mappers)
2163
- const rawValue = row[key];
2164
- if (disableMappers) {
2165
- // Skip mapper transformation for performance
2166
- result[key] = this.convertValue(rawValue);
2167
- }
2168
- else {
2169
- let mapper = value.getMapper();
2170
- if (mapper && rawValue !== null && rawValue !== undefined) {
2171
- // If mapper is a CustomTypeBuilder, get the actual type
2172
- if (typeof mapper.getType === 'function') {
2173
- mapper = mapper.getType();
2174
- }
2175
- // Apply the fromDriver transformation
2176
- if (typeof mapper.fromDriver === 'function') {
2177
- result[key] = mapper.fromDriver(rawValue);
2178
- }
2179
- else {
2180
- // Fallback if fromDriver doesn't exist
2181
- result[key] = this.convertValue(rawValue);
2182
- }
2183
- }
2184
- else {
2185
- // No mapper or null value - convert normally
2186
- result[key] = this.convertValue(rawValue);
2187
- }
2188
- }
2189
- }
2190
- else if (typeof value === 'object' && value !== null && '__fieldName' in value) {
2191
- // FieldRef object - check if it has a custom mapper
2192
- const rawValue = row[key];
2193
- if (disableMappers) {
2194
- // Skip mapper transformation for performance
2195
- result[key] = rawValue === null ? undefined : rawValue;
2196
- }
2197
- else {
2198
- // Use pre-cached column metadata instead of repeated lookups
2199
- const cached = columnMetadataCache[key];
2200
- if (cached) {
2201
- // Field is in our schema - use cached mapper info
2202
- result[key] = rawValue === null
2203
- ? undefined
2204
- : (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);
2205
2329
  }
2206
2330
  else {
2207
- // Field not in schema (e.g., CTE field, joined table field)
2208
- // Always call convertValue to handle numeric string conversion
2209
- result[key] = this.convertValue(row[key]);
2331
+ result[key] = items;
2210
2332
  }
2333
+ break;
2211
2334
  }
2212
- }
2213
- else {
2214
- // Convert null to undefined for all other values
2215
- // Also convert numeric strings to numbers for scalar subqueries (PostgreSQL returns NUMERIC as string)
2216
- const converted = this.convertValue(row[key]);
2217
- 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;
2218
2350
  }
2219
2351
  }
2220
- return result;
2221
- });
2352
+ results[rowIdx] = result;
2353
+ }
2354
+ return results;
2222
2355
  }
2223
2356
  /**
2224
2357
  * Convert database values: null to undefined, numeric strings to numbers
@@ -2229,11 +2362,9 @@ class SelectQueryBuilder {
2229
2362
  }
2230
2363
  // Check if it's a numeric string (PostgreSQL NUMERIC type)
2231
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
2232
2366
  if (typeof value === 'string' && NUMERIC_REGEX.test(value)) {
2233
- const num = Number(value);
2234
- if (!isNaN(num)) {
2235
- return num;
2236
- }
2367
+ return +value; // Faster than Number(value)
2237
2368
  }
2238
2369
  return value;
2239
2370
  }
@@ -2251,24 +2382,47 @@ class SelectQueryBuilder {
2251
2382
  // Skip mapper transformation for performance - return items as-is
2252
2383
  return items;
2253
2384
  }
2254
- 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];
2255
2412
  const transformedItem = {};
2256
- for (const [key, value] of Object.entries(item)) {
2257
- // Find the column in target schema
2258
- const column = targetSchema.columns[key];
2259
- if (column) {
2260
- const config = column.build();
2261
- // Apply fromDriver mapper if present
2262
- transformedItem[key] = config.mapper
2263
- ? config.mapper.fromDriver(value)
2264
- : 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);
2265
2418
  }
2266
2419
  else {
2267
2420
  transformedItem[key] = value;
2268
2421
  }
2269
2422
  }
2270
- return transformedItem;
2271
- });
2423
+ results[i] = transformedItem;
2424
+ }
2425
+ return results;
2272
2426
  }
2273
2427
  /**
2274
2428
  * Transform CTE aggregation items applying fromDriver mappers from selection metadata
@@ -2277,9 +2431,12 @@ class SelectQueryBuilder {
2277
2431
  if (!items || items.length === 0) {
2278
2432
  return [];
2279
2433
  }
2434
+ // Use pre-cached column metadata from schema
2435
+ const schemaColumnCache = this.schema.columnMetadataCache;
2280
2436
  // Build mapper cache from selection metadata
2281
2437
  const mapperCache = {};
2282
- for (const [key, value] of Object.entries(selectionMetadata)) {
2438
+ for (const key in selectionMetadata) {
2439
+ const value = selectionMetadata[key];
2283
2440
  // Check if value has getMapper (SqlFragment or field with mapper)
2284
2441
  if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
2285
2442
  let mapper = value.getMapper();
@@ -2294,29 +2451,45 @@ class SelectQueryBuilder {
2294
2451
  // Check if it's a FieldRef with schema column mapper
2295
2452
  else if (typeof value === 'object' && value !== null && '__fieldName' in value) {
2296
2453
  const fieldName = value.__fieldName;
2297
- const column = this.schema.columns[fieldName];
2298
- if (column) {
2299
- const config = column.build();
2300
- if (config.mapper && typeof config.mapper.fromDriver === 'function') {
2301
- 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
+ }
2302
2469
  }
2303
2470
  }
2304
2471
  }
2305
2472
  }
2306
- // Transform items
2307
- 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];
2308
2478
  const transformedItem = {};
2309
- for (const [key, value] of Object.entries(item)) {
2479
+ for (const key in item) {
2480
+ const value = item[key];
2310
2481
  const mapper = mapperCache[key];
2311
- if (mapper && value !== null && value !== undefined) {
2482
+ // Mappers handle null internally (mapWith wraps user functions)
2483
+ if (mapper) {
2312
2484
  transformedItem[key] = mapper.fromDriver(value);
2313
2485
  }
2314
2486
  else {
2315
2487
  transformedItem[key] = value;
2316
2488
  }
2317
2489
  }
2318
- return transformedItem;
2319
- });
2490
+ results[i] = transformedItem;
2491
+ }
2492
+ return results;
2320
2493
  }
2321
2494
  /**
2322
2495
  * Build aggregation query (MIN, MAX, SUM)
@@ -2562,13 +2735,22 @@ class ReferenceQueryBuilder {
2562
2735
  const mock = {};
2563
2736
  // Add columns - use pre-computed column name map if available
2564
2737
  const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
2738
+ // Performance: Lazy-cache FieldRef objects
2739
+ const fieldRefCache = {};
2740
+ const tableAlias = this.relationName;
2565
2741
  for (const [colName, dbColumnName] of columnNameMap) {
2566
2742
  Object.defineProperty(mock, colName, {
2567
- get: () => ({
2568
- __fieldName: colName,
2569
- __dbColumnName: dbColumnName,
2570
- __tableAlias: this.relationName, // Mark which table this belongs to
2571
- }),
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
+ },
2572
2754
  enumerable: true,
2573
2755
  configurable: true,
2574
2756
  });
@@ -2699,13 +2881,21 @@ class CollectionQueryBuilder {
2699
2881
  const mock = {};
2700
2882
  // Performance: Use pre-computed column name map if available
2701
2883
  const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
2884
+ // Performance: Lazy-cache FieldRef objects
2885
+ const fieldRefCache = {};
2702
2886
  // Add columns
2703
2887
  for (const [colName, dbColumnName] of columnNameMap) {
2704
2888
  Object.defineProperty(mock, colName, {
2705
- get: () => ({
2706
- __fieldName: colName,
2707
- __dbColumnName: dbColumnName,
2708
- }),
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
+ },
2709
2899
  enumerable: true,
2710
2900
  configurable: true,
2711
2901
  });
@@ -2996,8 +3186,10 @@ class CollectionQueryBuilder {
2996
3186
  resolveNavigationJoins(allTableAliases, joins, startSchema) {
2997
3187
  // Keep resolving until we've resolved all aliases or can't make progress
2998
3188
  let resolved = new Set();
2999
- let maxIterations = allTableAliases.size * 2; // Prevent infinite loops
3000
- 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;
3001
3193
  // Build a map of already joined schemas for path resolution
3002
3194
  const joinedSchemas = new Map();
3003
3195
  joinedSchemas.set(this.targetTable, startSchema);
@@ -3016,19 +3208,80 @@ class CollectionQueryBuilder {
3016
3208
  resolved.add(alias);
3017
3209
  continue;
3018
3210
  }
3019
- // 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;
3020
3213
  for (const [schemaAlias, schema] of joinedSchemas) {
3021
3214
  if (schema.relations && schema.relations[alias]) {
3022
3215
  const relation = schema.relations[alias];
3023
3216
  if (relation.type === 'one') {
3024
3217
  this.addNavigationJoin(alias, relation, joins, schemaAlias);
3025
3218
  resolved.add(alias);
3219
+ found = true;
3026
3220
  break;
3027
3221
  }
3028
3222
  }
3029
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
+ }
3030
3282
  }
3031
3283
  }
3284
+ return []; // No path found
3032
3285
  }
3033
3286
  /**
3034
3287
  * Build CTE for this collection query
@@ -3063,6 +3316,50 @@ class CollectionQueryBuilder {
3063
3316
  context.paramCounter = sqlBuildContext.paramCounter;
3064
3317
  return { alias, expression: fragmentSql };
3065
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
+ }
3066
3363
  else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
3067
3364
  // FieldRef object - use database column name with optional table alias
3068
3365
  const dbColumnName = field.__dbColumnName;
@@ -3189,9 +3486,9 @@ class CollectionQueryBuilder {
3189
3486
  defaultValue = "'{}'"; // Empty array literal
3190
3487
  }
3191
3488
  else {
3192
- // JSONB aggregation (default)
3489
+ // JSON aggregation (default) - use JSON instead of JSONB for better performance
3193
3490
  aggregationType = 'jsonb';
3194
- defaultValue = "'[]'::jsonb";
3491
+ defaultValue = "'[]'::json";
3195
3492
  }
3196
3493
  // Step 5: Detect navigation joins from the selected fields
3197
3494
  const navigationJoins = [];