linkgress-orm 0.0.2 → 0.1.0

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 (72) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +196 -196
  3. package/dist/entity/db-column.d.ts +38 -1
  4. package/dist/entity/db-column.d.ts.map +1 -1
  5. package/dist/entity/db-column.js.map +1 -1
  6. package/dist/entity/db-context.d.ts +429 -50
  7. package/dist/entity/db-context.d.ts.map +1 -1
  8. package/dist/entity/db-context.js +884 -203
  9. package/dist/entity/db-context.js.map +1 -1
  10. package/dist/entity/entity-base.d.ts +8 -0
  11. package/dist/entity/entity-base.d.ts.map +1 -1
  12. package/dist/entity/entity-base.js.map +1 -1
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +5 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/migration/db-schema-manager.js +77 -77
  18. package/dist/migration/enum-migrator.js +6 -6
  19. package/dist/query/collection-strategy.factory.d.ts.map +1 -1
  20. package/dist/query/collection-strategy.factory.js +7 -3
  21. package/dist/query/collection-strategy.factory.js.map +1 -1
  22. package/dist/query/collection-strategy.interface.d.ts +12 -6
  23. package/dist/query/collection-strategy.interface.d.ts.map +1 -1
  24. package/dist/query/conditions.d.ts +178 -24
  25. package/dist/query/conditions.d.ts.map +1 -1
  26. package/dist/query/conditions.js +165 -4
  27. package/dist/query/conditions.js.map +1 -1
  28. package/dist/query/cte-builder.d.ts +21 -5
  29. package/dist/query/cte-builder.d.ts.map +1 -1
  30. package/dist/query/cte-builder.js +31 -7
  31. package/dist/query/cte-builder.js.map +1 -1
  32. package/dist/query/grouped-query.d.ts +185 -8
  33. package/dist/query/grouped-query.d.ts.map +1 -1
  34. package/dist/query/grouped-query.js +516 -30
  35. package/dist/query/grouped-query.js.map +1 -1
  36. package/dist/query/join-builder.d.ts +5 -4
  37. package/dist/query/join-builder.d.ts.map +1 -1
  38. package/dist/query/join-builder.js +11 -33
  39. package/dist/query/join-builder.js.map +1 -1
  40. package/dist/query/query-builder.d.ts +89 -20
  41. package/dist/query/query-builder.d.ts.map +1 -1
  42. package/dist/query/query-builder.js +317 -168
  43. package/dist/query/query-builder.js.map +1 -1
  44. package/dist/query/query-utils.d.ts +45 -0
  45. package/dist/query/query-utils.d.ts.map +1 -0
  46. package/dist/query/query-utils.js +103 -0
  47. package/dist/query/query-utils.js.map +1 -0
  48. package/dist/query/sql-utils.d.ts +83 -0
  49. package/dist/query/sql-utils.d.ts.map +1 -0
  50. package/dist/query/sql-utils.js +218 -0
  51. package/dist/query/sql-utils.js.map +1 -0
  52. package/dist/query/strategies/cte-collection-strategy.d.ts +85 -0
  53. package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -0
  54. package/dist/query/strategies/cte-collection-strategy.js +338 -0
  55. package/dist/query/strategies/cte-collection-strategy.js.map +1 -0
  56. package/dist/query/strategies/lateral-collection-strategy.d.ts +59 -0
  57. package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -0
  58. package/dist/query/strategies/lateral-collection-strategy.js +243 -0
  59. package/dist/query/strategies/lateral-collection-strategy.js.map +1 -0
  60. package/dist/query/strategies/temptable-collection-strategy.d.ts +21 -0
  61. package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
  62. package/dist/query/strategies/temptable-collection-strategy.js +216 -94
  63. package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
  64. package/dist/query/subquery.d.ts +24 -1
  65. package/dist/query/subquery.d.ts.map +1 -1
  66. package/dist/query/subquery.js +38 -2
  67. package/dist/query/subquery.js.map +1 -1
  68. package/package.json +1 -1
  69. package/dist/query/strategies/jsonb-collection-strategy.d.ts +0 -51
  70. package/dist/query/strategies/jsonb-collection-strategy.d.ts.map +0 -1
  71. package/dist/query/strategies/jsonb-collection-strategy.js +0 -210
  72. package/dist/query/strategies/jsonb-collection-strategy.js.map +0 -1
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CollectionQueryBuilder = exports.ReferenceQueryBuilder = exports.SelectQueryBuilder = exports.QueryBuilder = void 0;
4
4
  const conditions_1 = require("./conditions");
