linkgress-orm 0.1.0 → 0.1.2

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.
Files changed (41) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +196 -196
  3. package/dist/entity/db-context.d.ts.map +1 -1
  4. package/dist/entity/db-context.js +7 -5
  5. package/dist/entity/db-context.js.map +1 -1
  6. package/dist/migration/db-schema-manager.d.ts +5 -0
  7. package/dist/migration/db-schema-manager.d.ts.map +1 -1
  8. package/dist/migration/db-schema-manager.js +147 -79
  9. package/dist/migration/db-schema-manager.js.map +1 -1
  10. package/dist/migration/enum-migrator.js +6 -6
  11. package/dist/query/collection-strategy.interface.d.ts +38 -0
  12. package/dist/query/collection-strategy.interface.d.ts.map +1 -1
  13. package/dist/query/cte-builder.d.ts +18 -1
  14. package/dist/query/cte-builder.d.ts.map +1 -1
  15. package/dist/query/cte-builder.js +102 -11
  16. package/dist/query/cte-builder.js.map +1 -1
  17. package/dist/query/grouped-query.d.ts +24 -1
  18. package/dist/query/grouped-query.d.ts.map +1 -1
  19. package/dist/query/grouped-query.js +260 -71
  20. package/dist/query/grouped-query.js.map +1 -1
  21. package/dist/query/join-builder.d.ts.map +1 -1
  22. package/dist/query/join-builder.js +10 -14
  23. package/dist/query/join-builder.js.map +1 -1
  24. package/dist/query/query-builder.d.ts +65 -1
  25. package/dist/query/query-builder.d.ts.map +1 -1
  26. package/dist/query/query-builder.js +566 -167
  27. package/dist/query/query-builder.js.map +1 -1
  28. package/dist/query/strategies/cte-collection-strategy.d.ts +4 -0
  29. package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
  30. package/dist/query/strategies/cte-collection-strategy.js +169 -78
  31. package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
  32. package/dist/query/strategies/lateral-collection-strategy.d.ts +4 -0
  33. package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
  34. package/dist/query/strategies/lateral-collection-strategy.js +86 -28
  35. package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
  36. package/dist/query/strategies/temptable-collection-strategy.js +97 -97
  37. package/dist/schema/table-builder.d.ts +16 -0
  38. package/dist/schema/table-builder.d.ts.map +1 -1
  39. package/dist/schema/table-builder.js +23 -1
  40. package/dist/schema/table-builder.js.map +1 -1
  41. package/package.json +1 -1
@@ -1,12 +1,117 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CollectionQueryBuilder = exports.ReferenceQueryBuilder = exports.SelectQueryBuilder = exports.QueryBuilder = void 0;
4
+ exports.getColumnNameMapForSchema = getColumnNameMapForSchema;
5
+ exports.getRelationEntriesForSchema = getRelationEntriesForSchema;
6
+ exports.getTargetSchemaForRelation = getTargetSchemaForRelation;
7
+ exports.createNestedFieldRefProxy = createNestedFieldRefProxy;
4
8
  const conditions_1 = require("./conditions");
5
9
  const query_utils_1 = require("./query-utils");
6
10
  const subquery_1 = require("./subquery");
7
11
  const grouped_query_1 = require("./grouped-query");
8
12
  const cte_builder_1 = require("./cte-builder");
9
13
  const collection_strategy_factory_1 = require("./collection-strategy.factory");
14
+ /**
15
+ * Performance utility: Get column name map from schema, using cached version if available
16
+ */
17
+ function getColumnNameMapForSchema(schema) {
18
+ if (schema.columnNameMap) {
19
+ return schema.columnNameMap;
20
+ }
21
+ // Fallback: build the map (for schemas that weren't built with the new TableBuilder)
22
+ const map = new Map();
23
+ for (const [colName, colBuilder] of Object.entries(schema.columns)) {
24
+ map.set(colName, colBuilder.build().name);
25
+ }
26
+ return map;
27
+ }
28
+ /**
29
+ * Performance utility: Get relation entries array from schema, using cached version if available
30
+ */
31
+ function getRelationEntriesForSchema(schema) {
32
+ if (schema.relationEntries) {
33
+ return schema.relationEntries;
34
+ }
35
+ // Fallback: build the array (for schemas that weren't built with the new TableBuilder)
36
+ return Object.entries(schema.relations);
37
+ }
38
+ /**
39
+ * Performance utility: Get target schema for a relation, using cached version if available
40
+ */
41
+ function getTargetSchemaForRelation(schema, relName, relConfig) {
42
+ // Try cached version first
43
+ if (schema.relationSchemaCache) {
44
+ const cached = schema.relationSchemaCache.get(relName);
45
+ if (cached)
46
+ return cached;
47
+ }
48
+ // Fallback: build the schema
49
+ if (relConfig.targetTableBuilder) {
50
+ return relConfig.targetTableBuilder.build();
51
+ }
52
+ return undefined;
53
+ }
54
+ // Performance: Cache nested field ref proxies per table alias
55
+ const nestedFieldRefProxyCache = new Map();
56
+ /**
57
+ * Creates a nested proxy that supports accessing properties at any depth.
58
+ * This allows patterns like `p.product.priceMode` to work even without full schema information.
59
+ * Each property access returns an object that is both a FieldRef and can be further accessed.
60
+ *
61
+ * @param tableAlias The table alias to use for the FieldRef
62
+ * @returns A proxy that creates FieldRefs for any property access
63
+ */
64
+ function createNestedFieldRefProxy(tableAlias) {
65
+ // Return cached proxy if available
66
+ const cached = nestedFieldRefProxyCache.get(tableAlias);
67
+ if (cached)
68
+ return cached;
69
+ const handler = {
70
+ get: (_target, prop) => {
71
+ // Handle Symbol.toPrimitive for string conversion (used in template literals)
72
+ if (prop === Symbol.toPrimitive || prop === 'toString' || prop === 'valueOf') {
73
+ return () => `[NestedFieldRefProxy:${tableAlias}]`;
74
+ }
75
+ if (typeof prop === 'symbol')
76
+ return undefined;
77
+ // Return an object that is both a FieldRef AND a proxy for further nesting
78
+ const fieldRef = {
79
+ __fieldName: prop,
80
+ __dbColumnName: prop,
81
+ __tableAlias: tableAlias,
82
+ };
83
+ // Return a proxy that acts as both the FieldRef and allows further property access
84
+ return new Proxy(fieldRef, {
85
+ get: (fieldTarget, nestedProp) => {
86
+ // Handle Symbol.toPrimitive for string conversion (used in template literals)
87
+ if (nestedProp === Symbol.toPrimitive || nestedProp === 'toString' || nestedProp === 'valueOf') {
88
+ return () => fieldTarget.__dbColumnName;
89
+ }
90
+ if (typeof nestedProp === 'symbol')
91
+ return undefined;
92
+ // If accessing FieldRef properties, return them
93
+ if (nestedProp === '__fieldName' || nestedProp === '__dbColumnName' || nestedProp === '__tableAlias') {
94
+ return fieldTarget[nestedProp];
95
+ }
96
+ // Otherwise, treat as nested navigation and create a new nested proxy
97
+ // The nested table alias is the property name (e.g., 'product' for p.product)
98
+ return createNestedFieldRefProxy(prop)[nestedProp];
99
+ },
100
+ has: (_fieldTarget, _nestedProp) => true,
101
+ });
102
+ },
103
+ has: (_target, prop) => {
104
+ // The outer proxy doesn't have FieldRef properties - only field names
105
+ if (prop === '__fieldName' || prop === '__dbColumnName' || prop === '__tableAlias') {
106
+ return false;
107
+ }
108
+ return true;
109
+ },
110
+ };
111
+ const proxy = new Proxy({}, handler);
112
+ nestedFieldRefProxyCache.set(tableAlias, proxy);
113
+ return proxy;
114
+ }
10
115
  /**
11
116
  * Cached regex for numeric string detection
12
117
  * Used to convert PostgreSQL NUMERIC/BIGINT strings to numbers
@@ -16,7 +121,7 @@ const NUMERIC_REGEX = /^-?\d+(\.\d+)?$/;
16
121
  * Query builder for a table
17
122
  */
