linkgress-orm 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +570 -275
- 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,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
|
|
@@ -1340,16 +1435,25 @@ class SelectQueryBuilder {
|
|
|
1340
1435
|
*/
|
|
1341
1436
|
createMockRow() {
|
|
1342
1437
|
const mock = {};
|
|
1438
|
+
const tableAlias = this.schema.name;
|
|
1343
1439
|
// Performance: Use pre-computed column name map if available
|
|
1344
1440
|
const columnNameMap = getColumnNameMapForSchema(this.schema);
|
|
1441
|
+
// Performance: Lazy-cache FieldRef objects
|
|
1442
|
+
const fieldRefCache = {};
|
|
1345
1443
|
// Add columns as FieldRef objects - type-safe with property name and database column name
|
|
1346
1444
|
for (const [colName, dbColumnName] of columnNameMap) {
|
|
1347
1445
|
Object.defineProperty(mock, colName, {
|
|
1348
|
-
get
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1446
|
+
get() {
|
|
1447
|
+
let cached = fieldRefCache[colName];
|
|
1448
|
+
if (!cached) {
|
|
1449
|
+
cached = fieldRefCache[colName] = {
|
|
1450
|
+
__fieldName: colName,
|
|
1451
|
+
__dbColumnName: dbColumnName,
|
|
1452
|
+
__tableAlias: tableAlias,
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
return cached;
|
|
1456
|
+
},
|
|
1353
1457
|
enumerable: true,
|
|
1354
1458
|
configurable: true,
|
|
1355
1459
|
});
|
|
@@ -1365,13 +1469,22 @@ class SelectQueryBuilder {
|
|
|
1365
1469
|
if (!mock[join.alias]) {
|
|
1366
1470
|
mock[join.alias] = {};
|
|
1367
1471
|
}
|
|
1472
|
+
// Lazy-cache for joined table
|
|
1473
|
+
const joinFieldRefCache = {};
|
|
1474
|
+
const joinAlias = join.alias;
|
|
1368
1475
|
for (const [colName, dbColumnName] of joinColumnNameMap) {
|
|
1369
1476
|
Object.defineProperty(mock[join.alias], colName, {
|
|
1370
|
-
get
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1477
|
+
get() {
|
|
1478
|
+
let cached = joinFieldRefCache[colName];
|
|
1479
|
+
if (!cached) {
|
|
1480
|
+
cached = joinFieldRefCache[colName] = {
|
|
1481
|
+
__fieldName: colName,
|
|
1482
|
+
__dbColumnName: dbColumnName,
|
|
1483
|
+
__tableAlias: joinAlias,
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
return cached;
|
|
1487
|
+
},
|
|
1375
1488
|
enumerable: true,
|
|
1376
1489
|
configurable: true,
|
|
1377
1490
|
});
|
|
@@ -2046,181 +2159,199 @@ class SelectQueryBuilder {
|
|
|
2046
2159
|
* Transform database results
|
|
2047
2160
|
*/
|
|
2048
2161
|
transformResults(rows, selection) {
|
|
2162
|
+
if (rows.length === 0) {
|
|
2163
|
+
return [];
|
|
2164
|
+
}
|
|
2049
2165
|
// Check if mappers are disabled for performance
|
|
2050
2166
|
const disableMappers = this.executor?.getOptions().disableMappers ?? false;
|
|
2051
|
-
// Pre-analyze selection structure
|
|
2052
|
-
// This
|
|
2053
|
-
const
|
|
2054
|
-
const
|
|
2055
|
-
//
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2167
|
+
// Pre-analyze selection structure ONCE and categorize each field
|
|
2168
|
+
// This moves all type checks out of the per-row loop
|
|
2169
|
+
const schemaColumnCache = this.schema.columnMetadataCache;
|
|
2170
|
+
const fieldConfigs = [];
|
|
2171
|
+
// Single pass to categorize all fields
|
|
2172
|
+
for (const key in selection) {
|
|
2173
|
+
const value = selection[key];
|
|
2174
|
+
// Check for navigation placeholders first (most common early exit)
|
|
2059
2175
|
if (Array.isArray(value) && value.length === 0) {
|
|
2060
|
-
|
|
2176
|
+
fieldConfigs.push({ key, type: 0 /* FieldType.NAVIGATION */, value: [] });
|
|
2177
|
+
continue;
|
|
2061
2178
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2179
|
+
if (value === undefined) {
|
|
2180
|
+
fieldConfigs.push({ key, type: 0 /* FieldType.NAVIGATION */, value: undefined });
|
|
2181
|
+
continue;
|
|
2064
2182
|
}
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2183
|
+
// Check for navigation property mocks (objects with getters)
|
|
2184
|
+
// These are treated as SIMPLE because the actual value comes from json_build_object in the row
|
|
2185
|
+
// The navigation mock is just a placeholder - actual data processing happens via FieldType.SIMPLE
|
|
2186
|
+
if (value && typeof value === 'object' && !('__dbColumnName' in value) && !('__fieldName' in value) && !('__collectionResult' in value) && !('__isAggregationArray' in value)) {
|
|
2068
2187
|
const props = Object.getOwnPropertyNames(value);
|
|
2069
2188
|
if (props.length > 0) {
|
|
2070
|
-
const
|
|
2071
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, firstProp);
|
|
2189
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, props[0]);
|
|
2072
2190
|
if (descriptor && descriptor.get) {
|
|
2073
|
-
|
|
2191
|
+
// Navigation mock - treat as simple, data will come from row via json_build_object
|
|
2192
|
+
// If row has no data, convertValue will return undefined
|
|
2193
|
+
fieldConfigs.push({ key, type: 8 /* FieldType.SIMPLE */, value });
|
|
2194
|
+
continue;
|
|
2074
2195
|
}
|
|
2075
2196
|
}
|
|
2076
2197
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2198
|
+
// Collection types
|
|
2199
|
+
if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
|
|
2200
|
+
const isScalarAgg = value instanceof CollectionQueryBuilder && value.isScalarAggregation();
|
|
2201
|
+
if (isScalarAgg) {
|
|
2202
|
+
const aggregationType = value.getAggregationType();
|
|
2203
|
+
fieldConfigs.push({
|
|
2204
|
+
key,
|
|
2205
|
+
type: 1 /* FieldType.COLLECTION_SCALAR */,
|
|
2206
|
+
value,
|
|
2207
|
+
aggregationType
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
else {
|
|
2211
|
+
const isArrayAgg = value && typeof value === 'object' && 'isArrayAggregation' in value && value.isArrayAggregation();
|
|
2212
|
+
if (isArrayAgg) {
|
|
2213
|
+
fieldConfigs.push({ key, type: 2 /* FieldType.COLLECTION_ARRAY */, value });
|
|
2214
|
+
}
|
|
2215
|
+
else {
|
|
2216
|
+
fieldConfigs.push({
|
|
2217
|
+
key,
|
|
2218
|
+
type: 3 /* FieldType.COLLECTION_JSON */,
|
|
2219
|
+
value,
|
|
2220
|
+
collectionBuilder: value instanceof CollectionQueryBuilder ? value : undefined
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
continue;
|
|
2225
|
+
}
|
|
2226
|
+
// CTE aggregation array
|
|
2227
|
+
if (typeof value === 'object' && value !== null && '__isAggregationArray' in value && value.__isAggregationArray) {
|
|
2228
|
+
fieldConfigs.push({
|
|
2229
|
+
key,
|
|
2230
|
+
type: 4 /* FieldType.CTE_AGGREGATION */,
|
|
2231
|
+
value,
|
|
2232
|
+
innerMetadata: value.__innerSelectionMetadata
|
|
2233
|
+
});
|
|
2234
|
+
continue;
|
|
2235
|
+
}
|
|
2236
|
+
// SqlFragment with mapper
|
|
2237
|
+
if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
|
|
2238
|
+
let mapper = disableMappers ? null : value.getMapper();
|
|
2239
|
+
if (mapper && typeof mapper.getType === 'function') {
|
|
2240
|
+
mapper = mapper.getType();
|
|
2241
|
+
}
|
|
2242
|
+
if (mapper && typeof mapper.fromDriver === 'function') {
|
|
2243
|
+
fieldConfigs.push({ key, type: 5 /* FieldType.SQL_FRAGMENT_MAPPER */, value, mapper });
|
|
2244
|
+
}
|
|
2245
|
+
else {
|
|
2246
|
+
fieldConfigs.push({ key, type: 8 /* FieldType.SIMPLE */, value });
|
|
2247
|
+
}
|
|
2248
|
+
continue;
|
|
2249
|
+
}
|
|
2250
|
+
// FieldRef with potential mapper
|
|
2251
|
+
if (typeof value === 'object' && value !== null && '__fieldName' in value) {
|
|
2252
|
+
if (disableMappers) {
|
|
2253
|
+
fieldConfigs.push({ key, type: 7 /* FieldType.FIELD_REF_NO_MAPPER */, value });
|
|
2254
|
+
}
|
|
2255
|
+
else {
|
|
2083
2256
|
const fieldName = value.__fieldName;
|
|
2084
|
-
const
|
|
2085
|
-
if (
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2257
|
+
const cached = schemaColumnCache?.get(fieldName);
|
|
2258
|
+
if (cached && cached.hasMapper) {
|
|
2259
|
+
fieldConfigs.push({ key, type: 6 /* FieldType.FIELD_REF_MAPPER */, value, mapper: cached.mapper });
|
|
2260
|
+
}
|
|
2261
|
+
else if (cached) {
|
|
2262
|
+
fieldConfigs.push({ key, type: 7 /* FieldType.FIELD_REF_NO_MAPPER */, value });
|
|
2263
|
+
}
|
|
2264
|
+
else {
|
|
2265
|
+
// Not in schema - treat as simple value
|
|
2266
|
+
fieldConfigs.push({ key, type: 8 /* FieldType.SIMPLE */, value });
|
|
2092
2267
|
}
|
|
2093
2268
|
}
|
|
2269
|
+
continue;
|
|
2094
2270
|
}
|
|
2271
|
+
// Default: simple value
|
|
2272
|
+
fieldConfigs.push({ key, type: 8 /* FieldType.SIMPLE */, value });
|
|
2095
2273
|
}
|
|
2096
|
-
|
|
2274
|
+
// Transform each row using pre-analyzed field configs
|
|
2275
|
+
// Using while(i--) for maximum performance - decrement and compare to 0 is faster
|
|
2276
|
+
const results = new Array(rows.length);
|
|
2277
|
+
const configCount = fieldConfigs.length;
|
|
2278
|
+
let rowIdx = rows.length;
|
|
2279
|
+
while (rowIdx--) {
|
|
2280
|
+
const row = rows[rowIdx];
|
|
2097
2281
|
const result = {};
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
//
|
|
2104
|
-
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;
|
|
2105
2290
|
continue;
|
|
2106
2291
|
}
|
|
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]);
|
|
2292
|
+
const rawValue = row[key];
|
|
2293
|
+
switch (config.type) {
|
|
2294
|
+
case 1 /* FieldType.COLLECTION_SCALAR */: {
|
|
2295
|
+
if (config.aggregationType === 'COUNT') {
|
|
2296
|
+
result[key] = this.convertValue(rawValue);
|
|
2117
2297
|
}
|
|
2118
2298
|
else {
|
|
2119
|
-
//
|
|
2120
|
-
const rawValue = row[key];
|
|
2299
|
+
// MAX/MIN/SUM: preserve NULL, convert numeric strings
|
|
2121
2300
|
if (rawValue === null) {
|
|
2122
2301
|
result[key] = null;
|
|
2123
2302
|
}
|
|
2124
2303
|
else if (typeof rawValue === 'string' && NUMERIC_REGEX.test(rawValue)) {
|
|
2125
|
-
result[key] =
|
|
2304
|
+
result[key] = +rawValue;
|
|
2126
2305
|
}
|
|
2127
2306
|
else {
|
|
2128
2307
|
result[key] = rawValue;
|
|
2129
2308
|
}
|
|
2130
2309
|
}
|
|
2310
|
+
break;
|
|
2131
2311
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2312
|
+
case 2 /* FieldType.COLLECTION_ARRAY */:
|
|
2313
|
+
result[key] = rawValue || [];
|
|
2314
|
+
break;
|
|
2315
|
+
case 3 /* FieldType.COLLECTION_JSON */: {
|
|
2316
|
+
const items = rawValue || [];
|
|
2317
|
+
if (config.collectionBuilder) {
|
|
2318
|
+
result[key] = this.transformCollectionItems(items, config.collectionBuilder);
|
|
2138
2319
|
}
|
|
2139
2320
|
else {
|
|
2140
|
-
|
|
2141
|
-
const collectionItems = row[key] || [];
|
|
2142
|
-
// Apply fromDriver mappers to collection items if needed
|
|
2143
|
-
if (value instanceof CollectionQueryBuilder) {
|
|
2144
|
-
result[key] = this.transformCollectionItems(collectionItems, value);
|
|
2145
|
-
}
|
|
2146
|
-
else {
|
|
2147
|
-
result[key] = collectionItems;
|
|
2148
|
-
}
|
|
2321
|
+
result[key] = items;
|
|
2149
2322
|
}
|
|
2323
|
+
break;
|
|
2150
2324
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
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
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
else {
|
|
2187
|
-
// No mapper or null value - convert normally
|
|
2188
|
-
result[key] = this.convertValue(rawValue);
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
else if (typeof value === 'object' && value !== null && '__fieldName' in value) {
|
|
2193
|
-
// FieldRef object - check if it has a custom mapper
|
|
2194
|
-
const rawValue = row[key];
|
|
2195
|
-
if (disableMappers) {
|
|
2196
|
-
// Skip mapper transformation for performance
|
|
2197
|
-
result[key] = rawValue === null ? undefined : rawValue;
|
|
2198
|
-
}
|
|
2199
|
-
else {
|
|
2200
|
-
// Use pre-cached column metadata instead of repeated lookups
|
|
2201
|
-
const cached = columnMetadataCache[key];
|
|
2202
|
-
if (cached) {
|
|
2203
|
-
// Field is in our schema - use cached mapper info
|
|
2204
|
-
result[key] = rawValue === null
|
|
2205
|
-
? undefined
|
|
2206
|
-
: (cached.hasMapper ? cached.mapper.fromDriver(rawValue) : rawValue);
|
|
2325
|
+
case 4 /* FieldType.CTE_AGGREGATION */: {
|
|
2326
|
+
const items = rawValue || [];
|
|
2327
|
+
if (config.innerMetadata && !disableMappers) {
|
|
2328
|
+
result[key] = this.transformCteAggregationItems(items, config.innerMetadata);
|
|
2207
2329
|
}
|
|
2208
2330
|
else {
|
|
2209
|
-
|
|
2210
|
-
// Always call convertValue to handle numeric string conversion
|
|
2211
|
-
result[key] = this.convertValue(row[key]);
|
|
2331
|
+
result[key] = items;
|
|
2212
2332
|
}
|
|
2333
|
+
break;
|
|
2213
2334
|
}
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2335
|
+
case 5 /* FieldType.SQL_FRAGMENT_MAPPER */:
|
|
2336
|
+
// mapWith wraps user functions to handle null
|
|
2337
|
+
result[key] = config.mapper.fromDriver(rawValue);
|
|
2338
|
+
break;
|
|
2339
|
+
case 6 /* FieldType.FIELD_REF_MAPPER */:
|
|
2340
|
+
// Column mappers (customType) - null check done here
|
|
2341
|
+
result[key] = config.mapper.fromDriver(rawValue);
|
|
2342
|
+
break;
|
|
2343
|
+
case 7 /* FieldType.FIELD_REF_NO_MAPPER */:
|
|
2344
|
+
result[key] = rawValue;
|
|
2345
|
+
break;
|
|
2346
|
+
case 8 /* FieldType.SIMPLE */:
|
|
2347
|
+
default:
|
|
2348
|
+
result[key] = this.convertValue(rawValue);
|
|
2349
|
+
break;
|
|
2220
2350
|
}
|
|
2221
2351
|
}
|
|
2222
|
-
|
|
2223
|
-
}
|
|
2352
|
+
results[rowIdx] = result;
|
|
2353
|
+
}
|
|
2354
|
+
return results;
|
|
2224
2355
|
}
|
|
2225
2356
|
/**
|
|
2226
2357
|
* Convert database values: null to undefined, numeric strings to numbers
|
|
@@ -2231,11 +2362,9 @@ class SelectQueryBuilder {
|
|
|
2231
2362
|
}
|
|
2232
2363
|
// Check if it's a numeric string (PostgreSQL NUMERIC type)
|
|
2233
2364
|
// This handles scalar subqueries with aggregates like AVG, SUM, etc.
|
|
2365
|
+
// The regex validates format, so Number() is guaranteed to produce a valid number
|
|
2234
2366
|
if (typeof value === 'string' && NUMERIC_REGEX.test(value)) {
|
|
2235
|
-
|
|
2236
|
-
if (!isNaN(num)) {
|
|
2237
|
-
return num;
|
|
2238
|
-
}
|
|
2367
|
+
return +value; // Faster than Number(value)
|
|
2239
2368
|
}
|
|
2240
2369
|
return value;
|
|
2241
2370
|
}
|
|
@@ -2253,24 +2382,47 @@ class SelectQueryBuilder {
|
|
|
2253
2382
|
// Skip mapper transformation for performance - return items as-is
|
|
2254
2383
|
return items;
|
|
2255
2384
|
}
|
|
2256
|
-
|
|
2385
|
+
// Use pre-cached column metadata from target schema
|
|
2386
|
+
// This avoids repeated column.build() calls for each item
|
|
2387
|
+
const columnCache = targetSchema.columnMetadataCache;
|
|
2388
|
+
if (!columnCache || columnCache.size === 0) {
|
|
2389
|
+
// Fallback for schemas without cache (shouldn't happen, but be safe)
|
|
2390
|
+
return items.map(item => {
|
|
2391
|
+
const transformedItem = {};
|
|
2392
|
+
for (const [key, value] of Object.entries(item)) {
|
|
2393
|
+
const column = targetSchema.columns[key];
|
|
2394
|
+
if (column) {
|
|
2395
|
+
const config = column.build();
|
|
2396
|
+
transformedItem[key] = config.mapper
|
|
2397
|
+
? config.mapper.fromDriver(value)
|
|
2398
|
+
: value;
|
|
2399
|
+
}
|
|
2400
|
+
else {
|
|
2401
|
+
transformedItem[key] = value;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
return transformedItem;
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
// Optimized path using cached metadata and while(i--) loop
|
|
2408
|
+
const results = new Array(items.length);
|
|
2409
|
+
let i = items.length;
|
|
2410
|
+
while (i--) {
|
|
2411
|
+
const item = items[i];
|
|
2257
2412
|
const transformedItem = {};
|
|
2258
|
-
for (const
|
|
2259
|
-
|
|
2260
|
-
const
|
|
2261
|
-
if (
|
|
2262
|
-
|
|
2263
|
-
// Apply fromDriver mapper if present
|
|
2264
|
-
transformedItem[key] = config.mapper
|
|
2265
|
-
? config.mapper.fromDriver(value)
|
|
2266
|
-
: value;
|
|
2413
|
+
for (const key in item) {
|
|
2414
|
+
const value = item[key];
|
|
2415
|
+
const cached = columnCache.get(key);
|
|
2416
|
+
if (cached && cached.hasMapper) {
|
|
2417
|
+
transformedItem[key] = cached.mapper.fromDriver(value);
|
|
2267
2418
|
}
|
|
2268
2419
|
else {
|
|
2269
2420
|
transformedItem[key] = value;
|
|
2270
2421
|
}
|
|
2271
2422
|
}
|
|
2272
|
-
|
|
2273
|
-
}
|
|
2423
|
+
results[i] = transformedItem;
|
|
2424
|
+
}
|
|
2425
|
+
return results;
|
|
2274
2426
|
}
|
|
2275
2427
|
/**
|
|
2276
2428
|
* Transform CTE aggregation items applying fromDriver mappers from selection metadata
|
|
@@ -2279,9 +2431,12 @@ class SelectQueryBuilder {
|
|
|
2279
2431
|
if (!items || items.length === 0) {
|
|
2280
2432
|
return [];
|
|
2281
2433
|
}
|
|
2434
|
+
// Use pre-cached column metadata from schema
|
|
2435
|
+
const schemaColumnCache = this.schema.columnMetadataCache;
|
|
2282
2436
|
// Build mapper cache from selection metadata
|
|
2283
2437
|
const mapperCache = {};
|
|
2284
|
-
for (const
|
|
2438
|
+
for (const key in selectionMetadata) {
|
|
2439
|
+
const value = selectionMetadata[key];
|
|
2285
2440
|
// Check if value has getMapper (SqlFragment or field with mapper)
|
|
2286
2441
|
if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
|
|
2287
2442
|
let mapper = value.getMapper();
|
|
@@ -2296,29 +2451,45 @@ class SelectQueryBuilder {
|
|
|
2296
2451
|
// Check if it's a FieldRef with schema column mapper
|
|
2297
2452
|
else if (typeof value === 'object' && value !== null && '__fieldName' in value) {
|
|
2298
2453
|
const fieldName = value.__fieldName;
|
|
2299
|
-
|
|
2300
|
-
if (
|
|
2301
|
-
const
|
|
2302
|
-
if (
|
|
2303
|
-
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
|
+
}
|
|
2304
2469
|
}
|
|
2305
2470
|
}
|
|
2306
2471
|
}
|
|
2307
2472
|
}
|
|
2308
|
-
// Transform items
|
|
2309
|
-
|
|
2473
|
+
// Transform items using while(i--) loop - decrement and compare to 0 is fastest
|
|
2474
|
+
const results = new Array(items.length);
|
|
2475
|
+
let i = items.length;
|
|
2476
|
+
while (i--) {
|
|
2477
|
+
const item = items[i];
|
|
2310
2478
|
const transformedItem = {};
|
|
2311
|
-
for (const
|
|
2479
|
+
for (const key in item) {
|
|
2480
|
+
const value = item[key];
|
|
2312
2481
|
const mapper = mapperCache[key];
|
|
2313
|
-
|
|
2482
|
+
// Mappers handle null internally (mapWith wraps user functions)
|
|
2483
|
+
if (mapper) {
|
|
2314
2484
|
transformedItem[key] = mapper.fromDriver(value);
|
|
2315
2485
|
}
|
|
2316
2486
|
else {
|
|
2317
2487
|
transformedItem[key] = value;
|
|
2318
2488
|
}
|
|
2319
2489
|
}
|
|
2320
|
-
|
|
2321
|
-
}
|
|
2490
|
+
results[i] = transformedItem;
|
|
2491
|
+
}
|
|
2492
|
+
return results;
|
|
2322
2493
|
}
|
|
2323
2494
|
/**
|
|
2324
2495
|
* Build aggregation query (MIN, MAX, SUM)
|
|
@@ -2564,13 +2735,22 @@ class ReferenceQueryBuilder {
|
|
|
2564
2735
|
const mock = {};
|
|
2565
2736
|
// Add columns - use pre-computed column name map if available
|
|
2566
2737
|
const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
|
|
2738
|
+
// Performance: Lazy-cache FieldRef objects
|
|
2739
|
+
const fieldRefCache = {};
|
|
2740
|
+
const tableAlias = this.relationName;
|
|
2567
2741
|
for (const [colName, dbColumnName] of columnNameMap) {
|
|
2568
2742
|
Object.defineProperty(mock, colName, {
|
|
2569
|
-
get
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2743
|
+
get() {
|
|
2744
|
+
let cached = fieldRefCache[colName];
|
|
2745
|
+
if (!cached) {
|
|
2746
|
+
cached = fieldRefCache[colName] = {
|
|
2747
|
+
__fieldName: colName,
|
|
2748
|
+
__dbColumnName: dbColumnName,
|
|
2749
|
+
__tableAlias: tableAlias, // Mark which table this belongs to
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
return cached;
|
|
2753
|
+
},
|
|
2574
2754
|
enumerable: true,
|
|
2575
2755
|
configurable: true,
|
|
2576
2756
|
});
|
|
@@ -2701,13 +2881,21 @@ class CollectionQueryBuilder {
|
|
|
2701
2881
|
const mock = {};
|
|
2702
2882
|
// Performance: Use pre-computed column name map if available
|
|
2703
2883
|
const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
|
|
2884
|
+
// Performance: Lazy-cache FieldRef objects
|
|
2885
|
+
const fieldRefCache = {};
|
|
2704
2886
|
// Add columns
|
|
2705
2887
|
for (const [colName, dbColumnName] of columnNameMap) {
|
|
2706
2888
|
Object.defineProperty(mock, colName, {
|
|
2707
|
-
get
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2889
|
+
get() {
|
|
2890
|
+
let cached = fieldRefCache[colName];
|
|
2891
|
+
if (!cached) {
|
|
2892
|
+
cached = fieldRefCache[colName] = {
|
|
2893
|
+
__fieldName: colName,
|
|
2894
|
+
__dbColumnName: dbColumnName,
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
return cached;
|
|
2898
|
+
},
|
|
2711
2899
|
enumerable: true,
|
|
2712
2900
|
configurable: true,
|
|
2713
2901
|
});
|
|
@@ -2998,8 +3186,10 @@ class CollectionQueryBuilder {
|
|
|
2998
3186
|
resolveNavigationJoins(allTableAliases, joins, startSchema) {
|
|
2999
3187
|
// Keep resolving until we've resolved all aliases or can't make progress
|
|
3000
3188
|
let resolved = new Set();
|
|
3001
|
-
let
|
|
3002
|
-
|
|
3189
|
+
let lastResolvedCount = -1;
|
|
3190
|
+
let maxIterations = 100; // Prevent infinite loops
|
|
3191
|
+
while (resolved.size < allTableAliases.size && resolved.size !== lastResolvedCount && maxIterations-- > 0) {
|
|
3192
|
+
lastResolvedCount = resolved.size;
|
|
3003
3193
|
// Build a map of already joined schemas for path resolution
|
|
3004
3194
|
const joinedSchemas = new Map();
|
|
3005
3195
|
joinedSchemas.set(this.targetTable, startSchema);
|
|
@@ -3018,19 +3208,80 @@ class CollectionQueryBuilder {
|
|
|
3018
3208
|
resolved.add(alias);
|
|
3019
3209
|
continue;
|
|
3020
3210
|
}
|
|
3021
|
-
//
|
|
3211
|
+
// First, look for this alias in any of the already joined schemas (direct lookup)
|
|
3212
|
+
let found = false;
|
|
3022
3213
|
for (const [schemaAlias, schema] of joinedSchemas) {
|
|
3023
3214
|
if (schema.relations && schema.relations[alias]) {
|
|
3024
3215
|
const relation = schema.relations[alias];
|
|
3025
3216
|
if (relation.type === 'one') {
|
|
3026
3217
|
this.addNavigationJoin(alias, relation, joins, schemaAlias);
|
|
3027
3218
|
resolved.add(alias);
|
|
3219
|
+
found = true;
|
|
3028
3220
|
break;
|
|
3029
3221
|
}
|
|
3030
3222
|
}
|
|
3031
3223
|
}
|
|
3224
|
+
// If not found directly, search transitively through all schemas in registry
|
|
3225
|
+
// to find an intermediate path
|
|
3226
|
+
if (!found && this.schemaRegistry) {
|
|
3227
|
+
const path = this.findNavigationPath(alias, joinedSchemas, startSchema);
|
|
3228
|
+
if (path.length > 0) {
|
|
3229
|
+
// Add all intermediate joins
|
|
3230
|
+
for (const step of path) {
|
|
3231
|
+
if (!joins.some(j => j.alias === step.alias)) {
|
|
3232
|
+
this.addNavigationJoin(step.alias, step.relation, joins, step.sourceAlias);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
resolved.add(alias);
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
/**
|
|
3242
|
+
* Find a path from already-joined schemas to the target alias
|
|
3243
|
+
* Uses BFS to find the shortest path through the schema graph
|
|
3244
|
+
*/
|
|
3245
|
+
findNavigationPath(targetAlias, joinedSchemas, _startSchema) {
|
|
3246
|
+
if (!this.schemaRegistry) {
|
|
3247
|
+
return [];
|
|
3248
|
+
}
|
|
3249
|
+
// BFS to find path from any joined schema to the target alias
|
|
3250
|
+
const queue = [];
|
|
3251
|
+
// Start from all currently joined schemas
|
|
3252
|
+
for (const [schemaAlias, schema] of joinedSchemas) {
|
|
3253
|
+
queue.push({ schemaAlias, schema, path: [] });
|
|
3254
|
+
}
|
|
3255
|
+
const visited = new Set();
|
|
3256
|
+
for (const [alias] of joinedSchemas) {
|
|
3257
|
+
visited.add(alias);
|
|
3258
|
+
}
|
|
3259
|
+
while (queue.length > 0) {
|
|
3260
|
+
const { schemaAlias, schema, path } = queue.shift();
|
|
3261
|
+
if (!schema.relations) {
|
|
3262
|
+
continue;
|
|
3263
|
+
}
|
|
3264
|
+
for (const [relName, relConfig] of Object.entries(schema.relations)) {
|
|
3265
|
+
if (relConfig.type !== 'one') {
|
|
3266
|
+
continue; // Only follow reference (one-to-one/many-to-one) relations
|
|
3267
|
+
}
|
|
3268
|
+
if (visited.has(relName)) {
|
|
3269
|
+
continue;
|
|
3270
|
+
}
|
|
3271
|
+
const newPath = [...path, { alias: relName, relation: relConfig, sourceAlias: schemaAlias }];
|
|
3272
|
+
// Found the target!
|
|
3273
|
+
if (relName === targetAlias) {
|
|
3274
|
+
return newPath;
|
|
3275
|
+
}
|
|
3276
|
+
// Continue searching through this relation's schema
|
|
3277
|
+
visited.add(relName);
|
|
3278
|
+
const nextSchema = this.schemaRegistry.get(relConfig.targetTable);
|
|
3279
|
+
if (nextSchema) {
|
|
3280
|
+
queue.push({ schemaAlias: relName, schema: nextSchema, path: newPath });
|
|
3281
|
+
}
|
|
3032
3282
|
}
|
|
3033
3283
|
}
|
|
3284
|
+
return []; // No path found
|
|
3034
3285
|
}
|
|
3035
3286
|
/**
|
|
3036
3287
|
* Build CTE for this collection query
|
|
@@ -3065,6 +3316,50 @@ class CollectionQueryBuilder {
|
|
|
3065
3316
|
context.paramCounter = sqlBuildContext.paramCounter;
|
|
3066
3317
|
return { alias, expression: fragmentSql };
|
|
3067
3318
|
}
|
|
3319
|
+
else if (field instanceof CollectionQueryBuilder) {
|
|
3320
|
+
// Nested collection query builder
|
|
3321
|
+
// For temptable strategy, nested collections are not supported - need lateral/CTE
|
|
3322
|
+
if (strategyType === 'temptable') {
|
|
3323
|
+
throw new Error(`Nested collections in temptable strategy are not supported. ` +
|
|
3324
|
+
`The field "${alias}" contains a nested collection query. ` +
|
|
3325
|
+
`Use collectionStrategy: 'lateral' or 'cte' for queries with nested collections.`);
|
|
3326
|
+
}
|
|
3327
|
+
// For lateral/CTE strategies, build the nested collection as a subquery
|
|
3328
|
+
const nestedCtx = {
|
|
3329
|
+
...context,
|
|
3330
|
+
cteCounter: context.cteCounter,
|
|
3331
|
+
};
|
|
3332
|
+
const nestedResult = field.buildCTE(nestedCtx, client);
|
|
3333
|
+
context.cteCounter = nestedCtx.cteCounter;
|
|
3334
|
+
// For CTE/LATERAL strategy, we need to track the nested join
|
|
3335
|
+
// The nested aggregation needs to be joined in the outer collection's subquery
|
|
3336
|
+
if (nestedResult.tableName) {
|
|
3337
|
+
let nestedJoinClause;
|
|
3338
|
+
if (nestedResult.isCTE) {
|
|
3339
|
+
// CTE strategy: join by parent_id
|
|
3340
|
+
// The join should be: this.targetTable.id = nestedCte.parent_id
|
|
3341
|
+
nestedJoinClause = `LEFT JOIN "${nestedResult.tableName}" ON "${this.targetTable}"."id" = "${nestedResult.tableName}".parent_id`;
|
|
3342
|
+
}
|
|
3343
|
+
else if (nestedResult.joinClause) {
|
|
3344
|
+
// LATERAL strategy: use the provided join clause (contains full LATERAL subquery)
|
|
3345
|
+
nestedJoinClause = nestedResult.joinClause;
|
|
3346
|
+
}
|
|
3347
|
+
else {
|
|
3348
|
+
// Fallback for other strategies
|
|
3349
|
+
nestedJoinClause = `LEFT JOIN "${nestedResult.tableName}" ON "${this.targetTable}"."id" = "${nestedResult.tableName}".parent_id`;
|
|
3350
|
+
}
|
|
3351
|
+
return {
|
|
3352
|
+
alias,
|
|
3353
|
+
expression: nestedResult.selectExpression || nestedResult.sql,
|
|
3354
|
+
nestedCteJoin: {
|
|
3355
|
+
cteName: nestedResult.tableName,
|
|
3356
|
+
joinClause: nestedJoinClause,
|
|
3357
|
+
},
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
// The nested collection becomes a correlated subquery in SELECT
|
|
3361
|
+
return { alias, expression: nestedResult.selectExpression || nestedResult.sql };
|
|
3362
|
+
}
|
|
3068
3363
|
else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
|
|
3069
3364
|
// FieldRef object - use database column name with optional table alias
|
|
3070
3365
|
const dbColumnName = field.__dbColumnName;
|