5
+ const query_utils_1 = require("./query-utils");
5
6
  const subquery_1 = require("./subquery");
6
7
  const grouped_query_1 = require("./grouped-query");
7
8
  const cte_builder_1 = require("./cte-builder");
@@ -38,6 +39,7 @@ class QueryBuilder {
38
39
  }
39
40
  /**
40
41
  * Define the selection with support for nested queries
42
+ * UnwrapSelection extracts the value types from SqlFragment<T> expressions
41
43
  */
42
44
  select(selector) {
43
45
  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 +49,25 @@ class QueryBuilder {
47
49
  }
48
50
  /**
49
51
  * Add WHERE condition
52
+ * Multiple where() calls are chained with AND logic
50
53
  */
51
54
  where(condition) {
52
55
  const mockRow = this.createMockRow();
53
- this.whereCond = condition(mockRow);
56
+ const newCondition = condition(mockRow);
57
+ if (this.whereCond) {
58
+ this.whereCond = (0, conditions_1.and)(this.whereCond, newCondition);
59
+ }
60
+ else {
61
+ this.whereCond = newCondition;
62
+ }
54
63
  return this;
55
64
  }
65
+ /**
66
+ * Add CTEs (Common Table Expressions) to the query
67
+ */
68
+ 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);
70
+ }
56
71
  /**
57
72
  * Create mock row for analysis
58
73
  */
@@ -118,6 +133,7 @@ class QueryBuilder {
118
133
  }
119
134
  /**
120
135
  * Add a LEFT JOIN to the query with a selector (supports both tables and subqueries)
136
+ * UnwrapSelection extracts the value types from SqlFragment<T> expressions
121
137
  */