18
123
  class QueryBuilder {
19
- constructor(schema, client, whereCond, limit, offset, orderBy, executor, manualJoins, joinCounter, collectionStrategy) {
124
+ constructor(schema, client, whereCond, limit, offset, orderBy, executor, manualJoins, joinCounter, collectionStrategy, schemaRegistry) {
20
125
  this.orderByFields = [];
21
126
  this.manualJoins = [];
22
127
  this.joinCounter = 0;
@@ -30,6 +135,7 @@ class QueryBuilder {
30
135
  this.manualJoins = manualJoins || [];
31
136
  this.joinCounter = joinCounter || 0;
32
137
  this.collectionStrategy = collectionStrategy;
138
+ this.schemaRegistry = schemaRegistry;
33
139
  }
34
140
  /**
35
141
  * Get qualified table name with schema prefix if specified
@@ -43,7 +149,7 @@ class QueryBuilder {
43
149
  */
44
150
  select(selector) {
45
151
  return new SelectQueryBuilder(this.schema, this.client, selector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false, // isDistinct defaults to false
46
- undefined, // schemaRegistry
152
+ this.schemaRegistry, // Pass schema registry for nested navigation resolution
47
153
  [], // ctes - start with empty array
48
154
  this.collectionStrategy);
49
155
  }
@@ -66,7 +172,8 @@ class QueryBuilder {
66
172
  * Add CTEs (Common Table Expressions) to the query
67
173
  */
68
174
  with(...ctes) {
69
- return new SelectQueryBuilder(this.schema, this.client, (row) => row, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false, undefined, ctes, this.collectionStrategy);
175
+ return new SelectQueryBuilder(this.schema, this.client, (row) => row, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, false, this.schemaRegistry, // Pass schema registry for nested navigation resolution
176
+ ctes, this.collectionStrategy);
70
177
  }
71
178
  /**
72
179
  * Create mock row for analysis
@@ -77,14 +184,10 @@ class QueryBuilder {
77
184
  return this._cachedMockRow;
78
185
  }
79
186
  const mock = {};
80
- // Performance: Build column configs once and cache them
81
- const columnEntries = Object.entries(this.schema.columns);
82
- const columnConfigs = new Map();
83
- for (const [colName, colBuilder] of columnEntries) {
84
- columnConfigs.set(colName, colBuilder.build().name);
85
- }
187
+ // Performance: Use pre-computed column name map if available
188
+ const columnNameMap = getColumnNameMapForSchema(this.schema);
86
189
  // Add columns as FieldRef objects - type-safe with property name and database column name
87
- for (const [colName, dbColumnName] of columnConfigs) {
190
+ for (const [colName, dbColumnName] of columnNameMap) {
88
191
  Object.defineProperty(mock, colName, {
89
192
  get: () => ({
90
193
  __fieldName: colName,
@@ -95,20 +198,20 @@ class QueryBuilder {
95
198
  configurable: true,
96
199
  });
97
200
  }
98
- // Performance: Cache target schemas for relations to avoid repeated .build() calls
99
- const relationSchemas = new Map();
100
- for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
101
- if (relConfig.targetTableBuilder) {
102
- relationSchemas.set(relName, relConfig.targetTableBuilder.build());
103
- }
104
- }
201
+ // Performance: Use pre-computed relation entries and cached schemas
202
+ const relationEntries = getRelationEntriesForSchema(this.schema);
105
203
  // Add relations (both collections and single references)
106
- for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
204
+ for (const [relName, relConfig] of relationEntries) {
205
+ // Performance: Use cached target schema, but prefer registry lookup for full relations
206
+ let targetSchema = this.schemaRegistry?.get(relConfig.targetTable);
207
+ if (!targetSchema) {
208
+ targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
209
+ }
107
210
  if (relConfig.type === 'many') {
108
- const targetSchema = relationSchemas.get(relName);
109
211
  Object.defineProperty(mock, relName, {
110
212
  get: () => {
111
- return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema);
213
+ return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
214
+ );
112
215
  },
113
216
  enumerable: true,
114
217
  configurable: true,
@@ -116,10 +219,10 @@ class QueryBuilder {
116
219
  }
117
220
  else {
118
221
  // Single reference navigation (many-to-one, one-to-one)
119
- const targetSchema = relationSchemas.get(relName);
120
222
  Object.defineProperty(mock, relName, {
121
223
  get: () => {
122
- const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
224
+ const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
225
+ );
123
226
  return refBuilder.createMockTargetRow();
124
227
  },
125
228
  enumerable: true,
@@ -234,14 +337,10 @@ class QueryBuilder {
234
337
  */
235
338
  createMockRowForTable(schema, alias) {
236
339
  const mock = {};
237
- // Performance: Build column configs once and cache them
238
- const columnEntries = Object.entries(schema.columns);
239
- const columnConfigs = new Map();
240
- for (const [colName, colBuilder] of columnEntries) {
241
- columnConfigs.set(colName, colBuilder.build().name);
242
- }
340
+ // Performance: Use pre-computed column name map if available
341
+ const columnNameMap = getColumnNameMapForSchema(schema);
243
342
  // Add columns as FieldRef objects with table alias
244
- for (const [colName, dbColumnName] of columnConfigs) {
343
+ for (const [colName, dbColumnName] of columnNameMap) {
245
344
  Object.defineProperty(mock, colName, {
246
345
  get: () => ({
247
346
  __fieldName: colName,
@@ -252,18 +351,14 @@ class QueryBuilder {
252
351
  configurable: true,
253
352
  });
254
353
  }
255
- // Performance: Cache target schemas for relations
256
- const relationSchemas = new Map();
257
- for (const [relName, relConfig] of Object.entries(schema.relations)) {
258
- if (relConfig.targetTableBuilder) {
259
- relationSchemas.set(relName, relConfig.targetTableBuilder.build());
260
- }
261
- }
354
+ // Performance: Use pre-computed relation entries and cached schemas
355
+ const relationEntries = getRelationEntriesForSchema(schema);
262
356
  // Add navigation properties (single references and collections)
263
- for (const [relName, relConfig] of Object.entries(schema.relations)) {
357
+ for (const [relName, relConfig] of relationEntries) {
358
+ // Performance: Use cached target schema
359
+ const targetSchema = getTargetSchemaForRelation(schema, relName, relConfig);
264
360
  if (relConfig.type === 'many') {
265
361
  // Collection navigation
266
- const targetSchema = relationSchemas.get(relName);
267
362
  Object.defineProperty(mock, relName, {
268
363
  get: () => {
269
364
  return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
@@ -274,7 +369,6 @@ class QueryBuilder {
274
369
  }
275
370
  else {
276
371
  // Single reference navigation
277
- const targetSchema = relationSchemas.get(relName);
278
372
  Object.defineProperty(mock, relName, {
279
373
  get: () => {
280
374
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
@@ -383,7 +477,12 @@ class SelectQueryBuilder {
383
477
  * .toList();
384
478
  */
385
479
  with(...ctes) {
386
- this.ctes.push(...ctes);
480
+ // Add CTEs, avoiding duplicates by name
481
+ for (const cte of ctes) {
482
+ if (!this.ctes.some(existing => existing.name === cte.name)) {
483
+ this.ctes.push(cte);
484
+ }
485
+ }
387
486
  return this;
388
487
  }
389
488
  /**
@@ -610,14 +709,10 @@ class SelectQueryBuilder {
610
709
  */
611
710
  createMockRowForTable(schema, alias) {
612
711
  const mock = {};
613
- // Performance: Build column configs once and cache them
614
- const columnEntries = Object.entries(schema.columns);
615
- const columnConfigs = new Map();
616
- for (const [colName, colBuilder] of columnEntries) {
617
- columnConfigs.set(colName, colBuilder.build().name);
618
- }
712
+ // Performance: Use pre-computed column name map if available
713
+ const columnNameMap = getColumnNameMapForSchema(schema);
619
714
  // Add columns as FieldRef objects with table alias
620
- for (const [colName, dbColumnName] of columnConfigs) {
715
+ for (const [colName, dbColumnName] of columnNameMap) {
621
716
  Object.defineProperty(mock, colName, {
622
717
  get: () => ({
623
718
  __fieldName: colName,
@@ -628,18 +723,14 @@ class SelectQueryBuilder {
628
723
  configurable: true,
629
724
  });
630
725
  }
631
- // Performance: Cache target schemas for relations
632
- const relationSchemas = new Map();
633
- for (const [relName, relConfig] of Object.entries(schema.relations)) {
634
- if (relConfig.targetTableBuilder) {
635
- relationSchemas.set(relName, relConfig.targetTableBuilder.build());
636
- }
637
- }
726
+ // Performance: Use pre-computed relation entries and cached schemas
727
+ const relationEntries = getRelationEntriesForSchema(schema);
638
728
  // Add navigation properties (single references and collections)
639
- for (const [relName, relConfig] of Object.entries(schema.relations)) {
729
+ for (const [relName, relConfig] of relationEntries) {
730
+ // Performance: Use cached target schema
731
+ const targetSchema = getTargetSchemaForRelation(schema, relName, relConfig);
640
732
  if (relConfig.type === 'many') {
641
733
  // Collection navigation
642
- const targetSchema = relationSchemas.get(relName);
643
734
  Object.defineProperty(mock, relName, {
644
735
  get: () => {
645
736
  return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
@@ -650,7 +741,6 @@ class SelectQueryBuilder {
650
741
  }
651
742
  else {
652
743
  // Single reference navigation
653
- const targetSchema = relationSchemas.get(relName);
654
744
  Object.defineProperty(mock, relName, {
655
745
  get: () => {
656
746
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
@@ -723,7 +813,7 @@ class SelectQueryBuilder {
723
813
  get(target, prop) {
724
814
  if (typeof prop === 'symbol')
725
815
  return undefined;
726
- // If we have selection metadata, check if this property has a mapper
816
+ // If we have selection metadata, check if this property has a mapper or is an aggregation array
727
817
  if (cte.selectionMetadata && prop in cte.selectionMetadata) {
728
818
  const value = cte.selectionMetadata[prop];
729
819
  // If it's a SqlFragment with a mapper, preserve it
@@ -736,6 +826,16 @@ class SelectQueryBuilder {
736
826
  getMapper: () => value.getMapper(),
737
827
  };
738
828
  }
829
+ // If it's a CTE aggregation array marker, preserve it with inner metadata
830
+ if (typeof value === 'object' && value !== null && '__isAggregationArray' in value && value.__isAggregationArray) {
831
+ return {
832
+ __fieldName: prop,
833
+ __dbColumnName: prop,
834
+ __tableAlias: cte.name,
835
+ __isAggregationArray: true,
836
+ __innerSelectionMetadata: value.__innerSelectionMetadata,
837
+ };
838
+ }
739
839
  }
740
840
  // Return a regular FieldRef for any property accessed
741
841
  return {
@@ -1238,9 +1338,10 @@ class SelectQueryBuilder {
1238
1338
  */
1239
1339
  createMockRow() {
1240
1340
  const mock = {};
1341
+ // Performance: Use pre-computed column name map if available
1342
+ const columnNameMap = getColumnNameMapForSchema(this.schema);
1241
1343
  // Add columns as FieldRef objects - type-safe with property name and database column name
1242
- for (const [colName, colBuilder] of Object.entries(this.schema.columns)) {
1243
- const dbColumnName = colBuilder.build().name;
1344
+ for (const [colName, dbColumnName] of columnNameMap) {
1244
1345
  Object.defineProperty(mock, colName, {
1245
1346
  get: () => ({
1246
1347
  __fieldName: colName,
@@ -1257,13 +1358,12 @@ class SelectQueryBuilder {
1257
1358
  if (join.isSubquery || !join.schema) {
1258
1359
  continue;
1259
1360
  }
1260
- for (const [colName, colBuilder] of Object.entries(join.schema.columns)) {
1261
- const dbColumnName = colBuilder.build().name;
1262
- // Create a unique property name by prefixing with table alias or using the column name directly
1263
- // For now, we'll create nested objects for each joined table
1264
- if (!mock[join.alias]) {
1265
- mock[join.alias] = {};
1266
- }
1361
+ // Performance: Use pre-computed column name map for joined schema
1362
+ const joinColumnNameMap = getColumnNameMapForSchema(join.schema);
1363
+ if (!mock[join.alias]) {
1364
+ mock[join.alias] = {};
1365
+ }
1366
+ for (const [colName, dbColumnName] of joinColumnNameMap) {
1267
1367
  Object.defineProperty(mock[join.alias], colName, {
1268
1368
  get: () => ({
1269
1369
  __fieldName: colName,
@@ -1275,15 +1375,18 @@ class SelectQueryBuilder {
1275
1375
  });
1276
1376
  }
1277
1377
  }
1378
+ // Performance: Use pre-computed relation entries
1379
+ const relationEntries = getRelationEntriesForSchema(this.schema);
1278
1380
  // Add relations as CollectionQueryBuilder or ReferenceQueryBuilder
1279
- for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
1280
- // Try to get target schema from registry (preferred, has full relations) or targetTableBuilder
1381
+ for (const [relName, relConfig] of relationEntries) {
1382
+ // Try to get target schema from registry (preferred, has full relations) or cached schema
1281
1383
  let targetSchema;
1282
1384
  if (this.schemaRegistry) {
1283
1385
  targetSchema = this.schemaRegistry.get(relConfig.targetTable);
1284
1386
  }
1285
- if (!targetSchema && relConfig.targetTableBuilder) {
1286
- targetSchema = relConfig.targetTableBuilder.build();
1387
+ if (!targetSchema) {
1388
+ // Performance: Use cached target schema
1389
+ targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
1287
1390
  }
1288
1391
  if (relConfig.type === 'many') {
1289
1392
  Object.defineProperty(mock, relName, {
@@ -1376,40 +1479,142 @@ class SelectQueryBuilder {
1376
1479
  }
1377
1480
  /**
1378
1481
  * Detect navigation property references in selection and add necessary JOINs
1482
+ * Supports multi-level navigation like task.level.createdBy
1379
1483
  */
1380
1484
  detectAndAddJoinsFromSelection(selection, joins) {
1381
1485
  if (!selection || typeof selection !== 'object') {
1382
1486
  return;
1383
1487
  }
1384
- for (const [key, value] of Object.entries(selection)) {
1488
+ // First pass: collect all table aliases
1489
+ const allTableAliases = new Set();
1490
+ this.collectTableAliasesFromSelection(selection, allTableAliases);
1491
+ // Second pass: resolve all joins through the schema graph
1492
+ this.resolveJoinsForTableAliases(allTableAliases, joins);
1493
+ }
1494
+ /**
1495
+ * Collect all table aliases from a selection
1496
+ */
1497
+ collectTableAliasesFromSelection(selection, allTableAliases) {
1498
+ if (!selection || typeof selection !== 'object') {
1499
+ return;
1500
+ }
1501
+ for (const [_key, value] of Object.entries(selection)) {
1385
1502
  if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
1386
- // This is a FieldRef with a table alias - check if it's from a related table
1503
+ // This is a FieldRef with a table alias
1387
1504
  const tableAlias = value.__tableAlias;
1388
- if (tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
1389
- // This references a related table - find the relation and add a JOIN
1390
- const relation = this.schema.relations[tableAlias];
1391
- if (relation && relation.type === 'one') {
1392
- // Get target schema from targetTableBuilder if available
1393
- let targetSchema;
1394
- if (relation.targetTableBuilder) {
1395
- const targetTableSchema = relation.targetTableBuilder.build();
1396
- targetSchema = targetTableSchema.schema;
1505
+ if (tableAlias && tableAlias !== this.schema.name) {
1506
+ allTableAliases.add(tableAlias);
1507
+ }
1508
+ }
1509
+ else if (value instanceof conditions_1.SqlFragment) {
1510
+ // SqlFragment may contain navigation property references
1511
+ const fieldRefs = value.getFieldRefs();
1512
+ for (const fieldRef of fieldRefs) {
1513
+ if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
1514
+ const tableAlias = fieldRef.__tableAlias;
1515
+ if (tableAlias && tableAlias !== this.schema.name) {
1516
+ allTableAliases.add(tableAlias);
1397
1517
  }
1398
- // Add a JOIN for this reference
1399
- joins.push({
1400
- alias: tableAlias,
1401
- targetTable: relation.targetTable,
1402
- targetSchema,
1403
- foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
1404
- matches: relation.matches || [],
1405
- isMandatory: relation.isMandatory ?? false,
1406
- });
1407
1518
  }
1408
1519
  }
1409
1520
  }
1410
- else if (value && typeof value === 'object' && !Array.isArray(value)) {
1521
+ else if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof CollectionQueryBuilder)) {
1411
1522
  // Recursively check nested objects
1412
- this.detectAndAddJoinsFromSelection(value, joins);
1523
+ this.collectTableAliasesFromSelection(value, allTableAliases);
1524
+ }
1525
+ }
1526
+ }
1527
+ /**
1528
+ * Resolve all navigation joins by finding the correct path through the schema graph
1529
+ * This handles multi-level navigation like task.level.createdBy
1530
+ */
1531
+ resolveJoinsForTableAliases(allTableAliases, joins) {
1532
+ if (allTableAliases.size === 0) {
1533
+ return;
1534
+ }
1535
+ // Keep resolving until we've resolved all aliases or can't make progress
1536
+ const resolved = new Set();
1537
+ let maxIterations = allTableAliases.size * 3; // Prevent infinite loops
1538
+ while (resolved.size < allTableAliases.size && maxIterations-- > 0) {
1539
+ // Build a map of already joined schemas for path resolution
1540
+ const joinedSchemas = new Map();
1541
+ joinedSchemas.set(this.schema.name, this.schema);
1542
+ for (const join of joins) {
1543
+ let schema;
1544
+ if (this.schemaRegistry) {
1545
+ schema = this.schemaRegistry.get(join.targetTable);
1546
+ }
1547
+ if (schema) {
1548
+ joinedSchemas.set(join.alias, schema);
1549
+ }
1550
+ }
1551
+ // Try to resolve each unresolved alias
1552
+ for (const alias of allTableAliases) {
1553
+ if (resolved.has(alias) || joins.some(j => j.alias === alias)) {
1554
+ resolved.add(alias);
1555
+ continue;
1556
+ }
1557
+ // Look for this alias in any of the already joined schemas
1558
+ for (const [sourceAlias, schema] of joinedSchemas) {
1559
+ if (schema.relations && schema.relations[alias]) {
1560
+ const relation = schema.relations[alias];
1561
+ if (relation.type === 'one') {
1562
+ // Get target schema
1563
+ let targetSchema;
1564
+ let targetSchemaName;
1565
+ if (this.schemaRegistry) {
1566
+ targetSchema = this.schemaRegistry.get(relation.targetTable);
1567
+ targetSchemaName = targetSchema?.schema;
1568
+ }
1569
+ if (!targetSchema && relation.targetTableBuilder) {
1570
+ targetSchema = relation.targetTableBuilder.build();
1571
+ targetSchemaName = targetSchema?.schema;
1572
+ }
1573
+ joins.push({
1574
+ alias,
1575
+ targetTable: relation.targetTable,
1576
+ targetSchema: targetSchemaName,
1577
+ foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
1578
+ matches: relation.matches || ['id'],
1579
+ isMandatory: relation.isMandatory ?? false,
1580
+ sourceAlias, // Track where this join comes from
1581
+ });
1582
+ resolved.add(alias);
1583
+ break;
1584
+ }
1585
+ }
1586
+ }
1587
+ }
1588
+ }
1589
+ }
1590
+ /**
1591
+ * Add a JOIN for a FieldRef if it references a related table
1592
+ * @deprecated Use detectAndAddJoinsFromSelection with multi-level resolution instead
1593
+ */
1594
+ addJoinForFieldRef(fieldRef, joins) {
1595
+ if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef) || !('__dbColumnName' in fieldRef)) {
1596
+ return;
1597
+ }
1598
+ const tableAlias = fieldRef.__tableAlias;
1599
+ if (tableAlias && tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
1600
+ // This references a related table - find the relation and add a JOIN
1601
+ const relation = this.schema.relations[tableAlias];
1602
+ if (relation && relation.type === 'one') {
1603
+ // Get target schema from targetTableBuilder if available
1604
+ let targetSchema;
1605
+ if (relation.targetTableBuilder) {
1606
+ const targetTableSchema = relation.targetTableBuilder.build();
1607
+ targetSchema = targetTableSchema.schema;
1608
+ }
1609
+ // Add a JOIN for this reference
1610
+ joins.push({
1611
+ alias: tableAlias,
1612
+ targetTable: relation.targetTable,
1613
+ targetSchema,
1614
+ foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
1615
+ matches: relation.matches || [],
1616
+ isMandatory: relation.isMandatory ?? false,
1617
+ });
1413
1618
  }
1414
1619
  }
1415
1620
  }
@@ -1420,35 +1625,19 @@ class SelectQueryBuilder {
1420
1625
  if (!condition) {
1421
1626
  return;
1422
1627
  }
1423
- // Get all field references from the condition
1628
+ // Collect all table aliases from the condition
1629
+ const allTableAliases = new Set();
1424
1630
  const fieldRefs = condition.getFieldRefs();
1425
1631
  for (const fieldRef of fieldRefs) {
1426
1632
  if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
1427
1633
  const tableAlias = fieldRef.__tableAlias;
1428
- // Check if this references a related table that isn't already joined
1429
- if (tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
1430
- // Find the relation config for this navigation
1431
- const relation = this.schema.relations[tableAlias];
1432
- if (relation && relation.type === 'one') {
1433
- // Get target schema from targetTableBuilder if available
1434
- let targetSchema;
1435
- if (relation.targetTableBuilder) {
1436
- const targetTableSchema = relation.targetTableBuilder.build();
1437
- targetSchema = targetTableSchema.schema;
1438
- }
1439
- // Add a JOIN for this reference
1440
- joins.push({
1441
- alias: tableAlias,
1442
- targetTable: relation.targetTable,
1443
- targetSchema,
1444
- foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
1445
- matches: relation.matches || [],
1446
- isMandatory: relation.isMandatory ?? false,
1447
- });
1448
- }
1634
+ if (tableAlias !== this.schema.name) {
1635
+ allTableAliases.add(tableAlias);
1449
1636
  }
1450
1637
  }
1451
1638
  }
1639
+ // Resolve all joins through the schema graph
1640
+ this.resolveJoinsForTableAliases(allTableAliases, joins);
1452
1641
  }
1453
1642
  /**
1454
1643
  * Build SQL query
@@ -1530,6 +1719,7 @@ class SelectQueryBuilder {
1530
1719
  if ('__tableAlias' in value && value.__tableAlias && typeof value.__tableAlias === 'string') {
1531
1720
  // This is a field from a joined table
1532
1721
  const tableAlias = value.__tableAlias;
1722
+ const columnName = value.__dbColumnName;
1533
1723
  // Find the relation config for this navigation
1534
1724
  const relConfig = this.schema.relations[tableAlias];
1535
1725
  if (relConfig) {
@@ -1551,7 +1741,15 @@ class SelectQueryBuilder {
1551
1741
  });
1552
1742
  }
1553
1743
  }
1554
- selectParts.push(`"${tableAlias}"."${value.__dbColumnName}" as "${key}"`);
1744
+ // Check if this is a CTE aggregation column that needs COALESCE
1745
+ const cteJoin = this.manualJoins.find(j => j.cte && j.cte.name === tableAlias);
1746
+ if (cteJoin && cteJoin.cte && cteJoin.cte.isAggregationColumn(columnName)) {
1747
+ // CTE aggregation column - wrap with COALESCE to return empty array instead of null
1748
+ selectParts.push(`COALESCE("${tableAlias}"."${columnName}", '[]'::jsonb) as "${key}"`);
1749
+ }
1750
+ else {
1751
+ selectParts.push(`"${tableAlias}"."${columnName}" as "${key}"`);
1752
+ }
1555
1753
  }
1556
1754
  else {
1557
1755
  // Regular field from the main table
@@ -1599,9 +1797,10 @@ class SelectQueryBuilder {
1599
1797
  // Select all columns from the target table and group them
1600
1798
  // We'll need to use JSON object building in SQL
1601
1799
  const fieldParts = [];
1602
- for (const [colKey, col] of Object.entries(targetSchema.columns)) {
1603
- const config = col.build();
1604
- fieldParts.push(`'${colKey}', "${alias}"."${config.name}"`);
1800
+ // Performance: Use cached column name map
1801
+ const targetColMap = getColumnNameMapForSchema(targetSchema);
1802
+ for (const [colKey, dbColName] of targetColMap) {
1803
+ fieldParts.push(`'${colKey}', "${alias}"."${dbColName}"`);
1605
1804
  }
1606
1805
  selectParts.push(`json_build_object(${fieldParts.join(', ')}) as "${key}"`);
1607
1806
  }
@@ -1631,11 +1830,8 @@ class SelectQueryBuilder {
1631
1830
  const relConfig = this.schema.relations[alias];
1632
1831
  if (relConfig && relConfig.type === 'one') {
1633
1832
  // This is a reference navigation - select all fields from the target table
1634
- // Find the target table schema
1635
- let targetSchema;
1636
- if (relConfig.targetTableBuilder) {
1637
- targetSchema = relConfig.targetTableBuilder.build();
1638
- }
1833
+ // Performance: Use cached target schema
1834
+ const targetSchema = getTargetSchemaForRelation(this.schema, alias, relConfig);
1639
1835
  if (targetSchema) {
1640
1836
  // Add JOIN if not already added
1641
1837
  if (!joins.find(j => j.alias === alias)) {
@@ -1654,9 +1850,10 @@ class SelectQueryBuilder {
1654
1850
  }
1655
1851
  // Select all columns from the target table and group them into a JSON object
1656
1852
  const fieldParts = [];
1657
- for (const [colKey, col] of Object.entries(targetSchema.columns)) {
1658
- const config = col.build();
1659
- fieldParts.push(`'${colKey}', "${alias}"."${config.name}"`);
1853
+ // Performance: Use cached column name map
1854
+ const targetColMap = getColumnNameMapForSchema(targetSchema);
1855
+ for (const [colKey, dbColName] of targetColMap) {
1856
+ fieldParts.push(`'${colKey}', "${alias}"."${dbColName}"`);
1660
1857
  }
1661
1858
  selectParts.push(`json_build_object(${fieldParts.join(', ')}) as "${key}"`);
1662
1859
  continue;
@@ -1811,11 +2008,14 @@ class SelectQueryBuilder {
1811
2008
  for (const join of joins) {
1812
2009
  const joinType = join.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
1813
2010
  // Build ON clause for the join
2011
+ // For multi-level navigation, use the sourceAlias (the intermediate table)
2012
+ // For direct navigation, use the main table name
2013
+ const sourceTable = join.sourceAlias || this.schema.name;
1814
2014
  const onConditions = [];
1815
2015
  for (let i = 0; i < join.foreignKeys.length; i++) {
1816
2016
  const fk = join.foreignKeys[i];
1817
2017
  const match = join.matches[i];
1818
- onConditions.push(`"${this.schema.name}"."${fk}" = "${join.alias}"."${match}"`);
2018
+ onConditions.push(`"${sourceTable}"."${fk}" = "${join.alias}"."${match}"`);
1819
2019
  }
1820
2020
  // Use schema-qualified table name if schema is specified
1821
2021
  const joinTableName = this.getQualifiedTableName(join.targetTable, join.targetSchema);
@@ -1947,6 +2147,17 @@ class SelectQueryBuilder {
1947
2147
  }
1948
2148
  }
1949
2149
  }
2150
+ else if (typeof value === 'object' && value !== null && '__isAggregationArray' in value && value.__isAggregationArray) {
2151
+ // CTE withAggregation array - apply mappers to items inside
2152
+ const collectionItems = row[key] || [];
2153
+ const innerMetadata = value.__innerSelectionMetadata;
2154
+ if (innerMetadata && !disableMappers) {
2155
+ result[key] = this.transformCteAggregationItems(collectionItems, innerMetadata);
2156
+ }
2157
+ else {
2158
+ result[key] = collectionItems;
2159
+ }
2160
+ }
1950
2161
  else if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
1951
2162
  // SqlFragment with custom mapper (check this BEFORE FieldRef to handle subquery/CTE fields with mappers)
1952
2163
  const rawValue = row[key];
@@ -2059,6 +2270,54 @@ class SelectQueryBuilder {
2059
2270
  return transformedItem;
2060
2271
  });
2061
2272
  }
2273
+ /**
2274
+ * Transform CTE aggregation items applying fromDriver mappers from selection metadata
2275
+ */
2276
+ transformCteAggregationItems(items, selectionMetadata) {
2277
+ if (!items || items.length === 0) {
2278
+ return [];
2279
+ }
2280
+ // Build mapper cache from selection metadata
2281
+ const mapperCache = {};
2282
+ for (const [key, value] of Object.entries(selectionMetadata)) {
2283
+ // Check if value has getMapper (SqlFragment or field with mapper)
2284
+ if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
2285
+ let mapper = value.getMapper();
2286
+ // If mapper is a CustomTypeBuilder, get the actual type
2287
+ if (mapper && typeof mapper.getType === 'function') {
2288
+ mapper = mapper.getType();
2289
+ }
2290
+ if (mapper && typeof mapper.fromDriver === 'function') {
2291
+ mapperCache[key] = mapper;
2292
+ }
2293
+ }
2294
+ // Check if it's a FieldRef with schema column mapper
2295
+ else if (typeof value === 'object' && value !== null && '__fieldName' in value) {
2296
+ const fieldName = value.__fieldName;
2297
+ const column = this.schema.columns[fieldName];
2298
+ if (column) {
2299
+ const config = column.build();
2300
+ if (config.mapper && typeof config.mapper.fromDriver === 'function') {
2301
+ mapperCache[key] = config.mapper;
2302
+ }
2303
+ }
2304
+ }
2305
+ }
2306
+ // Transform items
2307
+ return items.map(item => {
2308
+ const transformedItem = {};
2309
+ for (const [key, value] of Object.entries(item)) {
2310
+ const mapper = mapperCache[key];
2311
+ if (mapper && value !== null && value !== undefined) {
2312
+ transformedItem[key] = mapper.fromDriver(value);
2313
+ }
2314
+ else {
2315
+ transformedItem[key] = value;
2316
+ }
2317
+ }
2318
+ return transformedItem;
2319
+ });
2320
+ }
2062
2321
  /**
2063
2322
  * Build aggregation query (MIN, MAX, SUM)
2064
2323
  */
@@ -2301,9 +2560,9 @@ class ReferenceQueryBuilder {
2301
2560
  createMockTargetRow() {
2302
2561
  if (this.targetTableSchema) {
2303
2562
  const mock = {};
2304
- // Add columns
2305
- for (const [colName, colBuilder] of Object.entries(this.targetTableSchema.columns)) {
2306
- const dbColumnName = colBuilder.build().name;
2563
+ // Add columns - use pre-computed column name map if available
2564
+ const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
2565
+ for (const [colName, dbColumnName] of columnNameMap) {
2307
2566
  Object.defineProperty(mock, colName, {
2308
2567
  get: () => ({
2309
2568
  __fieldName: colName,
@@ -2356,15 +2615,8 @@ class ReferenceQueryBuilder {
2356
2615
  return mock;
2357
2616
  }
2358
2617
  else {
2359
- // Fallback: generic proxy
2360
- const handler = {
2361
- get: (target, prop) => ({
2362
- __fieldName: prop,
2363
- __dbColumnName: prop,
2364
- __tableAlias: this.relationName,
2365
- }),
2366
- };
2367
- return new Proxy({}, handler);
2618
+ // Fallback: use the shared nested proxy that supports deep property access
2619
+ return createNestedFieldRefProxy(this.relationName);
2368
2620
  }
2369
2621
  }
2370
2622
  }
@@ -2399,7 +2651,8 @@ class CollectionQueryBuilder {
2399
2651
  * Select specific fields from collection items
2400
2652
  */
2401
2653
  select(selector) {
2402
- const newBuilder = new CollectionQueryBuilder(this.relationName, this.targetTable, this.foreignKey, this.sourceTable, this.targetTableSchema);
2654
+ const newBuilder = new CollectionQueryBuilder(this.relationName, this.targetTable, this.foreignKey, this.sourceTable, this.targetTableSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
2655
+ );
2403
2656
  newBuilder.selector = selector;
2404
2657
  newBuilder.whereCond = this.whereCond;
2405
2658
  newBuilder.limitValue = this.limitValue;
@@ -2444,14 +2697,10 @@ class CollectionQueryBuilder {
2444
2697
  if (this.targetTableSchema) {
2445
2698
  // If we have schema information, create a properly typed mock
2446
2699
  const mock = {};
2447
- // Performance: Build column configs once and cache them
2448
- const columnEntries = Object.entries(this.targetTableSchema.columns);
2449
- const columnConfigs = new Map();
2450
- for (const [colName, colBuilder] of columnEntries) {
2451
- columnConfigs.set(colName, colBuilder.build().name);
2452
- }
2700
+ // Performance: Use pre-computed column name map if available
2701
+ const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
2453
2702
  // Add columns
2454
- for (const [colName, dbColumnName] of columnConfigs) {
2703
+ for (const [colName, dbColumnName] of columnNameMap) {
2455
2704
  Object.defineProperty(mock, colName, {
2456
2705
  get: () => ({
2457
2706
  __fieldName: colName,
@@ -2500,14 +2749,8 @@ class CollectionQueryBuilder {
2500
2749
  return mock;
2501
2750
  }
2502
2751
  else {
2503
- // Fallback: generic proxy (don't cache as it's dynamic)
2504
- const handler = {
2505
- get: (target, prop) => ({
2506
- __fieldName: prop,
2507
- __dbColumnName: prop,
2508
- }),
2509
- };
2510
- return new Proxy({}, handler);
2752
+ // Fallback: use the shared nested proxy that supports deep property access
2753
+ return createNestedFieldRefProxy(this.targetTable);
2511
2754
  }
2512
2755
  }
2513
2756
  /**
@@ -2642,6 +2885,151 @@ class CollectionQueryBuilder {
2642
2885
  getFlattenResultType() {
2643
2886
  return this.flattenResultType;
2644
2887
  }
2888
+ /**
2889
+ * Detect navigation property references in the selected fields and add necessary JOINs
2890
+ * This supports multi-level navigation like p.task.level.createdBy.username
2891
+ */
2892
+ detectNavigationJoins(selection, joins, currentSourceAlias, currentSchema) {
2893
+ if (!selection || typeof selection !== 'object') {
2894
+ return;
2895
+ }
2896
+ // Collect all table aliases referenced in the selection
2897
+ const allTableAliases = new Set();
2898
+ // Helper to collect from a single selection
2899
+ const collectFromSelection = (sel) => {
2900
+ if (!sel || typeof sel !== 'object') {
2901
+ return;
2902
+ }
2903
+ // Handle single FieldRef
2904
+ if ('__tableAlias' in sel && '__dbColumnName' in sel) {
2905
+ this.addNavigationJoinForFieldRef(sel, joins, currentSourceAlias, currentSchema, allTableAliases);
2906
+ return;
2907
+ }
2908
+ // Handle object with multiple fields
2909
+ for (const [_key, value] of Object.entries(sel)) {
2910
+ if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
2911
+ // This is a FieldRef with a table alias
2912
+ this.addNavigationJoinForFieldRef(value, joins, currentSourceAlias, currentSchema, allTableAliases);
2913
+ }
2914
+ else if (value instanceof conditions_1.SqlFragment) {
2915
+ // SqlFragment may contain navigation property references
2916
+ const fieldRefs = value.getFieldRefs();
2917
+ for (const fieldRef of fieldRefs) {
2918
+ this.addNavigationJoinForFieldRef(fieldRef, joins, currentSourceAlias, currentSchema, allTableAliases);
2919
+ }
2920
+ }
2921
+ else if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof CollectionQueryBuilder)) {
2922
+ // Recursively check nested objects
2923
+ collectFromSelection(value);
2924
+ }
2925
+ }
2926
+ };
2927
+ // First pass: collect all table aliases
2928
+ collectFromSelection(selection);
2929
+ // Second pass: resolve all navigation joins by finding the correct path through schemas
2930
+ if (allTableAliases.size > 0) {
2931
+ this.resolveNavigationJoins(allTableAliases, joins, currentSchema);
2932
+ }
2933
+ }
2934
+ /**
2935
+ * Add a navigation JOIN for a FieldRef if it references a related table
2936
+ * Handles multi-level navigation by recursively resolving the join chain
2937
+ */
2938
+ addNavigationJoinForFieldRef(fieldRef, joins, sourceAlias, sourceSchema, allTableAliases) {
2939
+ if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef)) {
2940
+ return;
2941
+ }
2942
+ const tableAlias = fieldRef.__tableAlias;
2943
+ // If this references the target table directly, no join needed
2944
+ if (!tableAlias || tableAlias === this.targetTable) {
2945
+ return;
2946
+ }
2947
+ // Collect this table alias for later resolution
2948
+ allTableAliases.add(tableAlias);
2949
+ // Check if we already have this join
2950
+ if (joins.some(j => j.alias === tableAlias)) {
2951
+ return;
2952
+ }
2953
+ // Find the relation in the current schema
2954
+ const relation = sourceSchema.relations?.[tableAlias];
2955
+ if (relation && relation.type === 'one') {
2956
+ this.addNavigationJoin(tableAlias, relation, joins, sourceAlias);
2957
+ }
2958
+ }
2959
+ /**
2960
+ * Add a navigation join and return the target schema
2961
+ */
2962
+ addNavigationJoin(alias, relation, joins, sourceAlias) {
2963
+ // Check if already added
2964
+ if (joins.some(j => j.alias === alias)) {
2965
+ return undefined;
2966
+ }
2967
+ // Get the target table schema
2968
+ let targetSchema;
2969
+ let targetSchemaName;
2970
+ if (this.schemaRegistry) {
2971
+ targetSchema = this.schemaRegistry.get(relation.targetTable);
2972
+ targetSchemaName = targetSchema?.schema;
2973
+ }
2974
+ if (!targetSchema && relation.targetTableBuilder) {
2975
+ targetSchema = relation.targetTableBuilder.build();
2976
+ targetSchemaName = targetSchema?.schema;
2977
+ }
2978
+ // Build the join info
2979
+ const foreignKeys = relation.foreignKeys || [relation.foreignKey || ''];
2980
+ const matches = relation.matches || ['id']; // Default to 'id' as the PK
2981
+ joins.push({
2982
+ alias,
2983
+ targetTable: relation.targetTable,
2984
+ targetSchema: targetSchemaName,
2985
+ foreignKeys,
2986
+ matches,
2987
+ isMandatory: relation.isMandatory ?? false,
2988
+ sourceAlias,
2989
+ });
2990
+ return targetSchema;
2991
+ }
2992
+ /**
2993
+ * Resolve all navigation joins by finding the correct path through the schema graph
2994
+ * This handles multi-level navigation like task.level.createdBy
2995
+ */
2996
+ resolveNavigationJoins(allTableAliases, joins, startSchema) {
2997
+ // Keep resolving until we've resolved all aliases or can't make progress
2998
+ let resolved = new Set();
2999
+ let maxIterations = allTableAliases.size * 2; // Prevent infinite loops
3000
+ while (resolved.size < allTableAliases.size && maxIterations-- > 0) {
3001
+ // Build a map of already joined schemas for path resolution
3002
+ const joinedSchemas = new Map();
3003
+ joinedSchemas.set(this.targetTable, startSchema);
3004
+ for (const join of joins) {
3005
+ let schema;
3006
+ if (this.schemaRegistry) {
3007
+ schema = this.schemaRegistry.get(join.targetTable);
3008
+ }
3009
+ if (schema) {
3010
+ joinedSchemas.set(join.alias, schema);
3011
+ }
3012
+ }
3013
+ // Try to resolve each unresolved alias
3014
+ for (const alias of allTableAliases) {
3015
+ if (resolved.has(alias) || joins.some(j => j.alias === alias)) {
3016
+ resolved.add(alias);
3017
+ continue;
3018
+ }
3019
+ // Look for this alias in any of the already joined schemas
3020
+ for (const [schemaAlias, schema] of joinedSchemas) {
3021
+ if (schema.relations && schema.relations[alias]) {
3022
+ const relation = schema.relations[alias];
3023
+ if (relation.type === 'one') {
3024
+ this.addNavigationJoin(alias, relation, joins, schemaAlias);
3025
+ resolved.add(alias);
3026
+ break;
3027
+ }
3028
+ }
3029
+ }
3030
+ }
3031
+ }
3032
+ }
2645
3033
  /**
2646
3034
  * Build CTE for this collection query
2647
3035
  * Now delegates to collection strategy pattern
@@ -2676,8 +3064,13 @@ class CollectionQueryBuilder {
2676
3064
  return { alias, expression: fragmentSql };
2677
3065
  }
2678
3066
  else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
2679
- // FieldRef object - use database column name
3067
+ // FieldRef object - use database column name with optional table alias
2680
3068
  const dbColumnName = field.__dbColumnName;
3069
+ const tableAlias = field.__tableAlias;
3070
+ // If tableAlias differs from the target table, it's a navigation property reference
3071
+ if (tableAlias && tableAlias !== this.targetTable) {
3072
+ return { alias, expression: `"${tableAlias}"."${dbColumnName}"` };
3073
+ }
2681
3074
  return { alias, expression: `"${dbColumnName}"` };
2682
3075
  }
2683
3076
  else if (typeof field === 'string') {
@@ -2724,9 +3117,9 @@ class CollectionQueryBuilder {
2724
3117
  else {
2725
3118
  // No selector - select all fields from the target table schema
2726
3119
  if (this.targetTableSchema && this.targetTableSchema.columns) {
2727
- for (const [colName, colBuilder] of Object.entries(this.targetTableSchema.columns)) {
2728
- const colConfig = colBuilder.build ? colBuilder.build() : colBuilder;
2729
- const dbColumnName = colConfig.name || colName;
3120
+ // Performance: Use cached column name map
3121
+ const colNameMap = getColumnNameMapForSchema(this.targetTableSchema);
3122
+ for (const [colName, dbColumnName] of colNameMap) {
2730
3123
  selectedFieldConfigs.push({
2731
3124
  alias: colName,
2732
3125
  expression: `"${dbColumnName}"`,
@@ -2756,13 +3149,11 @@ class CollectionQueryBuilder {
2756
3149
  // Step 3: Build ORDER BY clause SQL (without ORDER BY keyword)
2757
3150
  let orderByClause;
2758
3151
  if (this.orderByFields.length > 0) {
3152
+ // Performance: Pre-compute column name map for ORDER BY lookups
3153
+ const colNameMap = this.targetTableSchema ? getColumnNameMapForSchema(this.targetTableSchema) : null;
2759
3154
  const orderParts = this.orderByFields.map(({ field, direction }) => {
2760
- // Look up the database column name from the schema if available
2761
- let dbColumnName = field;
2762
- if (this.targetTableSchema && this.targetTableSchema.columns[field]) {
2763
- const colBuilder = this.targetTableSchema.columns[field];
2764
- dbColumnName = colBuilder.build().name;
2765
- }
3155
+ // Look up the database column name from the cached map if available
3156
+ const dbColumnName = colNameMap?.get(field) ?? field;
2766
3157
  return `"${dbColumnName}" ${direction}`;
2767
3158
  });
2768
3159
  orderByClause = orderParts.join(', ');
@@ -2802,7 +3193,14 @@ class CollectionQueryBuilder {
2802
3193
  aggregationType = 'jsonb';
2803
3194
  defaultValue = "'[]'::jsonb";
2804
3195
  }
2805
- // Step 5: Build CollectionAggregationConfig object
3196
+ // Step 5: Detect navigation joins from the selected fields
3197
+ const navigationJoins = [];
3198
+ if (this.selector && this.targetTableSchema) {
3199
+ const mockItem = this.createMockItem();
3200
+ const selectedFields = this.selector(mockItem);
3201
+ this.detectNavigationJoins(selectedFields, navigationJoins, this.targetTable, this.targetTableSchema);
3202
+ }
3203
+ // Step 6: Build CollectionAggregationConfig object
2806
3204
  const config = {
2807
3205
  relationName: this.relationName,
2808
3206
  targetTable: this.targetTable,
@@ -2821,6 +3219,7 @@ class CollectionQueryBuilder {
2821
3219
  arrayField,
2822
3220
  defaultValue,
2823
3221
  counter: context.cteCounter++,
3222
+ navigationJoins: navigationJoins.length > 0 ? navigationJoins : undefined,
2824
3223
  };
2825
3224
  // Step 6: Call the strategy
2826
3225
  const result = strategy.buildAggregation(config, context, client);