linkgress-orm 0.0.3 → 0.1.1

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 (73) hide show
  1. package/README.md +3 -3
  2. package/dist/entity/db-column.d.ts +38 -1
  3. package/dist/entity/db-column.d.ts.map +1 -1
  4. package/dist/entity/db-column.js.map +1 -1
  5. package/dist/entity/db-context.d.ts +429 -50
  6. package/dist/entity/db-context.d.ts.map +1 -1
  7. package/dist/entity/db-context.js +884 -203
  8. package/dist/entity/db-context.js.map +1 -1
  9. package/dist/entity/entity-base.d.ts +8 -0
  10. package/dist/entity/entity-base.d.ts.map +1 -1
  11. package/dist/entity/entity-base.js.map +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +4 -3
  15. package/dist/index.js.map +1 -1
  16. package/dist/query/collection-strategy.factory.d.ts.map +1 -1
  17. package/dist/query/collection-strategy.factory.js +7 -3
  18. package/dist/query/collection-strategy.factory.js.map +1 -1
  19. package/dist/query/collection-strategy.interface.d.ts +12 -6
  20. package/dist/query/collection-strategy.interface.d.ts.map +1 -1
  21. package/dist/query/conditions.d.ts +134 -23
  22. package/dist/query/conditions.d.ts.map +1 -1
  23. package/dist/query/conditions.js +58 -0
  24. package/dist/query/conditions.js.map +1 -1
  25. package/dist/query/cte-builder.d.ts +24 -5
  26. package/dist/query/cte-builder.d.ts.map +1 -1
  27. package/dist/query/cte-builder.js +45 -7
  28. package/dist/query/cte-builder.js.map +1 -1
  29. package/dist/query/grouped-query.d.ts +196 -8
  30. package/dist/query/grouped-query.d.ts.map +1 -1
  31. package/dist/query/grouped-query.js +586 -54
  32. package/dist/query/grouped-query.js.map +1 -1
  33. package/dist/query/join-builder.d.ts +5 -4
  34. package/dist/query/join-builder.d.ts.map +1 -1
  35. package/dist/query/join-builder.js +21 -47
  36. package/dist/query/join-builder.js.map +1 -1
  37. package/dist/query/query-builder.d.ts +118 -20
  38. package/dist/query/query-builder.d.ts.map +1 -1
  39. package/dist/query/query-builder.js +511 -280
  40. package/dist/query/query-builder.js.map +1 -1
  41. package/dist/query/query-utils.d.ts +45 -0
  42. package/dist/query/query-utils.d.ts.map +1 -0
  43. package/dist/query/query-utils.js +103 -0
  44. package/dist/query/query-utils.js.map +1 -0
  45. package/dist/query/sql-utils.d.ts +83 -0
  46. package/dist/query/sql-utils.d.ts.map +1 -0
  47. package/dist/query/sql-utils.js +218 -0
  48. package/dist/query/sql-utils.js.map +1 -0
  49. package/dist/query/strategies/cte-collection-strategy.d.ts +85 -0
  50. package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -0
  51. package/dist/query/strategies/cte-collection-strategy.js +338 -0
  52. package/dist/query/strategies/cte-collection-strategy.js.map +1 -0
  53. package/dist/query/strategies/lateral-collection-strategy.d.ts +59 -0
  54. package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -0
  55. package/dist/query/strategies/lateral-collection-strategy.js +243 -0
  56. package/dist/query/strategies/lateral-collection-strategy.js.map +1 -0
  57. package/dist/query/strategies/temptable-collection-strategy.d.ts +21 -0
  58. package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
  59. package/dist/query/strategies/temptable-collection-strategy.js +160 -38
  60. package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
  61. package/dist/query/subquery.d.ts +24 -1
  62. package/dist/query/subquery.d.ts.map +1 -1
  63. package/dist/query/subquery.js +38 -2
  64. package/dist/query/subquery.js.map +1 -1
  65. package/dist/schema/table-builder.d.ts +16 -0
  66. package/dist/schema/table-builder.d.ts.map +1 -1
  67. package/dist/schema/table-builder.js +23 -1
  68. package/dist/schema/table-builder.js.map +1 -1
  69. package/package.json +1 -1
  70. package/dist/query/strategies/jsonb-collection-strategy.d.ts +0 -51
  71. package/dist/query/strategies/jsonb-collection-strategy.d.ts.map +0 -1
  72. package/dist/query/strategies/jsonb-collection-strategy.js +0 -210
  73. package/dist/query/strategies/jsonb-collection-strategy.js.map +0 -1
@@ -1,11 +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");
9
+ const query_utils_1 = require("./query-utils");
5
10
  const subquery_1 = require("./subquery");
6
11
  const grouped_query_1 = require("./grouped-query");
7
12
  const cte_builder_1 = require("./cte-builder");
8
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
+ }
9
115
  /**
10
116
  * Cached regex for numeric string detection
11
117
  * Used to convert PostgreSQL NUMERIC/BIGINT strings to numbers
@@ -38,6 +144,7 @@ class QueryBuilder {
38
144
  }
39
145
  /**
40
146
  * Define the selection with support for nested queries
147
+ * UnwrapSelection extracts the value types from SqlFragment<T> expressions
41
148
  */
