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.
@@ -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
- // Transform results
1101
+ // Reconstruct nested objects from flat row data (if any)
1102
1102
  tracer.startPhase('resultProcessing');
1103
- const transformed = tracer.trace('transformResults', () => this.transformResults(result.rows, selectionResult), { rowCount: result.rows.length });
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 = orderByFields.length > 0
1233
- ? ` ORDER BY ${orderByFields.map(({ field, direction }) => `"${field}" ${direction}`).join(', ')}`
1234
- : ` ORDER BY "id" DESC`;
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
- return `"${this.schema.name}"."${field}" ${direction}`;
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 clause SQL (without ORDER BY keyword)
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
- // Performance: Pre-compute column name map for ORDER BY lookups
3858
- const colNameMap = this.targetTableSchema ? getColumnNameMapForSchema(this.targetTableSchema) : null;
3859
- const orderParts = this.orderByFields.map(({ field, direction }) => {
3860
- // Look up the database column name from the cached map if available
3861
- const dbColumnName = colNameMap?.get(field) ?? field;
3862
- return `"${dbColumnName}" ${direction}`;
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 = orderParts.join(', ');
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,