122
138
  leftJoin(rightTable, condition, selector, alias) {
123
139
  // Check if rightTable is a Subquery
@@ -166,6 +182,7 @@ class QueryBuilder {
166
182
  }
167
183
  /**
168
184
  * Add an INNER JOIN to the query with a selector (supports both tables and subqueries)
185
+ * UnwrapSelection extracts the value types from SqlFragment<T> expressions
169
186
  */
170
187
  innerJoin(rightTable, condition, selector, alias) {
171
188
  // Check if rightTable is a Subquery
@@ -287,35 +304,7 @@ class QueryBuilder {
287
304
  orderBy(selector) {
288
305
  const mockRow = this.createMockRow();
289
306
  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
- }
307
+ (0, query_utils_1.parseOrderBy)(result, this.orderByFields);
319
308
  return this;
320
309
  }
321
310
  }
@@ -353,6 +342,7 @@ class SelectQueryBuilder {
353
342
  }
354
343
  /**
355
344
  * Transform the selection with a new selector
345
+ * UnwrapSelection extracts the value types from SqlFragment<T> expressions
356
346
  */
357
347
  select(selector) {
358
348
  // Create a composed selector that applies both transformations
@@ -364,13 +354,22 @@ class SelectQueryBuilder {
364
354
  }
365
355
  /**
366
356
  * Add WHERE condition
357
+ * Multiple where() calls are chained with AND logic
367
358
  * Note: The row parameter represents the selected shape (after select())
368
359
  */
369
360
  where(condition) {
370
361
  const mockRow = this.createMockRow();
371
362
  // Apply the selector to get the selected shape that the user sees in the WHERE condition
372
363
  const selectedMock = this.selector(mockRow);
373
- this.whereCond = condition(selectedMock);
364
+ // Wrap in proxy - for WHERE, we preserve original column names
365
+ const fieldRefProxy = this.createFieldRefProxy(selectedMock, true);
366
+ const newCondition = condition(fieldRefProxy);
367
+ if (this.whereCond) {
368
+ this.whereCond = (0, conditions_1.and)(this.whereCond, newCondition);
369
+ }
370
+ else {
371
+ this.whereCond = newCondition;
372
+ }
374
373
  return this;
375
374
  }
376
375
  /**
@@ -404,36 +403,12 @@ class SelectQueryBuilder {
404
403
  orderBy(selector) {
405
404
  const mockRow = this.createMockRow();
406
405
  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
- }
406
+ // Wrap selectedMock in a proxy that returns FieldRefs for property access
407
+ const fieldRefProxy = this.createFieldRefProxy(selectedMock);
408
+ const result = selector(fieldRefProxy);
409
+ // Clear previous orderBy - last one takes precedence
410
+ this.orderByFields = [];
411
+ (0, query_utils_1.parseOrderBy)(result, this.orderByFields);
437
412
  return this;
438
413
  }
439
414
  /**
@@ -483,10 +458,7 @@ class SelectQueryBuilder {
483
458
  };
484
459
  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
460
  }
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
- */
461
+ // Implementation
490
462
  leftJoin(rightTable, condition, selector, alias) {
491
463
  // Check if rightTable is a CTE
492
464
  if ((0, cte_builder_1.isCte)(rightTable)) {
@@ -596,6 +568,7 @@ class SelectQueryBuilder {
596
568
  /**
597
569
  * Add an INNER JOIN to the query with a selector
598
570
  * Note: After select(), the left parameter in the join will be the selected shape (TSelection)
571
+ * UnwrapSelection extracts the value types from SqlFragment<T> expressions
599
572
  */
600
573
  innerJoin(rightTable, condition, selector, alias) {
601
574
  // Check if rightTable is a Subquery
@@ -1304,11 +1277,18 @@ class SelectQueryBuilder {
1304
1277
  }
1305
1278
  // Add relations as CollectionQueryBuilder or ReferenceQueryBuilder
1306
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
1281
+ let targetSchema;
1282
+ if (this.schemaRegistry) {
1283
+ targetSchema = this.schemaRegistry.get(relConfig.targetTable);
1284
+ }
1285
+ if (!targetSchema && relConfig.targetTableBuilder) {
1286
+ targetSchema = relConfig.targetTableBuilder.build();
1287
+ }
1307
1288
  if (relConfig.type === 'many') {
1308
1289
  Object.defineProperty(mock, relName, {
1309
1290
  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
1291
+ return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, // Pass the target schema directly
1312
1292
  this.schemaRegistry // Pass schema registry for nested resolution
1313
1293
  );
1314
1294
  },
@@ -1320,8 +1300,7 @@ class SelectQueryBuilder {
1320
1300
  // For single reference (many-to-one), create a ReferenceQueryBuilder
1321
1301
  Object.defineProperty(mock, relName, {
1322
1302
  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
1303
+ const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, // Pass the target schema directly
1325
1304
  this.schemaRegistry // Pass schema registry for nested resolution
1326
1305
  );
1327
1306
  // Return a mock object that exposes the target table's columns
@@ -1334,6 +1313,67 @@ class SelectQueryBuilder {
1334
1313
  }
1335
1314
  return mock;
1336
1315
  }
1316
+ /**
1317
+ * Create a proxy that wraps selected values and returns FieldRefs for property access
1318
+ * This enables orderBy and other operations to work with chained selects
1319
+ * @param preserveOriginal - If true (for WHERE), preserve original column names; if false (for ORDER BY), use alias names
1320
+ */
1321
+ createFieldRefProxy(selectedMock, preserveOriginal = false) {
1322
+ if (!selectedMock || typeof selectedMock !== 'object') {
1323
+ return selectedMock;
1324
+ }
1325
+ // If it already has FieldRef properties, return as-is
1326
+ if ('__fieldName' in selectedMock && '__dbColumnName' in selectedMock) {
1327
+ return selectedMock;
1328
+ }
1329
+ // Create a proxy that returns FieldRefs for each property access
1330
+ return new Proxy(selectedMock, {
1331
+ get: (target, prop) => {
1332
+ if (typeof prop === 'symbol' || prop === 'constructor' || prop === 'then') {
1333
+ return target[prop];
1334
+ }
1335
+ const value = target[prop];
1336
+ // If the value is already a FieldRef
1337
+ if (value && typeof value === 'object' && '__fieldName' in value && '__dbColumnName' in value) {
1338
+ if (preserveOriginal) {
1339
+ // For WHERE: preserve original column name and table alias
1340
+ // This ensures WHERE references the actual database column
1341
+ return {
1342
+ __fieldName: prop,
1343
+ __dbColumnName: value.__dbColumnName,
1344
+ __tableAlias: value.__tableAlias,
1345
+ };
1346
+ }
1347
+ else {
1348
+ // For ORDER BY: use the alias (property name) as the column name
1349
+ // In chained selects, the alias becomes the column name in the subquery
1350
+ return {
1351
+ __fieldName: prop,
1352
+ __dbColumnName: prop,
1353
+ // No table alias - column comes from the selection/subquery
1354
+ };
1355
+ }
1356
+ }
1357
+ // If the value is a SqlFragment, treat it as a FieldRef using the property name as the alias
1358
+ if (value && typeof value === 'object' && value instanceof conditions_1.SqlFragment) {
1359
+ return {
1360
+ __fieldName: prop,
1361
+ __dbColumnName: prop,
1362
+ };
1363
+ }
1364
+ // If the value is an object (nested selection), recursively wrap it
1365
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
1366
+ return this.createFieldRefProxy(value, preserveOriginal);
1367
+ }
1368
+ // For primitive values or arrays, create a FieldRef
1369
+ // Use the property name as both fieldName and dbColumnName
1370
+ return {
1371
+ __fieldName: prop,
1372
+ __dbColumnName: prop,
1373
+ };
1374
+ }
1375
+ });
1376
+ }
1337
1377
  /**
1338
1378
  * Detect navigation property references in selection and add necessary JOINs
1339
1379
  */
@@ -1373,6 +1413,43 @@ class SelectQueryBuilder {
1373
1413
  }
1374
1414
  }
1375
1415
  }
1416
+ /**
1417
+ * Detect navigation property references in a WHERE condition and add necessary JOINs
1418
+ */
1419
+ detectAndAddJoinsFromCondition(condition, joins) {
1420
+ if (!condition) {
1421
+ return;
1422
+ }
1423
+ // Get all field references from the condition
1424
+ const fieldRefs = condition.getFieldRefs();
1425
+ for (const fieldRef of fieldRefs) {
1426
+ if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
1427
+ 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
+ }
1449
+ }
1450
+ }
1451
+ }
1452
+ }
1376
1453
  /**
1377
1454
  * Build SQL query
1378
1455
  */
@@ -1387,6 +1464,8 @@ class SelectQueryBuilder {
1387
1464
  const joins = [];
1388
1465
  // Scan selection for navigation property references and add JOINs
1389
1466
  this.detectAndAddJoinsFromSelection(selection, joins);
1467
+ // Scan WHERE condition for navigation property references and add JOINs
1468
+ this.detectAndAddJoinsFromCondition(this.whereCond, joins);
1390
1469
  // Handle case where selection is a single value (not an object with properties)
1391
1470
  if (selection instanceof conditions_1.SqlFragment) {
1392
1471
  // Single SQL fragment - just build it directly
@@ -1411,11 +1490,19 @@ class SelectQueryBuilder {
1411
1490
  // Process selection object properties
1412
1491
  for (const [key, value] of Object.entries(selection)) {
1413
1492
  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++}`;
1493
+ // Handle collection - delegate to strategy pattern via buildCTE
1494
+ // The strategy handles CTE/LATERAL specifics and returns necessary info
1416
1495
  const cteData = value.buildCTE ? value.buildCTE(context) : value.buildCTE(context);
1417
- context.ctes.set(cteName, cteData);
1418
- collectionFields.push({ name: key, cteName });
1496
+ const isCTE = cteData.isCTE !== false; // Default to CTE if not specified
1497
+ // Note: For CTE strategy, context.ctes is already populated by the strategy
1498
+ // We don't need to add it again - the strategy has already done this
1499
+ collectionFields.push({
1500
+ name: key,
1501
+ cteName: cteData.tableName || `cte_${context.cteCounter - 1}`, // Use tableName from result or infer from counter
1502
+ isCTE,
1503
+ joinClause: cteData.joinClause,
1504
+ selectExpression: cteData.selectExpression,
1505
+ });
1419
1506
  }
1420
1507
  else if (value instanceof subquery_1.Subquery || (value && typeof value === 'object' && 'buildSql' in value && typeof value.buildSql === 'function' && '__mode' in value)) {
1421
1508
  // Handle Subquery - build SQL and wrap in parentheses
@@ -1601,8 +1688,14 @@ class SelectQueryBuilder {
1601
1688
  }
1602
1689
  } // End of for loop
1603
1690
  } // End of else block
1604
- // Add collection fields as JSON/array aggregations joined from CTEs
1605
- for (const { name, cteName } of collectionFields) {
1691
+ // Add collection fields as JSON/array aggregations joined from CTEs or LATERAL joins
1692
+ for (const { name, cteName, selectExpression } of collectionFields) {
1693
+ // If selectExpression is provided (from strategy), use it directly
1694
+ if (selectExpression) {
1695
+ selectParts.push(`${selectExpression} as "${name}"`);
1696
+ continue;
1697
+ }
1698
+ // Fallback to old logic for backward compatibility
1606
1699
  // Check if this is an array aggregation (from toNumberList/toStringList)
1607
1700
  const collectionValue = selection[name];
1608
1701
  const isArrayAgg = collectionValue && typeof collectionValue === 'object' && 'isArrayAggregation' in collectionValue && collectionValue.isArrayAggregation();
@@ -1728,9 +1821,16 @@ class SelectQueryBuilder {
1728
1821
  const joinTableName = this.getQualifiedTableName(join.targetTable, join.targetSchema);
1729
1822
  fromClause += `\n${joinType} ${joinTableName} AS "${join.alias}" ON ${onConditions.join(' AND ')}`;
1730
1823
  }
1731
- // Join CTEs for collections
1732
- for (const { cteName } of collectionFields) {
1733
- fromClause += `\nLEFT JOIN "${cteName}" ON "${cteName}".parent_id = ${qualifiedTableName}.id`;
1824
+ // Join CTEs and LATERAL subqueries for collections
1825
+ for (const { cteName, isCTE, joinClause } of collectionFields) {
1826
+ if (isCTE) {
1827
+ // CTE strategy - join by parent_id
1828
+ fromClause += `\nLEFT JOIN "${cteName}" ON "${cteName}".parent_id = ${qualifiedTableName}.id`;
1829
+ }
1830
+ else if (joinClause) {
1831
+ // LATERAL strategy - use the provided join clause (contains full LATERAL subquery)
1832
+ fromClause += `\n${joinClause}`;
1833
+ }
1734
1834
  }
1735
1835
  // Add DISTINCT if needed
1736
1836
  const distinctClause = this.isDistinct ? 'DISTINCT ' : '';
@@ -2106,7 +2206,35 @@ class SelectQueryBuilder {
2106
2206
  const mockRow = this.createMockRow();
2107
2207
  selectionMetadata = this.selector(mockRow);
2108
2208
  }
2109
- return new subquery_1.Subquery(sqlBuilder, mode, selectionMetadata);
2209
+ // Extract outer field refs from the WHERE condition
2210
+ // These are field refs that reference tables other than this subquery's table
2211
+ // and need to be propagated to the outer query for JOIN detection
2212
+ const outerFieldRefs = this.extractOuterFieldRefs();
2213
+ return new subquery_1.Subquery(sqlBuilder, mode, selectionMetadata, outerFieldRefs);
2214
+ }
2215
+ /**
2216
+ * Extract field refs from the WHERE condition that reference outer queries.
2217
+ * These are field refs with a __tableAlias that doesn't match this query's schema.
2218
+ */
2219
+ extractOuterFieldRefs() {
2220
+ if (!this.whereCond) {
2221
+ return [];
2222
+ }
2223
+ const allRefs = this.whereCond.getFieldRefs();
2224
+ const outerRefs = [];
2225
+ const currentTableName = this.schema.name;
2226
+ for (const ref of allRefs) {
2227
+ // Check if this ref is from an outer query (different table alias)
2228
+ if ('__tableAlias' in ref && ref.__tableAlias) {
2229
+ const tableAlias = ref.__tableAlias;
2230
+ // If the table alias doesn't match our current schema, it's from an outer query
2231
+ // Also check if it's not a navigation property of this table (which would be in schema.relations)
2232
+ if (tableAlias !== currentTableName && !this.schema.relations[tableAlias]) {
2233
+ outerRefs.push(ref);
2234
+ }
2235
+ }
2236
+ }
2237
+ return outerRefs;
2110
2238
  }
2111
2239
  }
2112
2240
  exports.SelectQueryBuilder = SelectQueryBuilder;
@@ -2120,12 +2248,15 @@ class ReferenceQueryBuilder {
2120
2248
  this.foreignKeys = foreignKeys;
2121
2249
  this.matches = matches;
2122
2250
  this.isMandatory = isMandatory;
2123
- this.targetTableSchema = targetTableSchema;
2124
2251
  this.schemaRegistry = schemaRegistry;
2125
- // If targetTableSchema is not provided but we have a registry, look it up
2126
- if (!this.targetTableSchema && this.schemaRegistry) {
2252
+ // Prefer registry lookup (has full relations) over passed schema
2253
+ if (this.schemaRegistry) {
2127
2254
  this.targetTableSchema = this.schemaRegistry.get(targetTable);
2128
2255
  }
2256
+ // Fallback to passed schema if registry lookup failed
2257
+ if (!this.targetTableSchema) {
2258
+ this.targetTableSchema = targetTableSchema;
2259
+ }
2129
2260
  }
2130
2261
  /**
2131
2262
  * Get the alias to use for this reference in the query
@@ -2186,14 +2317,20 @@ class ReferenceQueryBuilder {
2186
2317
  // Add navigation properties (both collections and references)
2187
2318
  if (this.targetTableSchema.relations) {
2188
2319
  for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
2320
+ // Try to get target schema from registry (preferred, has full relations) or targetTableBuilder
2321
+ let nestedTargetSchema;
2322
+ if (this.schemaRegistry) {
2323
+ nestedTargetSchema = this.schemaRegistry.get(relConfig.targetTable);
2324
+ }
2325
+ if (!nestedTargetSchema && relConfig.targetTableBuilder) {
2326
+ nestedTargetSchema = relConfig.targetTableBuilder.build();
2327
+ }
2189
2328
  if (relConfig.type === 'many') {
2190
2329
  // Collection navigation
2191
2330
  Object.defineProperty(mock, relName, {
2192
2331
  get: () => {
2193
- // Don't call build() - it returns schema without relations
2194
- // Instead, pass undefined and let CollectionQueryBuilder look it up from registry
2195
2332
  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
2333
+ return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.targetTable, nestedTargetSchema, // Pass the target schema directly
2197
2334
  this.schemaRegistry // Pass schema registry for nested resolution
2198
2335
  );
2199
2336
  },
@@ -2205,9 +2342,7 @@ class ReferenceQueryBuilder {
2205
2342
  // Reference navigation
2206
2343
  Object.defineProperty(mock, relName, {
2207
2344
  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
2345
+ const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
2211
2346
  this.schemaRegistry // Pass schema registry for nested resolution
2212
2347
  );
2213
2348
  return refBuilder.createMockTargetRow();
@@ -2248,9 +2383,16 @@ class CollectionQueryBuilder {
2248
2383
  this.foreignKey = foreignKey;
2249
2384
  this.sourceTable = sourceTable;
2250
2385
  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);
2386
+ // Prefer registry lookup (has full relations) over passed schema
2387
+ if (this.schemaRegistry) {
2388
+ const registrySchema = this.schemaRegistry.get(targetTable);
2389
+ if (registrySchema) {
2390
+ this.targetTableSchema = registrySchema;
2391
+ }
2392
+ }
2393
+ // Fallback to passed schema if registry lookup failed
2394
+ if (!this.targetTableSchema) {
2395
+ this.targetTableSchema = targetTableSchema;
2254
2396
  }
2255
2397
  }
2256
2398
  /**
@@ -2277,11 +2419,18 @@ class CollectionQueryBuilder {
2277
2419
  }
2278
2420
  /**
2279
2421
  * Filter collection items
2422
+ * Multiple where() calls are chained with AND logic
2280
2423
  */
2281
2424
  where(condition) {
2282
2425
  // Create mock item with proper schema if available
2283
2426
  const mockItem = this.createMockItem();
2284
- this.whereCond = condition(mockItem);
2427
+ const newCondition = condition(mockItem);
2428
+ if (this.whereCond) {
2429
+ this.whereCond = (0, conditions_1.and)(this.whereCond, newCondition);
2430
+ }
2431
+ else {
2432
+ this.whereCond = newCondition;
2433
+ }
2285
2434
  return this;
2286
2435
  }
2287
2436
  /**
@@ -2378,35 +2527,7 @@ class CollectionQueryBuilder {
2378
2527
  orderBy(selector) {
2379
2528
  const mockItem = this.createMockItem();
2380
2529
  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
- }
2530
+ (0, query_utils_1.parseOrderBy)(result, this.orderByFields);
2410
2531
  return this;
2411
2532
  }
2412
2533
  /**
@@ -2524,14 +2645,61 @@ class CollectionQueryBuilder {
2524
2645
  /**
2525
2646
  * Build CTE for this collection query
2526
2647
  * Now delegates to collection strategy pattern
2648
+ * Returns full CollectionAggregationResult for strategies that need special handling (like LATERAL)
2527
2649
  */
2528
2650
  buildCTE(context, client, parentIds) {
2529
- // Determine strategy type - default to 'jsonb' if not specified
2530
- const strategyType = context.collectionStrategy || 'jsonb';
2651
+ // Determine strategy type - default to 'lateral' if not specified
2652
+ const strategyType = context.collectionStrategy || 'lateral';
2531
2653
  const strategy = collection_strategy_factory_1.CollectionStrategyFactory.getStrategy(strategyType);
2532
- // Build selected fields configuration
2654
+ // Build selected fields configuration (supports nested objects)
2533
2655
  const selectedFieldConfigs = [];
2534
2656
  const localParams = [];
2657
+ // Helper function to check if a value is a plain object (not FieldRef, SqlFragment, etc.)
2658
+ const isPlainObject = (val) => {
2659
+ return typeof val === 'object' &&
2660
+ val !== null &&
2661
+ !('__dbColumnName' in val) &&
2662
+ !(val instanceof conditions_1.SqlFragment) &&
2663
+ !Array.isArray(val) &&
2664
+ val.constructor === Object;
2665
+ };
2666
+ // Helper function to recursively process fields and build SelectedField structures
2667
+ const processField = (alias, field) => {
2668
+ if (field instanceof conditions_1.SqlFragment) {
2669
+ // SQL Fragment - build the SQL expression
2670
+ const sqlBuildContext = {
2671
+ paramCounter: context.paramCounter,
2672
+ params: context.allParams,
2673
+ };
2674
+ const fragmentSql = field.buildSql(sqlBuildContext);
2675
+ context.paramCounter = sqlBuildContext.paramCounter;
2676
+ return { alias, expression: fragmentSql };
2677
+ }
2678
+ else if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
2679
+ // FieldRef object - use database column name
2680
+ const dbColumnName = field.__dbColumnName;
2681
+ return { alias, expression: `"${dbColumnName}"` };
2682
+ }
2683
+ else if (typeof field === 'string') {
2684
+ // Simple string reference (for backward compatibility)
2685
+ return { alias, expression: `"${field}"` };
2686
+ }
2687
+ else if (isPlainObject(field)) {
2688
+ // Nested object - recursively process its fields
2689
+ const nestedFields = [];
2690
+ for (const [nestedAlias, nestedField] of Object.entries(field)) {
2691
+ nestedFields.push(processField(nestedAlias, nestedField));
2692
+ }
2693
+ return { alias, nested: nestedFields };
2694
+ }
2695
+ else {
2696
+ // Literal value or expression
2697
+ const expression = `$${context.paramCounter++}`;
2698
+ context.allParams.push(field);
2699
+ localParams.push(field);
2700
+ return { alias, expression };
2701
+ }
2702
+ };
2535
2703
  // Step 1: Build field selection configuration
2536
2704
  if (this.selector) {
2537
2705
  const mockItem = this.createMockItem();
@@ -2547,54 +2715,31 @@ class CollectionQueryBuilder {
2547
2715
  });
2548
2716
  }
2549
2717
  else {
2550
- // Object selection - extract each field
2718
+ // Object selection - extract each field (with support for nested objects)
2551
2719
  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
- }
2720
+ selectedFieldConfigs.push(processField(alias, field));
2589
2721
  }
2590
2722
  }
2591
2723
  }
2592
2724
  else {
2593
- // No selector - select all fields (use * for now, strategy will handle it)
2594
- selectedFieldConfigs.push({
2595
- alias: '*',
2596
- expression: '*',
2597
- });
2725
+ // No selector - select all fields from the target table schema
2726
+ 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;
2730
+ selectedFieldConfigs.push({
2731
+ alias: colName,
2732
+ expression: `"${dbColumnName}"`,
2733
+ });
2734
+ }
2735
+ }
2736
+ else {
2737
+ // Fallback: use * (less ideal, may cause issues)
2738
+ selectedFieldConfigs.push({
2739
+ alias: '*',
2740
+ expression: '*',
2741
+ });
2742
+ }
2598
2743
  }
2599
2744
  // Step 2: Build WHERE clause SQL (without WHERE keyword)
2600
2745
  let whereClause;
@@ -2687,10 +2832,14 @@ class CollectionQueryBuilder {
2687
2832
  // Async strategy - return special marker with the promise
2688
2833
  return result;
2689
2834
  }
2690
- // Synchronous strategy (JSONB/CTE)
2835
+ // Synchronous strategy (JSONB/CTE/LATERAL)
2691
2836
  return {
2692
2837
  sql: result.sql,
2693
2838
  params: localParams,
2839
+ isCTE: result.isCTE,
2840
+ joinClause: result.joinClause,
2841
+ selectExpression: result.selectExpression,
2842
+ tableName: result.tableName,
2694
2843
  };
2695
2844
  }
2696
2845
  }