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.
- package/dist/entity/db-context.d.ts +79 -0
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js +126 -1
- package/dist/entity/db-context.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -6
- package/dist/index.js.map +1 -1
- package/dist/query/collection-strategy.interface.d.ts +8 -0
- package/dist/query/collection-strategy.interface.d.ts.map +1 -1
- package/dist/query/conditions.js +2 -2
- package/dist/query/conditions.js.map +1 -1
- package/dist/query/cte-builder.d.ts.map +1 -1
- package/dist/query/cte-builder.js +27 -6
- package/dist/query/cte-builder.js.map +1 -1
- package/dist/query/grouped-query.d.ts +23 -0
- package/dist/query/grouped-query.d.ts.map +1 -1
- package/dist/query/grouped-query.js +337 -129
- package/dist/query/grouped-query.js.map +1 -1
- package/dist/query/query-builder.d.ts +5 -0
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +579 -282
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.d.ts +11 -5
- package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.js +36 -14
- package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.d.ts +18 -2
- package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/lateral-collection-strategy.js +140 -7
- package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.d.ts +9 -3
- package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.js +53 -13
- package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
- package/dist/query/subquery.d.ts +19 -2
- package/dist/query/subquery.d.ts.map +1 -1
- package/dist/query/subquery.js +12 -0
- package/dist/query/subquery.js.map +1 -1
- package/dist/schema/table-builder.d.ts +20 -1
- package/dist/schema/table-builder.d.ts.map +1 -1
- package/dist/schema/table-builder.js +11 -2
- package/dist/schema/table-builder.js.map +1 -1
- package/dist/types/custom-types.d.ts +4 -2
- package/dist/types/custom-types.d.ts.map +1 -1
- package/dist/types/custom-types.js +6 -4
- package/dist/types/custom-types.js.map +1 -1
- 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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1031
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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 =
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
dataMap.
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
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 "'
|
|
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 "'[]'::
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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}", '[]'::
|
|
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
|
|
1922
|
-
selectParts.push(`COALESCE("${cteName}".data, '[]'::
|
|
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
|
|
2050
|
-
// This
|
|
2051
|
-
const
|
|
2052
|
-
const
|
|
2053
|
-
//
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
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
|
-
|
|
2176
|
+
fieldConfigs.push({ key, type: 0 /* FieldType.NAVIGATION */, value: [] });
|
|
2177
|
+
continue;
|
|
2059
2178
|
}
|
|
2060
|
-
|
|
2061
|
-
|
|
2179
|
+
if (value === undefined) {
|
|
2180
|
+
fieldConfigs.push({ key, type: 0 /* FieldType.NAVIGATION */, value: undefined });
|
|
2181
|
+
continue;
|
|
2062
2182
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
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
|
|
2069
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, firstProp);
|
|
2189
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, props[0]);
|
|
2070
2190
|
if (descriptor && descriptor.get) {
|
|
2071
|
-
|
|
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
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
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
|
|
2083
|
-
if (
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
//
|
|
2102
|
-
if (
|
|
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
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
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
|
-
//
|
|
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] =
|
|
2304
|
+
result[key] = +rawValue;
|
|
2124
2305
|
}
|
|
2125
2306
|
else {
|
|
2126
2307
|
result[key] = rawValue;
|
|
2127
2308
|
}
|
|
2128
2309
|
}
|
|
2310
|
+
break;
|
|
2129
2311
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2257
|
-
|
|
2258
|
-
const
|
|
2259
|
-
if (
|
|
2260
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2298
|
-
if (
|
|
2299
|
-
const
|
|
2300
|
-
if (
|
|
2301
|
-
mapperCache[key] =
|
|
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
|
-
|
|
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
|
|
2479
|
+
for (const key in item) {
|
|
2480
|
+
const value = item[key];
|
|
2310
2481
|
const mapper = mapperCache[key];
|
|
2311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
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
|
-
|
|
2707
|
-
|
|
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
|
|
3000
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
3489
|
+
// JSON aggregation (default) - use JSON instead of JSONB for better performance
|
|
3193
3490
|
aggregationType = 'jsonb';
|
|
3194
|
-
defaultValue = "'[]'::
|
|
3491
|
+
defaultValue = "'[]'::json";
|
|
3195
3492
|
}
|
|
3196
3493
|
// Step 5: Detect navigation joins from the selected fields
|
|
3197
3494
|
const navigationJoins = [];
|