42
149
  select(selector) {
43
150
  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
@@ -47,12 +154,25 @@ class QueryBuilder {
47
154
  }
48
155
  /**
49
156
  * Add WHERE condition
157
+ * Multiple where() calls are chained with AND logic
50
158
  */
51
159
  where(condition) {
52
160
  const mockRow = this.createMockRow();
53
- this.whereCond = condition(mockRow);
161
+ const newCondition = condition(mockRow);
162
+ if (this.whereCond) {
163
+ this.whereCond = (0, conditions_1.and)(this.whereCond, newCondition);
164
+ }
165
+ else {
166
+ this.whereCond = newCondition;
167
+ }
54
168
  return this;
55
169
  }
170
+ /**
171
+ * Add CTEs (Common Table Expressions) to the query
172
+ */
173
+ with(...ctes) {
174
+ 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
+ }
56
176
  /**
57
177
  * Create mock row for analysis
58
178
  */
@@ -62,14 +182,10 @@ class QueryBuilder {
62
182
  return this._cachedMockRow;
63
183
  }
64
184
  const mock = {};
65
- // Performance: Build column configs once and cache them
66
- const columnEntries = Object.entries(this.schema.columns);
67
- const columnConfigs = new Map();
68
- for (const [colName, colBuilder] of columnEntries) {
69
- columnConfigs.set(colName, colBuilder.build().name);
70
- }
185
+ // Performance: Use pre-computed column name map if available
186
+ const columnNameMap = getColumnNameMapForSchema(this.schema);
71
187
  // Add columns as FieldRef objects - type-safe with property name and database column name
72
- for (const [colName, dbColumnName] of columnConfigs) {
188
+ for (const [colName, dbColumnName] of columnNameMap) {
73
189
  Object.defineProperty(mock, colName, {
74
190
  get: () => ({
75
191
  __fieldName: colName,
@@ -80,17 +196,13 @@ class QueryBuilder {
80
196
  configurable: true,
81
197
  });
82
198
  }
83
- // Performance: Cache target schemas for relations to avoid repeated .build() calls
84
- const relationSchemas = new Map();
85
- for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
86
- if (relConfig.targetTableBuilder) {
87
- relationSchemas.set(relName, relConfig.targetTableBuilder.build());
88
- }
89
- }
199
+ // Performance: Use pre-computed relation entries and cached schemas
200
+ const relationEntries = getRelationEntriesForSchema(this.schema);
90
201
  // Add relations (both collections and single references)
91
- for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
202
+ for (const [relName, relConfig] of relationEntries) {
203
+ // Performance: Use cached target schema
204
+ const targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
92
205
  if (relConfig.type === 'many') {
93
- const targetSchema = relationSchemas.get(relName);
94
206
  Object.defineProperty(mock, relName, {
95
207
  get: () => {
96
208
  return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema);
@@ -101,7 +213,6 @@ class QueryBuilder {
101
213
  }
102
214
  else {
103
215
  // Single reference navigation (many-to-one, one-to-one)
104
- const targetSchema = relationSchemas.get(relName);
105
216
  Object.defineProperty(mock, relName, {
106
217
  get: () => {
107
218
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
@@ -118,6 +229,7 @@ class QueryBuilder {
118
229
  }
119
230
  /**
120
231
  * Add a LEFT JOIN to the query with a selector (supports both tables and subqueries)
232
+ * UnwrapSelection extracts the value types from SqlFragment<T> expressions
121
233
  */
122
234
  leftJoin(rightTable, condition, selector, alias) {
123
235
  // Check if rightTable is a Subquery
@@ -166,6 +278,7 @@ class QueryBuilder {
166
278
  }
167
279
  /**
168
280
  * Add an INNER JOIN to the query with a selector (supports both tables and subqueries)
281
+ * UnwrapSelection extracts the value types from SqlFragment<T> expressions
169
282
  */
170
283
  innerJoin(rightTable, condition, selector, alias) {
171
284
  // Check if rightTable is a Subquery
@@ -217,14 +330,10 @@ class QueryBuilder {
217
330
  */
218
331
  createMockRowForTable(schema, alias) {
219
332
  const mock = {};
220
- // Performance: Build column configs once and cache them
221
- const columnEntries = Object.entries(schema.columns);
222
- const columnConfigs = new Map();
223
- for (const [colName, colBuilder] of columnEntries) {
224
- columnConfigs.set(colName, colBuilder.build().name);
225
- }
333
+ // Performance: Use pre-computed column name map if available
334
+ const columnNameMap = getColumnNameMapForSchema(schema);
226
335
  // Add columns as FieldRef objects with table alias
227
- for (const [colName, dbColumnName] of columnConfigs) {
336
+ for (const [colName, dbColumnName] of columnNameMap) {
228
337
  Object.defineProperty(mock, colName, {
229
338
  get: () => ({
230
339
  __fieldName: colName,
@@ -235,18 +344,14 @@ class QueryBuilder {
235
344
  configurable: true,
236
345
  });
237
346
  }
238
- // Performance: Cache target schemas for relations
239
- const relationSchemas = new Map();
240
- for (const [relName, relConfig] of Object.entries(schema.relations)) {
241
- if (relConfig.targetTableBuilder) {
242
- relationSchemas.set(relName, relConfig.targetTableBuilder.build());
243
- }
244
- }
347
+ // Performance: Use pre-computed relation entries and cached schemas
348
+ const relationEntries = getRelationEntriesForSchema(schema);
245
349
  // Add navigation properties (single references and collections)
246
- for (const [relName, relConfig] of Object.entries(schema.relations)) {
350
+ for (const [relName, relConfig] of relationEntries) {
351
+ // Performance: Use cached target schema
352
+ const targetSchema = getTargetSchemaForRelation(schema, relName, relConfig);
247
353
  if (relConfig.type === 'many') {
248
354
  // Collection navigation
249
- const targetSchema = relationSchemas.get(relName);
250
355
  Object.defineProperty(mock, relName, {
251
356
  get: () => {
252
357
  return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
@@ -257,7 +362,6 @@ class QueryBuilder {
257
362
  }
258
363
  else {
259
364
  // Single reference navigation
260
- const targetSchema = relationSchemas.get(relName);
261
365
  Object.defineProperty(mock, relName, {
262
366
  get: () => {
263
367
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
@@ -287,35 +391,7 @@ class QueryBuilder {
287
391
  orderBy(selector) {
288
392
  const mockRow = this.createMockRow();
289
393
  const result = selector(mockRow);
290
- // Handle array of [field, direction] tuples
291
- if (Array.isArray(result) && result.length > 0 && Array.isArray(result[0])) {
292
- for (const [fieldRef, direction] of result) {
293
- if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
294
- this.orderByFields.push({
295
- field: fieldRef.__dbColumnName || fieldRef.__fieldName,
296
- direction: direction || 'ASC'
297
- });
298
- }
299
- }
300
- }
301
- // Handle array of fields (all ASC)
302
- else if (Array.isArray(result)) {
303
- for (const fieldRef of result) {
304
- if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
305
- this.orderByFields.push({
306
- field: fieldRef.__dbColumnName || fieldRef.__fieldName,
307
- direction: 'ASC'
308
- });
309
- }
310
- }
311
- }
312
- // Handle single field
313
- else if (result && typeof result === 'object' && '__fieldName' in result) {
314
- this.orderByFields.push({
315
- field: result.__dbColumnName || result.__fieldName,
316
- direction: 'ASC'
317
- });
318
- }
394
+ (0, query_utils_1.parseOrderBy)(result, this.orderByFields);
319
395
  return this;
320
396
  }
321
397
  }
@@ -353,6 +429,7 @@ class SelectQueryBuilder {
353
429
  }
354
430
  /**
355
431
  * Transform the selection with a new selector
432
+ * UnwrapSelection extracts the value types from SqlFragment<T> expressions
356
433
  */
357
434
  select(selector) {
358
435
  // Create a composed selector that applies both transformations
@@ -364,13 +441,22 @@ class SelectQueryBuilder {
364
441
  }
365
442
  /**
366
443
  * Add WHERE condition
444
+ * Multiple where() calls are chained with AND logic
367
445
  * Note: The row parameter represents the selected shape (after select())
368
446
  */
369
447
  where(condition) {
370
448
  const mockRow = this.createMockRow();
371
449
  // Apply the selector to get the selected shape that the user sees in the WHERE condition
372
450
  const selectedMock = this.selector(mockRow);
373
- this.whereCond = condition(selectedMock);
451
+ // Wrap in proxy - for WHERE, we preserve original column names
452
+ const fieldRefProxy = this.createFieldRefProxy(selectedMock, true);
453
+ const newCondition = condition(fieldRefProxy);
454
+ if (this.whereCond) {
455
+ this.whereCond = (0, conditions_1.and)(this.whereCond, newCondition);
456
+ }
457
+ else {
458
+ this.whereCond = newCondition;
459
+ }
374
460
  return this;
375
461
  }
376
462
  /**
@@ -384,7 +470,12 @@ class SelectQueryBuilder {
384
470
  * .toList();
385
471
  */
386
472
  with(...ctes) {
387
- this.ctes.push(...ctes);
473
+ // Add CTEs, avoiding duplicates by name
474
+ for (const cte of ctes) {
475
+ if (!this.ctes.some(existing => existing.name === cte.name)) {
476
+ this.ctes.push(cte);
477
+ }
478
+ }
388
479
  return this;
389
480
  }
390
481
  /**
@@ -404,36 +495,12 @@ class SelectQueryBuilder {
404
495
  orderBy(selector) {
405
496
  const mockRow = this.createMockRow();
406
497
  const selectedMock = this.selector(mockRow);
407
- const result = selector(selectedMock);
408
- // Handle array of [field, direction] tuples
409
- if (Array.isArray(result) && result.length > 0 && Array.isArray(result[0])) {
410
- for (const [fieldRef, direction] of result) {
411
- if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
412
- this.orderByFields.push({
413
- field: fieldRef.__dbColumnName || fieldRef.__fieldName,
414
- direction: direction || 'ASC'
415
- });
416
- }
417
- }
418
- }
419
- // Handle array of fields (all ASC)
420
- else if (Array.isArray(result)) {
421
- for (const fieldRef of result) {
422
- if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
423
- this.orderByFields.push({
424
- field: fieldRef.__dbColumnName || fieldRef.__fieldName,
425
- direction: 'ASC'
426
- });
427
- }
428
- }
429
- }
430
- // Handle single field
431
- else if (result && typeof result === 'object' && '__fieldName' in result) {
432
- this.orderByFields.push({
433
- field: result.__dbColumnName || result.__fieldName,
434
- direction: 'ASC'
435
- });
436
- }
498
+ // Wrap selectedMock in a proxy that returns FieldRefs for property access
499
+ const fieldRefProxy = this.createFieldRefProxy(selectedMock);
500
+ const result = selector(fieldRefProxy);
501
+ // Clear previous orderBy - last one takes precedence
502
+ this.orderByFields = [];
503
+ (0, query_utils_1.parseOrderBy)(result, this.orderByFields);
437
504
  return this;
438
505
  }
439
506
  /**
@@ -483,10 +550,7 @@ class SelectQueryBuilder {
483
550
  };
484
551
  return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy);
485
552
  }
486
- /**
487
- * Add a LEFT JOIN to the query with a selector
488
- * Note: After select(), the left parameter in the join will be the selected shape (TSelection)
489
- */
553
+ // Implementation
490
554
  leftJoin(rightTable, condition, selector, alias) {
491
555
  // Check if rightTable is a CTE
492
556
  if ((0, cte_builder_1.isCte)(rightTable)) {
@@ -596,6 +660,7 @@ class SelectQueryBuilder {
596
660
  /**
597
661
  * Add an INNER JOIN to the query with a selector
598
662
  * Note: After select(), the left parameter in the join will be the selected shape (TSelection)
663
+ * UnwrapSelection extracts the value types from SqlFragment<T> expressions
599
664
  */
600
665
  innerJoin(rightTable, condition, selector, alias) {
601
666
  // Check if rightTable is a Subquery
@@ -637,14 +702,10 @@ class SelectQueryBuilder {
637
702
  */
638
703
  createMockRowForTable(schema, alias) {
639
704
  const mock = {};
640
- // Performance: Build column configs once and cache them
641
- const columnEntries = Object.entries(schema.columns);
642
- const columnConfigs = new Map();
643
- for (const [colName, colBuilder] of columnEntries) {
644
- columnConfigs.set(colName, colBuilder.build().name);
645
- }
705
+ // Performance: Use pre-computed column name map if available
706
+ const columnNameMap = getColumnNameMapForSchema(schema);
646
707
  // Add columns as FieldRef objects with table alias
647
- for (const [colName, dbColumnName] of columnConfigs) {
708
+ for (const [colName, dbColumnName] of columnNameMap) {
648
709
  Object.defineProperty(mock, colName, {
649
710
  get: () => ({
650
711
  __fieldName: colName,
@@ -655,18 +716,14 @@ class SelectQueryBuilder {
655
716
  configurable: true,
656
717
  });
657
718
  }
658
- // Performance: Cache target schemas for relations
659
- const relationSchemas = new Map();
660
- for (const [relName, relConfig] of Object.entries(schema.relations)) {
661
- if (relConfig.targetTableBuilder) {
662
- relationSchemas.set(relName, relConfig.targetTableBuilder.build());
663
- }
664
- }
719
+ // Performance: Use pre-computed relation entries and cached schemas
720
+ const relationEntries = getRelationEntriesForSchema(schema);
665
721
  // Add navigation properties (single references and collections)
666
- for (const [relName, relConfig] of Object.entries(schema.relations)) {
722
+ for (const [relName, relConfig] of relationEntries) {
723
+ // Performance: Use cached target schema
724
+ const targetSchema = getTargetSchemaForRelation(schema, relName, relConfig);
667
725
  if (relConfig.type === 'many') {
668
726
  // Collection navigation
669
- const targetSchema = relationSchemas.get(relName);
670
727
  Object.defineProperty(mock, relName, {
671
728
  get: () => {
672
729
  return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
@@ -677,7 +734,6 @@ class SelectQueryBuilder {
677
734
  }
678
735
  else {
679
736
  // Single reference navigation
680
- const targetSchema = relationSchemas.get(relName);
681
737
  Object.defineProperty(mock, relName, {
682
738
  get: () => {
683
739
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
@@ -1265,9 +1321,10 @@ class SelectQueryBuilder {
1265
1321
  */
1266
1322
  createMockRow() {
1267
1323
  const mock = {};
1324
+ // Performance: Use pre-computed column name map if available
1325
+ const columnNameMap = getColumnNameMapForSchema(this.schema);
1268
1326
  // Add columns as FieldRef objects - type-safe with property name and database column name
1269
- for (const [colName, colBuilder] of Object.entries(this.schema.columns)) {
1270
- const dbColumnName = colBuilder.build().name;
1327
+ for (const [colName, dbColumnName] of columnNameMap) {
1271
1328
  Object.defineProperty(mock, colName, {
1272
1329
  get: () => ({
1273
1330
  __fieldName: colName,
@@ -1284,13 +1341,12 @@ class SelectQueryBuilder {
1284
1341
  if (join.isSubquery || !join.schema) {
1285
1342
  continue;
1286
1343
  }
1287
- for (const [colName, colBuilder] of Object.entries(join.schema.columns)) {
1288
- const dbColumnName = colBuilder.build().name;
1289
- // Create a unique property name by prefixing with table alias or using the column name directly
1290
- // For now, we'll create nested objects for each joined table
1291
- if (!mock[join.alias]) {
1292
- mock[join.alias] = {};
1293
- }
1344
+ // Performance: Use pre-computed column name map for joined schema
1345
+ const joinColumnNameMap = getColumnNameMapForSchema(join.schema);
1346
+ if (!mock[join.alias]) {
1347
+ mock[join.alias] = {};
1348
+ }
1349
+ for (const [colName, dbColumnName] of joinColumnNameMap) {
1294
1350
  Object.defineProperty(mock[join.alias], colName, {
1295
1351
  get: () => ({
1296
1352
  __fieldName: colName,
@@ -1302,13 +1358,23 @@ class SelectQueryBuilder {
1302
1358
  });
1303
1359
  }
1304
1360
  }
1361
+ // Performance: Use pre-computed relation entries
1362
+ const relationEntries = getRelationEntriesForSchema(this.schema);
1305
1363
  // Add relations as CollectionQueryBuilder or ReferenceQueryBuilder
1306
- for (const [relName, relConfig] of Object.entries(this.schema.relations)) {
1364
+ for (const [relName, relConfig] of relationEntries) {
1365
+ // Try to get target schema from registry (preferred, has full relations) or cached schema
1366
+ let targetSchema;
1367
+ if (this.schemaRegistry) {
1368
+ targetSchema = this.schemaRegistry.get(relConfig.targetTable);
1369
+ }
1370
+ if (!targetSchema) {
1371
+ // Performance: Use cached target schema
1372
+ targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
1373
+ }
1307
1374
  if (relConfig.type === 'many') {
1308
1375
  Object.defineProperty(mock, relName, {
1309
1376
  get: () => {
1310
- // Don't call build() - force registry lookup to get schema with relations
1311
- return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, undefined, // Don't pass schema, force registry lookup
1377
+ return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, // Pass the target schema directly
1312
1378
  this.schemaRegistry // Pass schema registry for nested resolution
1313
1379
  );
1314
1380
  },
@@ -1320,8 +1386,7 @@ class SelectQueryBuilder {
1320
1386
  // For single reference (many-to-one), create a ReferenceQueryBuilder
1321
1387
  Object.defineProperty(mock, relName, {
1322
1388
  get: () => {
1323
- // Don't call build() - force registry lookup to get schema with relations
1324
- const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, undefined, // Don't pass schema, force registry lookup
1389
+ const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, // Pass the target schema directly
1325
1390
  this.schemaRegistry // Pass schema registry for nested resolution
1326
1391
  );
1327
1392
  // Return a mock object that exposes the target table's columns
@@ -1334,6 +1399,67 @@ class SelectQueryBuilder {
1334
1399
  }
1335
1400
  return mock;
1336
1401
  }
1402
+ /**
1403
+ * Create a proxy that wraps selected values and returns FieldRefs for property access
1404
+ * This enables orderBy and other operations to work with chained selects
1405
+ * @param preserveOriginal - If true (for WHERE), preserve original column names; if false (for ORDER BY), use alias names
1406
+ */
1407
+ createFieldRefProxy(selectedMock, preserveOriginal = false) {
1408
+ if (!selectedMock || typeof selectedMock !== 'object') {
1409
+ return selectedMock;
1410
+ }
1411
+ // If it already has FieldRef properties, return as-is
1412
+ if ('__fieldName' in selectedMock && '__dbColumnName' in selectedMock) {
1413
+ return selectedMock;
1414
+ }
1415
+ // Create a proxy that returns FieldRefs for each property access
1416
+ return new Proxy(selectedMock, {
1417
+ get: (target, prop) => {
1418
+ if (typeof prop === 'symbol' || prop === 'constructor' || prop === 'then') {
1419
+ return target[prop];
1420
+ }
1421
+ const value = target[prop];
1422
+ // If the value is already a FieldRef
1423
+ if (value && typeof value === 'object' && '__fieldName' in value && '__dbColumnName' in value) {
1424
+ if (preserveOriginal) {
1425
+ // For WHERE: preserve original column name and table alias
1426
+ // This ensures WHERE references the actual database column
1427
+ return {
1428
+ __fieldName: prop,
1429
+ __dbColumnName: value.__dbColumnName,
1430
+ __tableAlias: value.__tableAlias,
1431
+ };
1432
+ }
1433
+ else {
1434
+ // For ORDER BY: use the alias (property name) as the column name
1435
+ // In chained selects, the alias becomes the column name in the subquery
1436
+ return {
1437
+ __fieldName: prop,
1438
+ __dbColumnName: prop,
1439
+ // No table alias - column comes from the selection/subquery
1440
+ };
1441
+ }
1442
+ }
1443
+ // If the value is a SqlFragment, treat it as a FieldRef using the property name as the alias
1444
+ if (value && typeof value === 'object' && value instanceof conditions_1.SqlFragment) {
1445
+ return {
1446
+ __fieldName: prop,
1447
+ __dbColumnName: prop,
1448
+ };
1449
+ }
1450
+ // If the value is an object (nested selection), recursively wrap it
1451
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
1452
+ return this.createFieldRefProxy(value, preserveOriginal);
1453
+ }
1454
+ // For primitive values or arrays, create a FieldRef
1455
+ // Use the property name as both fieldName and dbColumnName
1456
+ return {
1457
+ __fieldName: prop,
1458
+ __dbColumnName: prop,
1459
+ };
1460
+ }
1461
+ });
1462
+ }
1337
1463
  /**
1338
1464
  * Detect navigation property references in selection and add necessary JOINs
1339
1465
  */
@@ -1344,9 +1470,66 @@ class SelectQueryBuilder {
1344
1470
  for (const [key, value] of Object.entries(selection)) {
1345
1471
  if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
1346
1472
  // This is a FieldRef with a table alias - check if it's from a related table
1347
- const tableAlias = value.__tableAlias;
1473
+ this.addJoinForFieldRef(value, joins);
1474
+ }
1475
+ else if (value instanceof conditions_1.SqlFragment) {
1476
+ // SqlFragment may contain navigation property references - extract them
1477
+ const fieldRefs = value.getFieldRefs();
1478
+ for (const fieldRef of fieldRefs) {
1479
+ this.addJoinForFieldRef(fieldRef, joins);
1480
+ }
1481
+ }
1482
+ else if (value && typeof value === 'object' && !Array.isArray(value)) {
1483
+ // Recursively check nested objects
1484
+ this.detectAndAddJoinsFromSelection(value, joins);
1485
+ }
1486
+ }
1487
+ }
1488
+ /**
1489
+ * Add a JOIN for a FieldRef if it references a related table
1490
+ */
1491
+ addJoinForFieldRef(fieldRef, joins) {
1492
+ if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef) || !('__dbColumnName' in fieldRef)) {
1493
+ return;
1494
+ }
1495
+ const tableAlias = fieldRef.__tableAlias;
1496
+ if (tableAlias && tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
1497
+ // This references a related table - find the relation and add a JOIN
1498
+ const relation = this.schema.relations[tableAlias];
1499
+ if (relation && relation.type === 'one') {
1500
+ // Get target schema from targetTableBuilder if available
1501
+ let targetSchema;
1502
+ if (relation.targetTableBuilder) {
1503
+ const targetTableSchema = relation.targetTableBuilder.build();
1504
+ targetSchema = targetTableSchema.schema;
1505
+ }
1506
+ // Add a JOIN for this reference
1507
+ joins.push({
1508
+ alias: tableAlias,
1509
+ targetTable: relation.targetTable,
1510
+ targetSchema,
1511
+ foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
1512
+ matches: relation.matches || [],
1513
+ isMandatory: relation.isMandatory ?? false,
1514
+ });
1515
+ }
1516
+ }
1517
+ }
1518
+ /**
1519
+ * Detect navigation property references in a WHERE condition and add necessary JOINs
1520
+ */
1521
+ detectAndAddJoinsFromCondition(condition, joins) {
1522
+ if (!condition) {
1523
+ return;
1524
+ }
1525
+ // Get all field references from the condition
1526
+ const fieldRefs = condition.getFieldRefs();
1527
+ for (const fieldRef of fieldRefs) {
1528
+ if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
1529
+ const tableAlias = fieldRef.__tableAlias;
1530
+ // Check if this references a related table that isn't already joined
1348
1531
  if (tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
1349
- // This references a related table - find the relation and add a JOIN
1532
+ // Find the relation config for this navigation
1350
1533
  const relation = this.schema.relations[tableAlias];
1351
1534
  if (relation && relation.type === 'one') {
1352
1535
  // Get target schema from targetTableBuilder if available
@@ -1367,10 +1550,6 @@ class SelectQueryBuilder {
1367
1550
  }
1368
1551
  }
1369
1552
  }
1370
- else if (value && typeof value === 'object' && !Array.isArray(value)) {
1371
- // Recursively check nested objects
1372
- this.detectAndAddJoinsFromSelection(value, joins);
1373
- }
1374
1553
  }
1375
1554
  }
1376
1555
  /**
@@ -1387,6 +1566,8 @@ class SelectQueryBuilder {
1387
1566
  const joins = [];
1388
1567
  // Scan selection for navigation property references and add JOINs
1389
1568
  this.detectAndAddJoinsFromSelection(selection, joins);
1569
+ // Scan WHERE condition for navigation property references and add JOINs
1570
+ this.detectAndAddJoinsFromCondition(this.whereCond, joins);
1390
1571
  // Handle case where selection is a single value (not an object with properties)
1391
1572
  if (selection instanceof conditions_1.SqlFragment) {
1392
1573
  // Single SQL fragment - just build it directly
@@ -1411,11 +1592,19 @@ class SelectQueryBuilder {
1411
1592
  // Process selection object properties
1412
1593
  for (const [key, value] of Object.entries(selection)) {
1413
1594
  if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
1414
- // Handle collection - create CTE (works for both CollectionQueryBuilder and CollectionResult)
1415
- const cteName = `cte_${context.cteCounter++}`;
1595
+ // Handle collection - delegate to strategy pattern via buildCTE
1596
+ // The strategy handles CTE/LATERAL specifics and returns necessary info
1416
1597
  const cteData = value.buildCTE ? value.buildCTE(context) : value.buildCTE(context);
1417
- context.ctes.set(cteName, cteData);
1418
- collectionFields.push({ name: key, cteName });
1598
+ const isCTE = cteData.isCTE !== false; // Default to CTE if not specified
1599
+ // Note: For CTE strategy, context.ctes is already populated by the strategy
1600
+ // We don't need to add it again - the strategy has already done this
1601
+ collectionFields.push({
1602
+ name: key,
1603
+ cteName: cteData.tableName || `cte_${context.cteCounter - 1}`, // Use tableName from result or infer from counter
1604
+ isCTE,
1605
+ joinClause: cteData.joinClause,
1606
+ selectExpression: cteData.selectExpression,
1607
+ });
1419
1608
  }
1420
1609
  else if (value instanceof subquery_1.Subquery || (value && typeof value === 'object' && 'buildSql' in value && typeof value.buildSql === 'function' && '__mode' in value)) {
1421
1610
  // Handle Subquery - build SQL and wrap in parentheses
@@ -1512,9 +1701,10 @@ class SelectQueryBuilder {
1512
1701
  // Select all columns from the target table and group them
1513
1702
  // We'll need to use JSON object building in SQL
1514
1703
  const fieldParts = [];
1515
- for (const [colKey, col] of Object.entries(targetSchema.columns)) {
1516
- const config = col.build();
1517
- fieldParts.push(`'${colKey}', "${alias}"."${config.name}"`);
1704
+ // Performance: Use cached column name map
1705
+ const targetColMap = getColumnNameMapForSchema(targetSchema);
1706
+ for (const [colKey, dbColName] of targetColMap) {
1707
+ fieldParts.push(`'${colKey}', "${alias}"."${dbColName}"`);
1518
1708
  }
1519
1709
  selectParts.push(`json_build_object(${fieldParts.join(', ')}) as "${key}"`);
1520
1710
  }
@@ -1544,11 +1734,8 @@ class SelectQueryBuilder {
1544
1734
  const relConfig = this.schema.relations[alias];
1545
1735
  if (relConfig && relConfig.type === 'one') {
1546
1736
  // This is a reference navigation - select all fields from the target table
1547
- // Find the target table schema
1548
- let targetSchema;
1549
- if (relConfig.targetTableBuilder) {
1550
- targetSchema = relConfig.targetTableBuilder.build();
1551
- }
1737
+ // Performance: Use cached target schema
1738
+ const targetSchema = getTargetSchemaForRelation(this.schema, alias, relConfig);
1552
1739
  if (targetSchema) {
1553
1740
  // Add JOIN if not already added
1554
1741
  if (!joins.find(j => j.alias === alias)) {
@@ -1567,9 +1754,10 @@ class SelectQueryBuilder {
1567
1754
  }
1568
1755
  // Select all columns from the target table and group them into a JSON object
1569
1756
  const fieldParts = [];
1570
- for (const [colKey, col] of Object.entries(targetSchema.columns)) {
1571
- const config = col.build();
1572
- fieldParts.push(`'${colKey}', "${alias}"."${config.name}"`);
1757
+ // Performance: Use cached column name map
1758
+ const targetColMap = getColumnNameMapForSchema(targetSchema);
1759
+ for (const [colKey, dbColName] of targetColMap) {
1760
+ fieldParts.push(`'${colKey}', "${alias}"."${dbColName}"`);
1573
1761
  }
1574
1762
  selectParts.push(`json_build_object(${fieldParts.join(', ')}) as "${key}"`);
1575
1763
  continue;
@@ -1601,8 +1789,14 @@ class SelectQueryBuilder {
1601
1789
  }
1602
1790
  } // End of for loop
1603
1791
  } // End of else block
1604
- // Add collection fields as JSON/array aggregations joined from CTEs
1605
- for (const { name, cteName } of collectionFields) {
1792
+ // Add collection fields as JSON/array aggregations joined from CTEs or LATERAL joins
1793
+ for (const { name, cteName, selectExpression } of collectionFields) {
1794
+ // If selectExpression is provided (from strategy), use it directly
1795
+ if (selectExpression) {
1796
+ selectParts.push(`${selectExpression} as "${name}"`);
1797
+ continue;
1798
+ }
1799
+ // Fallback to old logic for backward compatibility
1606
1800
  // Check if this is an array aggregation (from toNumberList/toStringList)
1607
1801
  const collectionValue = selection[name];
1608
1802
  const isArrayAgg = collectionValue && typeof collectionValue === 'object' && 'isArrayAggregation' in collectionValue && collectionValue.isArrayAggregation();
@@ -1728,9 +1922,16 @@ class SelectQueryBuilder {
1728
1922
  const joinTableName = this.getQualifiedTableName(join.targetTable, join.targetSchema);
1729
1923
  fromClause += `\n${joinType} ${joinTableName} AS "${join.alias}" ON ${onConditions.join(' AND ')}`;
1730
1924
  }
1731
- // Join CTEs for collections
1732
- for (const { cteName } of collectionFields) {
1733
- fromClause += `\nLEFT JOIN "${cteName}" ON "${cteName}".parent_id = ${qualifiedTableName}.id`;
1925
+ // Join CTEs and LATERAL subqueries for collections
1926
+ for (const { cteName, isCTE, joinClause } of collectionFields) {
1927
+ if (isCTE) {
1928
+ // CTE strategy - join by parent_id
1929
+ fromClause += `\nLEFT JOIN "${cteName}" ON "${cteName}".parent_id = ${qualifiedTableName}.id`;
1930
+ }
1931
+ else if (joinClause) {
1932
+ // LATERAL strategy - use the provided join clause (contains full LATERAL subquery)
1933
+ fromClause += `\n${joinClause}`;
1934
+ }
1734
1935
  }
1735
1936
  // Add DISTINCT if needed
1736
1937
  const distinctClause = this.isDistinct ? 'DISTINCT ' : '';
@@ -2106,7 +2307,35 @@ class SelectQueryBuilder {
2106
2307
  const mockRow = this.createMockRow();
2107
2308
  selectionMetadata = this.selector(mockRow);
2108
2309
  }
2109
- return new subquery_1.Subquery(sqlBuilder, mode, selectionMetadata);
2310
+ // Extract outer field refs from the WHERE condition
2311
+ // These are field refs that reference tables other than this subquery's table
2312
+ // and need to be propagated to the outer query for JOIN detection
2313
+ const outerFieldRefs = this.extractOuterFieldRefs();
2314
+ return new subquery_1.Subquery(sqlBuilder, mode, selectionMetadata, outerFieldRefs);
2315
+ }
2316
+ /**
2317
+ * Extract field refs from the WHERE condition that reference outer queries.
2318
+ * These are field refs with a __tableAlias that doesn't match this query's schema.
2319
+ */
2320
+ extractOuterFieldRefs() {
2321
+ if (!this.whereCond) {
2322
+ return [];
2323
+ }
2324
+ const allRefs = this.whereCond.getFieldRefs();
2325
+ const outerRefs = [];
2326
+ const currentTableName = this.schema.name;
2327
+ for (const ref of allRefs) {
2328
+ // Check if this ref is from an outer query (different table alias)
2329
+ if ('__tableAlias' in ref && ref.__tableAlias) {
2330
+ const tableAlias = ref.__tableAlias;
2331
+ // If the table alias doesn't match our current schema, it's from an outer query
2332
+ // Also check if it's not a navigation property of this table (which would be in schema.relations)
2333
+ if (tableAlias !== currentTableName && !this.schema.relations[tableAlias]) {
2334
+ outerRefs.push(ref);
2335
+ }
2336
+ }
2337
+ }
2338
+ return outerRefs;
2110
2339
  }
2111
2340
  }
2112
2341
  exports.SelectQueryBuilder = SelectQueryBuilder;
@@ -2120,12 +2349,15 @@ class ReferenceQueryBuilder {
2120
2349
  this.foreignKeys = foreignKeys;
2121
2350
  this.matches = matches;
2122
2351
  this.isMandatory = isMandatory;
2123
- this.targetTableSchema = targetTableSchema;
2124
2352
  this.schemaRegistry = schemaRegistry;
2125
- // If targetTableSchema is not provided but we have a registry, look it up
2126
- if (!this.targetTableSchema && this.schemaRegistry) {
2353
+ // Prefer registry lookup (has full relations) over passed schema
2354
+ if (this.schemaRegistry) {
2127
2355
  this.targetTableSchema = this.schemaRegistry.get(targetTable);
2128
2356
  }
2357
+ // Fallback to passed schema if registry lookup failed
2358
+ if (!this.targetTableSchema) {
2359
+ this.targetTableSchema = targetTableSchema;
2360
+ }
2129
2361
  }
2130
2362
  /**
2131
2363
  * Get the alias to use for this reference in the query
@@ -2170,9 +2402,9 @@ class ReferenceQueryBuilder {
2170
2402
  createMockTargetRow() {
2171
2403
  if (this.targetTableSchema) {
2172
2404
  const mock = {};
2173
- // Add columns
2174
- for (const [colName, colBuilder] of Object.entries(this.targetTableSchema.columns)) {
2175
- const dbColumnName = colBuilder.build().name;
2405
+ // Add columns - use pre-computed column name map if available
2406
+ const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
2407
+ for (const [colName, dbColumnName] of columnNameMap) {
2176
2408
  Object.defineProperty(mock, colName, {
2177
2409
  get: () => ({
2178
2410
  __fieldName: colName,
@@ -2186,14 +2418,20 @@ class ReferenceQueryBuilder {
2186
2418
  // Add navigation properties (both collections and references)
2187
2419
  if (this.targetTableSchema.relations) {
2188
2420
  for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
2421
+ // Try to get target schema from registry (preferred, has full relations) or targetTableBuilder
2422
+ let nestedTargetSchema;
2423
+ if (this.schemaRegistry) {
2424
+ nestedTargetSchema = this.schemaRegistry.get(relConfig.targetTable);
2425
+ }
2426
+ if (!nestedTargetSchema && relConfig.targetTableBuilder) {
2427
+ nestedTargetSchema = relConfig.targetTableBuilder.build();
2428
+ }
2189
2429
  if (relConfig.type === 'many') {
2190
2430
  // Collection navigation
2191
2431
  Object.defineProperty(mock, relName, {
2192
2432
  get: () => {
2193
- // Don't call build() - it returns schema without relations
2194
- // Instead, pass undefined and let CollectionQueryBuilder look it up from registry
2195
2433
  const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
2196
- return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.targetTable, undefined, // Don't pass schema, force registry lookup
2434
+ return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.targetTable, nestedTargetSchema, // Pass the target schema directly
2197
2435
  this.schemaRegistry // Pass schema registry for nested resolution
2198
2436
  );
2199
2437
  },
@@ -2205,9 +2443,7 @@ class ReferenceQueryBuilder {
2205
2443
  // Reference navigation
2206
2444
  Object.defineProperty(mock, relName, {
2207
2445
  get: () => {
2208
- // Don't call build() - it returns schema without relations
2209
- // Instead, pass undefined and let ReferenceQueryBuilder look it up from registry
2210
- const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, undefined, // Don't pass schema, force registry lookup
2446
+ const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
2211
2447
  this.schemaRegistry // Pass schema registry for nested resolution
2212
2448
  );
2213
2449
  return refBuilder.createMockTargetRow();
@@ -2221,15 +2457,8 @@ class ReferenceQueryBuilder {
2221
2457
  return mock;
2222
2458
  }
2223
2459
  else {
2224
- // Fallback: generic proxy
2225
- const handler = {
2226
- get: (target, prop) => ({
2227
- __fieldName: prop,
2228
- __dbColumnName: prop,
2229
- __tableAlias: this.relationName,
2230
- }),
2231
- };
2232
- return new Proxy({}, handler);
2460
+ // Fallback: use the shared nested proxy that supports deep property access
2461
+ return createNestedFieldRefProxy(this.relationName);
2233
2462
  }
2234
2463
  }
2235
2464
  }
@@ -2248,9 +2477,16 @@ class CollectionQueryBuilder {
2248
2477
  this.foreignKey = foreignKey;
2249
2478
  this.sourceTable = sourceTable;
2250
2479
  this.schemaRegistry = schemaRegistry;
2251
- // If targetTableSchema is not provided but we have a registry, look it up
2252
- if (!this.targetTableSchema && this.schemaRegistry) {
2253
- this.targetTableSchema = this.schemaRegistry.get(targetTable);
2480
+ // Prefer registry lookup (has full relations) over passed schema
2481
+ if (this.schemaRegistry) {
2482
+ const registrySchema = this.schemaRegistry.get(targetTable);
2483
+ if (registrySchema) {
2484
+ this.targetTableSchema = registrySchema;
2485
+ }
2486
+ }
2487
+ // Fallback to passed schema if registry lookup failed
2488
+ if (!this.targetTableSchema) {
2489
+ this.targetTableSchema = targetTableSchema;
2254
2490
  }
2255
2491
  }
2256
2492
  /**
@@ -2277,11 +2513,18 @@ class CollectionQueryBuilder {
2277
2513
  }
2278
2514
  /**
2279
2515
  * Filter collection items
2516
+ * Multiple where() calls are chained with AND logic
2280
2517
  */
2281
2518
  where(condition) {
2282
2519
  // Create mock item with proper schema if available
2283
2520
  const mockItem = this.createMockItem();
2284
- this.whereCond = condition(mockItem);
2521
+ const newCondition = condition(mockItem);
2522
+ if (this.whereCond) {
2523
+ this.whereCond = (0, conditions_1.and)(this.whereCond, newCondition);
2524
+ }
2525
+ else {
2526
+ this.whereCond = newCondition;
2527
+ }
2285
2528
  return this;
2286
2529
  }
2287
2530
  /**
@@ -2295,14 +2538,10 @@ class CollectionQueryBuilder {
2295
2538
  if (this.targetTableSchema) {
2296
2539
  // If we have schema information, create a properly typed mock
2297
2540
  const mock = {};
2298
- // Performance: Build column configs once and cache them
2299
- const columnEntries = Object.entries(this.targetTableSchema.columns);
2300
- const columnConfigs = new Map();
2301
- for (const [colName, colBuilder] of columnEntries) {
2302
- columnConfigs.set(colName, colBuilder.build().name);
2303
- }
2541
+ // Performance: Use pre-computed column name map if available
2542
+ const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
2304
2543
  // Add columns
2305
- for (const [colName, dbColumnName] of columnConfigs) {
2544
+ for (const [colName, dbColumnName] of columnNameMap) {
2306
2545
  Object.defineProperty(mock, colName, {
2307
2546
  get: () => ({
2308
2547
  __fieldName: colName,
@@ -2351,14 +2590,8 @@ class CollectionQueryBuilder {
2351
2590
  return mock;
2352
2591
  }
2353
2592
  else {
2354
- // Fallback: generic proxy (don't cache as it's dynamic)
2355
- const handler = {
2356
- get: (target, prop) => ({
2357
- __fieldName: prop,
2358
- __dbColumnName: prop,
2359
- }),
2360
- };
2361
- return new Proxy({}, handler);
2593
+ // Fallback: use the shared nested proxy that supports deep property access
2594
+ return createNestedFieldRefProxy(this.targetTable);
2362
2595
  }
2363
2596
  }
2364
2597
  /**
@@ -2378,35 +2611,7 @@ class CollectionQueryBuilder {
2378
2611
  orderBy(selector) {
2379
2612
  const mockItem = this.createMockItem();
2380
2613
  const result = selector(mockItem);
2381
- // Handle array of [field, direction] tuples
2382
- if (Array.isArray(result) && result.length > 0 && Array.isArray(result[0])) {
2383
- for (const [fieldRef, direction] of result) {
2384
- if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
2385
- this.orderByFields.push({
2386
- field: fieldRef.__dbColumnName || fieldRef.__fieldName,
2387
- direction: direction || 'ASC'
2388
- });
2389
- }
2390
- }
2391
- }
2392
- // Handle array of fields (all ASC)
2393
- else if (Array.isArray(result)) {
2394
- for (const fieldRef of result) {
2395
- if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
2396
- this.orderByFields.push({
2397
- field: fieldRef.__dbColumnName || fieldRef.__fieldName,
2398
- direction: 'ASC'
2399
- });
2400
- }
2401
- }
2402
- }
2403
- // Handle single field
2404
- else if (result && typeof result === 'object' && '__fieldName' in result) {
2405
- this.orderByFields.push({
2406
- field: result.__dbColumnName || result.__fieldName,
2407
- direction: 'ASC'
2408
- });
2409
- }
2614
+ (0, query_utils_1.parseOrderBy)(result, this.orderByFields);
2410
2615
  return this;
2411
2616
  }
2412
2617
  /**
@@ -2524,14 +2729,61 @@ class CollectionQueryBuilder {
2524
2729
  /**
2525
2730
  * Build CTE for this collection query
2526
2731
  * Now delegates to collection strategy pattern
2732
+ * Returns full CollectionAggregationResult for strategies that need special handling (like LATERAL)
2527
2733
  */
2528
2734
  buildCTE(context, client, parentIds) {
2529
- // Determine strategy type - default to 'jsonb' if not specified
2530
- const strategyType = context.collectionStrategy || 'jsonb';
2735
+ // Determine strategy type - default to 'lateral' if not specified
2736
+ const strategyType = context.collectionStrategy || 'lateral';
2531
2737
  const strategy = collection_strategy_factory_1.CollectionStrategyFactory.getStrategy(strategyType);
2532
- // Build selected fields configuration
2738
+ // Build selected fields configuration (supports nested objects)
2533
2739
  const selectedFieldConfigs = [];
2534
2740
  const localParams = [];
2741
+ // Helper function to check if a value is a plain object (not FieldRef, SqlFragment, etc.)
2742
+ const isPlainObject = (val) => {
2743
+ return typeof val === 'object' &&
2744
+ val !== null &&
2745
+ !('__dbColumnName' in val) &&
2746
+ !(val instanceof conditions_1.SqlFragment) &&
2747
+ !Array.isArray(val) &&
2748
+ val.constructor === Object;
2749
+ };
2750
+ // Helper function to recursively process fields and build SelectedField structures
2751
+ const processField = (alias, field) => {
2752
+ if (field instanceof conditions_1.SqlFragment) {
2753
+ // SQL Fragment - build the SQL expression
2754
+ const sqlBuildContext = {
2755
+ paramCounter: context.paramCounter,
2756
+ params: context.allParams,
2757
+ };
2758
+ const fragmentSql = field.buildSql(sqlBuildContext);
2759
+ context.paramCounter = sqlBuildContext.paramCounter;
2760
+ return { alias, expression: fragmentSql };
2761
+ }
2762
+ else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
2763
+ // FieldRef object - use database column name
2764
+ const dbColumnName = field.__dbColumnName;
2765
+ return { alias, expression: `"${dbColumnName}"` };
2766
+ }
2767
+ else if (typeof field === 'string') {
2768
+ // Simple string reference (for backward compatibility)
2769
+ return { alias, expression: `"${field}"` };
2770
+ }
2771
+ else if (isPlainObject(field)) {
2772
+ // Nested object - recursively process its fields
2773
+ const nestedFields = [];
2774
+ for (const [nestedAlias, nestedField] of Object.entries(field)) {
2775
+ nestedFields.push(processField(nestedAlias, nestedField));
2776
+ }
2777
+ return { alias, nested: nestedFields };
2778
+ }
2779
+ else {
2780
+ // Literal value or expression
2781
+ const expression = `$${context.paramCounter++}`;
2782
+ context.allParams.push(field);
2783
+ localParams.push(field);
2784
+ return { alias, expression };
2785
+ }
2786
+ };
2535
2787
  // Step 1: Build field selection configuration
2536
2788
  if (this.selector) {
2537
2789
  const mockItem = this.createMockItem();
@@ -2547,54 +2799,31 @@ class CollectionQueryBuilder {
2547
2799
  });
2548
2800
  }
2549
2801
  else {
2550
- // Object selection - extract each field
2802
+ // Object selection - extract each field (with support for nested objects)
2551
2803
  for (const [alias, field] of Object.entries(selectedFields)) {
2552
- if (field instanceof conditions_1.SqlFragment) {
2553
- // SQL Fragment - build the SQL expression
2554
- const sqlBuildContext = {
2555
- paramCounter: context.paramCounter,
2556
- params: context.allParams,
2557
- };
2558
- const fragmentSql = field.buildSql(sqlBuildContext);
2559
- context.paramCounter = sqlBuildContext.paramCounter;
2560
- selectedFieldConfigs.push({
2561
- alias,
2562
- expression: fragmentSql,
2563
- });
2564
- }
2565
- else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
2566
- // FieldRef object - use database column name
2567
- const dbColumnName = field.__dbColumnName;
2568
- selectedFieldConfigs.push({
2569
- alias,
2570
- expression: `"${dbColumnName}"`,
2571
- });
2572
- }
2573
- else if (typeof field === 'string') {
2574
- // Simple string reference (for backward compatibility)
2575
- selectedFieldConfigs.push({
2576
- alias,
2577
- expression: `"${field}"`,
2578
- });
2579
- }
2580
- else {
2581
- // Literal value or expression
2582
- selectedFieldConfigs.push({
2583
- alias,
2584
- expression: `$${context.paramCounter++}`,
2585
- });
2586
- context.allParams.push(field);
2587
- localParams.push(field);
2588
- }
2804
+ selectedFieldConfigs.push(processField(alias, field));
2589
2805
  }
2590
2806
  }
2591
2807
  }
2592
2808
  else {
2593
- // No selector - select all fields (use * for now, strategy will handle it)
2594
- selectedFieldConfigs.push({
2595
- alias: '*',
2596
- expression: '*',
2597
- });
2809
+ // No selector - select all fields from the target table schema
2810
+ if (this.targetTableSchema && this.targetTableSchema.columns) {
2811
+ // Performance: Use cached column name map
2812
+ const colNameMap = getColumnNameMapForSchema(this.targetTableSchema);
2813
+ for (const [colName, dbColumnName] of colNameMap) {
2814
+ selectedFieldConfigs.push({
2815
+ alias: colName,
2816
+ expression: `"${dbColumnName}"`,
2817
+ });
2818
+ }
2819
+ }
2820
+ else {
2821
+ // Fallback: use * (less ideal, may cause issues)
2822
+ selectedFieldConfigs.push({
2823
+ alias: '*',
2824
+ expression: '*',
2825
+ });
2826
+ }
2598
2827
  }
2599
2828
  // Step 2: Build WHERE clause SQL (without WHERE keyword)
2600
2829
  let whereClause;
@@ -2611,13 +2840,11 @@ class CollectionQueryBuilder {
2611
2840
  // Step 3: Build ORDER BY clause SQL (without ORDER BY keyword)
2612
2841
  let orderByClause;
2613
2842
  if (this.orderByFields.length > 0) {
2843
+ // Performance: Pre-compute column name map for ORDER BY lookups
2844
+ const colNameMap = this.targetTableSchema ? getColumnNameMapForSchema(this.targetTableSchema) : null;
2614
2845
  const orderParts = this.orderByFields.map(({ field, direction }) => {
2615
- // Look up the database column name from the schema if available
2616
- let dbColumnName = field;
2617
- if (this.targetTableSchema && this.targetTableSchema.columns[field]) {
2618
- const colBuilder = this.targetTableSchema.columns[field];
2619
- dbColumnName = colBuilder.build().name;
2620
- }
2846
+ // Look up the database column name from the cached map if available
2847
+ const dbColumnName = colNameMap?.get(field) ?? field;
2621
2848
  return `"${dbColumnName}" ${direction}`;
2622
2849
  });
2623
2850
  orderByClause = orderParts.join(', ');
@@ -2687,10 +2914,14 @@ class CollectionQueryBuilder {
2687
2914
  // Async strategy - return special marker with the promise
2688
2915
  return result;
2689
2916
  }
2690
- // Synchronous strategy (JSONB/CTE)
2917
+ // Synchronous strategy (JSONB/CTE/LATERAL)
2691
2918
  return {
2692
2919
  sql: result.sql,
2693
2920
  params: localParams,
2921
+ isCTE: result.isCTE,
2922
+ joinClause: result.joinClause,
2923
+ selectExpression: result.selectExpression,
2924
+ tableName: result.tableName,
2694
2925
  };
2695
2926
  }
2696
2927
  }