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