linkgress-orm 0.1.21 → 0.1.23
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.map +1 -1
- package/dist/entity/db-context.js +10 -1
- package/dist/entity/db-context.js.map +1 -1
- package/dist/query/collection-strategy.interface.d.ts +7 -1
- package/dist/query/collection-strategy.interface.d.ts.map +1 -1
- package/dist/query/grouped-query.d.ts.map +1 -1
- package/dist/query/grouped-query.js +12 -2
- package/dist/query/grouped-query.js.map +1 -1
- package/dist/query/join-builder.d.ts.map +1 -1
- package/dist/query/join-builder.js +13 -1
- package/dist/query/join-builder.js.map +1 -1
- package/dist/query/query-builder.d.ts +20 -0
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +199 -15
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/sql-utils.d.ts.map +1 -1
- package/dist/query/sql-utils.js +4 -0
- package/dist/query/sql-utils.js.map +1 -1
- package/dist/query/strategies/cte-collection-strategy.js +4 -4
- package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
- package/dist/query/strategies/temptable-collection-strategy.js +6 -6
- package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
- package/package.json +1 -1
|
@@ -1086,7 +1086,7 @@ class SelectQueryBuilder {
|
|
|
1086
1086
|
async executeSinglePhase(selectionResult, context, tracer) {
|
|
1087
1087
|
// Build the query
|
|
1088
1088
|
tracer.startPhase('queryBuild');
|
|
1089
|
-
const { sql, params } = tracer.trace('buildQuery', () => this.buildQuery(selectionResult, context));
|
|
1089
|
+
const { sql, params, nestedPaths } = tracer.trace('buildQuery', () => this.buildQuery(selectionResult, context));
|
|
1090
1090
|
tracer.endPhase();
|
|
1091
1091
|
// Execute using executor if available, otherwise use client directly
|
|
1092
1092
|
tracer.startPhase('queryExecution');
|
|
@@ -1098,9 +1098,14 @@ class SelectQueryBuilder {
|
|
|
1098
1098
|
if (this.executor?.getOptions().rawResult) {
|
|
1099
1099
|
return result.rows;
|
|
1100
1100
|
}
|
|
1101
|
-
//
|
|
1101
|
+
// Reconstruct nested objects from flat row data (if any)
|
|
1102
1102
|
tracer.startPhase('resultProcessing');
|
|
1103
|
-
|
|
1103
|
+
let rows = result.rows;
|
|
1104
|
+
if (nestedPaths.size > 0) {
|
|
1105
|
+
rows = tracer.trace('reconstructNestedObjects', () => rows.map(row => this.reconstructNestedObjects(row, nestedPaths)), { rowCount: rows.length });
|
|
1106
|
+
}
|
|
1107
|
+
// Transform results
|
|
1108
|
+
const transformed = tracer.trace('transformResults', () => this.transformResults(rows, selectionResult), { rowCount: rows.length });
|
|
1104
1109
|
tracer.endPhase();
|
|
1105
1110
|
return transformed;
|
|
1106
1111
|
}
|
|
@@ -1229,9 +1234,34 @@ class SelectQueryBuilder {
|
|
|
1229
1234
|
selectedFieldsSQL = fieldParts.join(', ');
|
|
1230
1235
|
}
|
|
1231
1236
|
// Build ORDER BY
|
|
1232
|
-
let orderBySQL
|
|
1233
|
-
|
|
1234
|
-
|
|
1237
|
+
let orderBySQL;
|
|
1238
|
+
const targetSchema = builderAny.targetTableSchema;
|
|
1239
|
+
if (orderByFields.length > 0) {
|
|
1240
|
+
const colNameMap = targetSchema ? getColumnNameMapForSchema(targetSchema) : null;
|
|
1241
|
+
orderBySQL = ` ORDER BY ${orderByFields.map(({ field, direction }) => {
|
|
1242
|
+
const dbColumnName = colNameMap?.get(field) ?? field;
|
|
1243
|
+
return `"${dbColumnName}" ${direction}`;
|
|
1244
|
+
}).join(', ')}`;
|
|
1245
|
+
}
|
|
1246
|
+
else {
|
|
1247
|
+
// Find primary key column from schema, fallback to "id" if not found
|
|
1248
|
+
let pkColumn = null;
|
|
1249
|
+
if (targetSchema) {
|
|
1250
|
+
for (const colBuilder of Object.values(targetSchema.columns)) {
|
|
1251
|
+
const config = colBuilder.build();
|
|
1252
|
+
if (config.primaryKey) {
|
|
1253
|
+
pkColumn = config.name;
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (pkColumn) {
|
|
1259
|
+
orderBySQL = ` ORDER BY "${pkColumn}" DESC`;
|
|
1260
|
+
}
|
|
1261
|
+
else {
|
|
1262
|
+
orderBySQL = ' ';
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1235
1265
|
const collectionSQL = `SELECT "${foreignKey}" as parent_id, ${selectedFieldsSQL} FROM "${targetTable}" WHERE "${foreignKey}" IN (SELECT "__pk_id" FROM ${baseTempTable})${orderBySQL}`;
|
|
1236
1266
|
sqls.push(collectionSQL);
|
|
1237
1267
|
}
|
|
@@ -1865,6 +1895,129 @@ class SelectQueryBuilder {
|
|
|
1865
1895
|
* Detect navigation property references in selection and add necessary JOINs
|
|
1866
1896
|
* Supports multi-level navigation like task.level.createdBy
|
|
1867
1897
|
*/
|
|
1898
|
+
/**
|
|
1899
|
+
* Try to build flat SQL SELECT parts for a nested object containing FieldRefs.
|
|
1900
|
+
* Uses path-encoded aliases (e.g., __nested__address__street) for JS-side reconstruction.
|
|
1901
|
+
* This is more performant than json_build_object() as it avoids JSON serialization overhead.
|
|
1902
|
+
*
|
|
1903
|
+
* @param obj The nested object from the selector
|
|
1904
|
+
* @param context Query context for parameter tracking
|
|
1905
|
+
* @param joins Array to add necessary JOINs
|
|
1906
|
+
* @param selectParts Array to add SELECT parts to
|
|
1907
|
+
* @param pathPrefix Current path prefix for alias encoding (e.g., "__nested__address")
|
|
1908
|
+
* @param nestedPaths Set to track all nested paths for reconstruction
|
|
1909
|
+
* @returns true if handled as nested object, false if not a valid nested object
|
|
1910
|
+
*/
|
|
1911
|
+
tryBuildFlatNestedSelect(obj, context, joins, selectParts, pathPrefix, nestedPaths) {
|
|
1912
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
1913
|
+
return false;
|
|
1914
|
+
}
|
|
1915
|
+
const entries = Object.entries(obj);
|
|
1916
|
+
if (entries.length === 0) {
|
|
1917
|
+
return false;
|
|
1918
|
+
}
|
|
1919
|
+
// First pass: check if this object contains any CollectionQueryBuilder instances
|
|
1920
|
+
// If so, this is NOT a plain nested object and needs special handling elsewhere
|
|
1921
|
+
for (const [, nestedValue] of entries) {
|
|
1922
|
+
if (nestedValue instanceof CollectionQueryBuilder) {
|
|
1923
|
+
return false; // Contains collections - let the main loop handle this
|
|
1924
|
+
}
|
|
1925
|
+
if (nestedValue && typeof nestedValue === 'object' && '__collectionResult' in nestedValue) {
|
|
1926
|
+
return false; // Contains collection result marker
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
// Track this path as a nested object
|
|
1930
|
+
nestedPaths.add(pathPrefix);
|
|
1931
|
+
for (const [nestedKey, nestedValue] of entries) {
|
|
1932
|
+
const fieldPath = `${pathPrefix}__${nestedKey}`;
|
|
1933
|
+
if (nestedValue instanceof conditions_1.SqlFragment) {
|
|
1934
|
+
// SQL Fragment - build the SQL expression
|
|
1935
|
+
const sqlBuildContext = {
|
|
1936
|
+
paramCounter: context.paramCounter,
|
|
1937
|
+
params: context.allParams,
|
|
1938
|
+
};
|
|
1939
|
+
const fragmentSql = nestedValue.buildSql(sqlBuildContext);
|
|
1940
|
+
context.paramCounter = sqlBuildContext.paramCounter;
|
|
1941
|
+
selectParts.push(`${fragmentSql} as "${fieldPath}"`);
|
|
1942
|
+
}
|
|
1943
|
+
else if (typeof nestedValue === 'object' && nestedValue !== null && '__dbColumnName' in nestedValue) {
|
|
1944
|
+
// FieldRef - extract table alias and column name
|
|
1945
|
+
const tableAlias = ('__tableAlias' in nestedValue && nestedValue.__tableAlias)
|
|
1946
|
+
? nestedValue.__tableAlias
|
|
1947
|
+
: this.schema.name;
|
|
1948
|
+
const columnName = nestedValue.__dbColumnName;
|
|
1949
|
+
// Add JOIN if needed for navigation fields
|
|
1950
|
+
if (tableAlias !== this.schema.name) {
|
|
1951
|
+
const relConfig = this.schema.relations[tableAlias];
|
|
1952
|
+
if (relConfig && !joins.find(j => j.alias === tableAlias)) {
|
|
1953
|
+
let targetSchema;
|
|
1954
|
+
if (relConfig.targetTableBuilder) {
|
|
1955
|
+
const targetTableSchema = relConfig.targetTableBuilder.build();
|
|
1956
|
+
targetSchema = targetTableSchema.schema;
|
|
1957
|
+
}
|
|
1958
|
+
joins.push({
|
|
1959
|
+
alias: tableAlias,
|
|
1960
|
+
targetTable: relConfig.targetTable,
|
|
1961
|
+
targetSchema,
|
|
1962
|
+
foreignKeys: relConfig.foreignKeys || [relConfig.foreignKey || ''],
|
|
1963
|
+
matches: relConfig.matches || [],
|
|
1964
|
+
isMandatory: relConfig.isMandatory ?? false,
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
selectParts.push(`"${tableAlias}"."${columnName}" as "${fieldPath}"`);
|
|
1969
|
+
}
|
|
1970
|
+
else if (typeof nestedValue === 'object' && nestedValue !== null && !Array.isArray(nestedValue)) {
|
|
1971
|
+
// Recursively handle deeper nested objects
|
|
1972
|
+
const handled = this.tryBuildFlatNestedSelect(nestedValue, context, joins, selectParts, fieldPath, nestedPaths);
|
|
1973
|
+
if (!handled) {
|
|
1974
|
+
// Not a valid nested object structure - return false to let caller handle
|
|
1975
|
+
return false;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
else if (nestedValue === undefined || nestedValue === null) {
|
|
1979
|
+
selectParts.push(`NULL as "${fieldPath}"`);
|
|
1980
|
+
}
|
|
1981
|
+
else {
|
|
1982
|
+
// Literal value (string, number, boolean)
|
|
1983
|
+
selectParts.push(`$${context.paramCounter++} as "${fieldPath}"`);
|
|
1984
|
+
context.allParams.push(nestedValue);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
return true;
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Reconstruct nested objects from flat row data with path-encoded column names.
|
|
1991
|
+
* Transforms { "__nested__address__street": "Main St", "__nested__address__city": "NYC" }
|
|
1992
|
+
* into { address: { street: "Main St", city: "NYC" } }
|
|
1993
|
+
*/
|
|
1994
|
+
reconstructNestedObjects(row, nestedPaths) {
|
|
1995
|
+
if (nestedPaths.size === 0) {
|
|
1996
|
+
return row;
|
|
1997
|
+
}
|
|
1998
|
+
const result = {};
|
|
1999
|
+
const nestedPrefix = '__nested__';
|
|
2000
|
+
for (const [key, value] of Object.entries(row)) {
|
|
2001
|
+
if (key.startsWith(nestedPrefix)) {
|
|
2002
|
+
// This is a nested field - parse the path and set the value
|
|
2003
|
+
const pathParts = key.substring(nestedPrefix.length).split('__');
|
|
2004
|
+
let current = result;
|
|
2005
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
2006
|
+
const part = pathParts[i];
|
|
2007
|
+
if (!(part in current)) {
|
|
2008
|
+
current[part] = {};
|
|
2009
|
+
}
|
|
2010
|
+
current = current[part];
|
|
2011
|
+
}
|
|
2012
|
+
current[pathParts[pathParts.length - 1]] = value;
|
|
2013
|
+
}
|
|
2014
|
+
else {
|
|
2015
|
+
// Regular field
|
|
2016
|
+
result[key] = value;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
return result;
|
|
2020
|
+
}
|
|
1868
2021
|
detectAndAddJoinsFromSelection(selection, joins) {
|
|
1869
2022
|
if (!selection || typeof selection !== 'object') {
|
|
1870
2023
|
return;
|
|
@@ -2035,6 +2188,7 @@ class SelectQueryBuilder {
|
|
|
2035
2188
|
const selectParts = [];
|
|
2036
2189
|
const collectionFields = [];
|
|
2037
2190
|
const joins = [];
|
|
2191
|
+
const nestedPaths = new Set(); // Track nested object paths for JS-side reconstruction
|
|
2038
2192
|
// Scan selection for navigation property references and add JOINs
|
|
2039
2193
|
this.detectAndAddJoinsFromSelection(selection, joins);
|
|
2040
2194
|
// Scan WHERE condition for navigation property references and add JOINs
|
|
@@ -2253,6 +2407,13 @@ class SelectQueryBuilder {
|
|
|
2253
2407
|
continue;
|
|
2254
2408
|
}
|
|
2255
2409
|
}
|
|
2410
|
+
// Check if this is a plain nested object containing FieldRefs
|
|
2411
|
+
// e.g., address: { street: p.street, city: p.city }
|
|
2412
|
+
// Use flat select with path-encoded aliases for better performance
|
|
2413
|
+
const handled = this.tryBuildFlatNestedSelect(value, context, joins, selectParts, `__nested__${key}`, nestedPaths);
|
|
2414
|
+
if (handled) {
|
|
2415
|
+
continue;
|
|
2416
|
+
}
|
|
2256
2417
|
}
|
|
2257
2418
|
// Otherwise, treat as literal value
|
|
2258
2419
|
selectParts.push(`$${context.paramCounter++} as "${key}"`);
|
|
@@ -2318,6 +2479,8 @@ class SelectQueryBuilder {
|
|
|
2318
2479
|
// Build ORDER BY clause
|
|
2319
2480
|
let orderByClause = '';
|
|
2320
2481
|
if (this.orderByFields.length > 0) {
|
|
2482
|
+
// Performance: Pre-compute column name map for ORDER BY lookups
|
|
2483
|
+
const colNameMap = getColumnNameMapForSchema(this.schema);
|
|
2321
2484
|
const orderParts = this.orderByFields.map(({ field, direction }) => {
|
|
2322
2485
|
// Check if the field is in the selection (after a select() call)
|
|
2323
2486
|
// If so, reference it as an alias, otherwise use table.column notation
|
|
@@ -2327,7 +2490,9 @@ class SelectQueryBuilder {
|
|
|
2327
2490
|
}
|
|
2328
2491
|
else {
|
|
2329
2492
|
// Field is not in the selection, use table.column notation
|
|
2330
|
-
|
|
2493
|
+
// Look up the database column name from the schema
|
|
2494
|
+
const dbColumnName = colNameMap.get(field) ?? field;
|
|
2495
|
+
return `"${this.schema.name}"."${dbColumnName}" ${direction}`;
|
|
2331
2496
|
}
|
|
2332
2497
|
});
|
|
2333
2498
|
orderByClause = `ORDER BY ${orderParts.join(', ')}`;
|
|
@@ -2422,6 +2587,7 @@ class SelectQueryBuilder {
|
|
|
2422
2587
|
return {
|
|
2423
2588
|
sql: finalQuery,
|
|
2424
2589
|
params: context.allParams,
|
|
2590
|
+
nestedPaths,
|
|
2425
2591
|
};
|
|
2426
2592
|
}
|
|
2427
2593
|
/**
|
|
@@ -3851,17 +4017,34 @@ class CollectionQueryBuilder {
|
|
|
3851
4017
|
localParams.push(...params);
|
|
3852
4018
|
context.allParams.push(...params);
|
|
3853
4019
|
}
|
|
3854
|
-
// Step 3: Build ORDER BY
|
|
4020
|
+
// Step 3: Build ORDER BY clauses SQL (without ORDER BY keyword)
|
|
4021
|
+
// We need two versions:
|
|
4022
|
+
// - orderByClause: uses database column names (for subquery ORDER BY on raw table)
|
|
4023
|
+
// - orderByClauseAlias: uses property names/aliases (for json_agg ORDER BY on aliased subquery output)
|
|
4024
|
+
// Note: orderByFields[].field already contains the database column name (from parseOrderBy using __dbColumnName)
|
|
3855
4025
|
let orderByClause;
|
|
4026
|
+
let orderByClauseAlias;
|
|
3856
4027
|
if (this.orderByFields.length > 0) {
|
|
3857
|
-
//
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
const
|
|
3862
|
-
|
|
4028
|
+
// Build reverse lookup: db column name -> property name
|
|
4029
|
+
let dbToPropertyMap = null;
|
|
4030
|
+
if (this.targetTableSchema) {
|
|
4031
|
+
dbToPropertyMap = new Map();
|
|
4032
|
+
for (const [propName, colBuilder] of Object.entries(this.targetTableSchema.columns)) {
|
|
4033
|
+
const config = colBuilder.build();
|
|
4034
|
+
dbToPropertyMap.set(config.name, propName);
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
const orderPartsDb = this.orderByFields.map(({ field, direction }) => {
|
|
4038
|
+
// field is already the database column name
|
|
4039
|
+
return `"${field}" ${direction}`;
|
|
4040
|
+
});
|
|
4041
|
+
const orderPartsAlias = this.orderByFields.map(({ field, direction }) => {
|
|
4042
|
+
// Look up the property name from the db column name
|
|
4043
|
+
const propertyName = dbToPropertyMap?.get(field) ?? field;
|
|
4044
|
+
return `"${propertyName}" ${direction}`;
|
|
3863
4045
|
});
|
|
3864
|
-
orderByClause =
|
|
4046
|
+
orderByClause = orderPartsDb.join(', ');
|
|
4047
|
+
orderByClauseAlias = orderPartsAlias.join(', ');
|
|
3865
4048
|
}
|
|
3866
4049
|
// Step 4: Determine aggregation type and field
|
|
3867
4050
|
let aggregationType;
|
|
@@ -3922,6 +4105,7 @@ class CollectionQueryBuilder {
|
|
|
3922
4105
|
whereClause,
|
|
3923
4106
|
whereParams, // Pass WHERE clause parameters
|
|
3924
4107
|
orderByClause,
|
|
4108
|
+
orderByClauseAlias, // For json_agg ORDER BY which uses aliases
|
|
3925
4109
|
limitValue: this.limitValue,
|
|
3926
4110
|
offsetValue: this.offsetValue,
|
|
3927
4111
|
isDistinct: this.isDistinct,
|