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,8 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GroupedSelectQueryBuilder = exports.GroupedQueryBuilder = void 0;
3
+ exports.GroupedJoinedQueryBuilder = exports.GroupedSelectQueryBuilder = exports.GroupedQueryBuilder = void 0;
4
4
  const conditions_1 = require("./conditions");
5
+ const query_utils_1 = require("./query-utils");
5
6
  const subquery_1 = require("./subquery");
7
+ const query_builder_1 = require("./query-builder");
8
+ const cte_builder_1 = require("./cte-builder");
6
9
  /**
7
10
  * Create an aggregate field reference that can be used in conditions
8
11
  */
@@ -80,9 +83,9 @@ class GroupedQueryBuilder {
80
83
  */
81
84
  createMockRow() {
82
85
  const mock = {};
83
- // Add columns as FieldRef objects
84
- for (const [colName, colBuilder] of Object.entries(this.schema.columns)) {
85
- const dbColumnName = colBuilder.build().name;
86
+ // Add columns as FieldRef objects - use pre-computed column name map if available
87
+ const columnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(this.schema);
88
+ for (const [colName, dbColumnName] of columnNameMap) {
86
89
  Object.defineProperty(mock, colName, {
87
90
  get: () => ({
88
91
  __fieldName: colName,
@@ -93,13 +96,38 @@ class GroupedQueryBuilder {
93
96
  configurable: true,
94
97
  });
95
98
  }
99
+ // Add navigation properties (collections and single references)
100
+ // Performance: Use pre-computed relation entries and cached schemas
101
+ const relationEntries = (0, query_builder_1.getRelationEntriesForSchema)(this.schema);
102
+ for (const [relName, relConfig] of relationEntries) {
103
+ const targetSchema = (0, query_builder_1.getTargetSchemaForRelation)(this.schema, relName, relConfig);
104
+ if (relConfig.type === 'many') {
105
+ Object.defineProperty(mock, relName, {
106
+ get: () => {
107
+ return new query_builder_1.CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema);
108
+ },
109
+ enumerable: true,
110
+ configurable: true,
111
+ });
112
+ }
113
+ else {
114
+ Object.defineProperty(mock, relName, {
115
+ get: () => {
116
+ const refBuilder = new query_builder_1.ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
117
+ return refBuilder.createMockTargetRow();
118
+ },
119
+ enumerable: true,
120
+ configurable: true,
121
+ });
122
+ }
123
+ }
96
124
  // Add columns from manually joined tables
97
125
  for (const join of this.manualJoins) {
98
126
  if (join.isSubquery || !join.schema) {
99
127
  continue;
100
128
  }
101
- for (const [colName, colBuilder] of Object.entries(join.schema.columns)) {
102
- const dbColumnName = colBuilder.build().name;
129
+ const joinColumnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(join.schema);
130
+ for (const [colName, dbColumnName] of joinColumnNameMap) {
103
131
  if (!mock[join.alias]) {
104
132
  mock[join.alias] = {};
105
133
  }
@@ -166,35 +194,7 @@ class GroupedSelectQueryBuilder {
166
194
  const mockGroup = this.createMockGroupedItem();
167
195
  const mockResult = this.resultSelector(mockGroup);
168
196
  const result = selector(mockResult);
169
- // Handle array of [field, direction] tuples
170
- if (Array.isArray(result) && result.length > 0 && Array.isArray(result[0])) {
171
- for (const [fieldRef, direction] of result) {
172
- if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
173
- this.orderByFields.push({
174
- field: fieldRef.__dbColumnName || fieldRef.__fieldName,
175
- direction: direction || 'ASC'
176
- });
177
- }
178
- }
179
- }
180
- // Handle array of fields (all ASC)
181
- else if (Array.isArray(result)) {
182
- for (const fieldRef of result) {
183
- if (fieldRef && typeof fieldRef === 'object' && '__fieldName' in fieldRef) {
184
- this.orderByFields.push({
185
- field: fieldRef.__dbColumnName || fieldRef.__fieldName,
186
- direction: 'ASC'
187
- });
188
- }
189
- }
190
- }
191
- // Handle single field
192
- else if (result && typeof result === 'object' && '__fieldName' in result) {
193
- this.orderByFields.push({
194
- field: result.__dbColumnName || result.__fieldName,
195
- direction: 'ASC'
196
- });
197
- }
197
+ (0, query_utils_1.parseOrderBy)(result, this.orderByFields);
198
198
  return this;
199
199
  }
200
200
  /**
@@ -279,6 +279,167 @@ class GroupedSelectQueryBuilder {
279
279
  };
280
280
  return new subquery_1.Subquery(sqlBuilder, mode);
281
281
  }
282
+ /**
283
+ * Build SQL for use in CTEs - public interface for CTE builder
284
+ * @internal
285
+ */
286
+ buildCteQuery(queryContext) {
287
+ return this.buildQuery(queryContext);
288
+ }
289
+ /**
290
+ * Add a LEFT JOIN to the grouped query result
291
+ * This wraps the grouped query as a subquery and joins to it
292
+ *
293
+ * @example
294
+ * const result = await db.orders
295
+ * .select(o => ({ customerId: o.customerId, total: o.total }))
296
+ * .groupBy(o => ({ customerId: o.customerId }))
297
+ * .select(g => ({ customerId: g.key.customerId, totalSum: g.sum(o => o.total) }))
298
+ * .leftJoin(
299
+ * customerDetailsCte,
300
+ * (grouped, details) => eq(grouped.customerId, details.customerId),
301
+ * (grouped, details) => ({ ...grouped, details: details.items })
302
+ * )
303
+ * .toList();
304
+ */
305
+ leftJoin(rightSource, condition, selector, alias) {
306
+ return this.joinInternal('LEFT', rightSource, condition, selector, alias);
307
+ }
308
+ /**
309
+ * Add an INNER JOIN to the grouped query result
310
+ * This wraps the grouped query as a subquery and joins to it
311
+ */
312
+ innerJoin(rightSource, condition, selector, alias) {
313
+ return this.joinInternal('INNER', rightSource, condition, selector, alias);
314
+ }
315
+ /**
316
+ * Internal join implementation
317
+ */
318
+ joinInternal(joinType, rightSource, condition, selector, alias) {
319
+ // Wrap this grouped query as a subquery
320
+ const leftSubquery = this.asSubquery('table');
321
+ const leftAlias = 'grouped_0';
322
+ // Determine the right alias and source info
323
+ let rightAlias;
324
+ let isCteJoin = false;
325
+ let cte;
326
+ if ((0, cte_builder_1.isCte)(rightSource)) {
327
+ rightAlias = rightSource.name;
328
+ isCteJoin = true;
329
+ cte = rightSource;
330
+ }
331
+ else {
332
+ if (!alias) {
333
+ throw new Error('Alias is required when joining a subquery');
334
+ }
335
+ rightAlias = alias;
336
+ }
337
+ // Create mock for left (the grouped query result)
338
+ const mockLeft = this.createMockForSelection(leftAlias);
339
+ // Create mock for right - at runtime these are already FieldRef-like objects
340
+ const mockRight = (isCteJoin
341
+ ? this.createMockForCte(cte)
342
+ : this.createMockForSubquery(rightAlias, rightSource));
343
+ // Evaluate the join condition
344
+ const joinCondition = condition(mockLeft, mockRight);
345
+ // Create the result selector
346
+ const createLeftMock = () => this.createMockForSelection(leftAlias);
347
+ const createRightMock = () => (isCteJoin
348
+ ? this.createMockForCte(cte)
349
+ : this.createMockForSubquery(rightAlias, rightSource));
350
+ return new GroupedJoinedQueryBuilder(this.schema, this.client, leftSubquery, leftAlias, rightSource, rightAlias, joinType, joinCondition, selector, createLeftMock, createRightMock, this.executor, isCteJoin ? cte : undefined);
351
+ }
352
+ /**
353
+ * Create a mock object for the current selection (for join conditions)
354
+ * The key is the alias used in the SELECT clause, so we use it as __dbColumnName
355
+ */
356
+ createMockForSelection(alias) {
357
+ const mockGroup = this.createMockGroupedItem();
358
+ const mockResult = this.resultSelector(mockGroup);
359
+ // Wrap with alias - always use the key as the column name since
360
+ // that's what the subquery SELECT clause uses as the alias
361
+ const wrapped = {};
362
+ for (const [key, value] of Object.entries(mockResult)) {
363
+ // Preserve mapper if present
364
+ const mapper = (typeof value === 'object' && value !== null && typeof value.getMapper === 'function')
365
+ ? { getMapper: () => value.getMapper() }
366
+ : {};
367
+ wrapped[key] = {
368
+ __fieldName: key,
369
+ __dbColumnName: key, // Use key as column name (the subquery alias)
370
+ __tableAlias: alias,
371
+ ...mapper,
372
+ };
373
+ }
374
+ return wrapped;
375
+ }
376
+ /**
377
+ * Create a mock for a subquery result
378
+ */
379
+ createMockForSubquery(alias, subquery) {
380
+ const selectionMetadata = subquery.getSelectionMetadata();
381
+ return new Proxy({}, {
382
+ get(target, prop) {
383
+ if (typeof prop === 'symbol')
384
+ return undefined;
385
+ if (selectionMetadata && prop in selectionMetadata) {
386
+ const value = selectionMetadata[prop];
387
+ if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
388
+ return {
389
+ __fieldName: prop,
390
+ __dbColumnName: prop,
391
+ __tableAlias: alias,
392
+ getMapper: () => value.getMapper(),
393
+ };
394
+ }
395
+ }
396
+ return {
397
+ __fieldName: prop,
398
+ __dbColumnName: prop,
399
+ __tableAlias: alias,
400
+ };
401
+ },
402
+ has() { return true; },
403
+ ownKeys() { return []; },
404
+ getOwnPropertyDescriptor() {
405
+ return { enumerable: true, configurable: true };
406
+ }
407
+ });
408
+ }
409
+ /**
410
+ * Create a mock for a CTE
411
+ */
412
+ createMockForCte(cte) {
413
+ const alias = cte.name;
414
+ const selectionMetadata = cte.selectionMetadata;
415
+ return new Proxy({}, {
416
+ get(target, prop) {
417
+ if (typeof prop === 'symbol')
418
+ return undefined;
419
+ if (selectionMetadata && prop in selectionMetadata) {
420
+ const value = selectionMetadata[prop];
421
+ if (typeof value === 'object' && value !== null && typeof value.getMapper === 'function') {
422
+ return {
423
+ __fieldName: prop,
424
+ __dbColumnName: prop,
425
+ __tableAlias: alias,
426
+ getMapper: () => value.getMapper(),
427
+ };
428
+ }
429
+ }
430
+ return {
431
+ __fieldName: prop,
432
+ __dbColumnName: prop,
433
+ __tableAlias: alias,
434
+ };
435
+ },
436
+ has() { return true; },
437
+ ownKeys() { return []; },
438
+ getOwnPropertyDescriptor() {
439
+ return { enumerable: true, configurable: true };
440
+ }
441
+ });
442
+ }
282
443
  /**
283
444
  * Build the SQL query for grouped results
284
445
  */
@@ -287,12 +448,34 @@ class GroupedSelectQueryBuilder {
287
448
  const mockRow = this.createMockRow();
288
449
  const mockOriginalSelection = this.originalSelector(mockRow);
289
450
  const mockGroupingKey = this.groupingKeySelector(mockOriginalSelection);
290
- const mockGroup = this.createMockGroupedItem();
451
+ // Create mock grouped item using the SAME grouping key (not a fresh one)
452
+ // This ensures SqlFragment instances are shared between GROUP BY and SELECT
453
+ const mockGroup = {
454
+ key: mockGroupingKey,
455
+ count: () => createAggregateFieldRef('COUNT'),
456
+ sum: (selector) => createAggregateFieldRef('SUM', selector),
457
+ min: (selector) => createAggregateFieldRef('MIN', selector),
458
+ max: (selector) => createAggregateFieldRef('MAX', selector),
459
+ avg: (selector) => createAggregateFieldRef('AVG', selector),
460
+ };
291
461
  const mockResult = this.resultSelector(mockGroup);
292
462
  // Extract GROUP BY fields from the grouping key
463
+ // We build these first and cache the SQL for SqlFragments so they can be reused in SELECT
293
464
  const groupByFields = [];
465
+ const sqlFragmentCache = new Map(); // Cache built SQL for reuse in SELECT
294
466
  for (const [key, value] of Object.entries(mockGroupingKey)) {
295
- if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
467
+ if (value instanceof conditions_1.SqlFragment) {
468
+ // SqlFragment in GROUP BY - build the SQL expression and cache it
469
+ const sqlBuildContext = {
470
+ paramCounter: context.paramCounter,
471
+ params: context.allParams,
472
+ };
473
+ const fragmentSql = value.buildSql(sqlBuildContext);
474
+ context.paramCounter = sqlBuildContext.paramCounter;
475
+ groupByFields.push(fragmentSql);
476
+ sqlFragmentCache.set(value, fragmentSql);
477
+ }
478
+ else if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
296
479
  const field = value;
297
480
  const tableAlias = field.__tableAlias || this.schema.name;
298
481
  groupByFields.push(`"${tableAlias}"."${field.__dbColumnName}"`);
@@ -311,8 +494,9 @@ class GroupedSelectQueryBuilder {
311
494
  }
312
495
  else if (aggField.__aggregateSelector) {
313
496
  // SUM, MIN, MAX, AVG with selector
314
- const mockOriginalRow = this.createMockRow();
315
- const field = aggField.__aggregateSelector(mockOriginalRow);
497
+ // Note: The selector references fields from the ORIGINAL SELECTION (after first .select()),
498
+ // not the raw table row. So we need to use mockOriginalSelection, not a fresh mock row.
499
+ const field = aggField.__aggregateSelector(mockOriginalSelection);
316
500
  if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
317
501
  const fieldRef = field;
318
502
  const tableAlias = fieldRef.__tableAlias || this.schema.name;
@@ -339,8 +523,8 @@ class GroupedSelectQueryBuilder {
339
523
  selectParts.push(`CAST(COUNT(*) AS INTEGER) as "${alias}"`);
340
524
  }
341
525
  else if (agg.__selector) {
342
- const mockOriginalRow = this.createMockRow();
343
- const field = agg.__selector(mockOriginalRow);
526
+ // Use mockOriginalSelection - the selector references fields from the first .select()
527
+ const field = agg.__selector(mockOriginalSelection);
344
528
  if (typeof field === 'object' && field !== null && '__dbColumnName' in field) {
345
529
  const fieldRef = field;
346
530
  const tableAlias = fieldRef.__tableAlias || this.schema.name;
@@ -360,18 +544,45 @@ class GroupedSelectQueryBuilder {
360
544
  selectParts.push(`"${tableAlias}"."${field.__dbColumnName}" as "${alias}"`);
361
545
  }
362
546
  else if (value instanceof conditions_1.SqlFragment) {
363
- // SQL fragment
364
- const sqlBuildContext = {
365
- paramCounter: context.paramCounter,
366
- params: context.allParams,
367
- };
368
- const fragmentSql = value.buildSql(sqlBuildContext);
369
- context.paramCounter = sqlBuildContext.paramCounter;
370
- selectParts.push(`${fragmentSql} as "${alias}"`);
547
+ // SQL fragment - check if we already built this for GROUP BY
548
+ const cachedSql = sqlFragmentCache.get(value);
549
+ if (cachedSql) {
550
+ // Reuse the cached SQL to ensure same parameter numbers
551
+ selectParts.push(`${cachedSql} as "${alias}"`);
552
+ }
553
+ else {
554
+ // Build new SQL for this fragment
555
+ const sqlBuildContext = {
556
+ paramCounter: context.paramCounter,
557
+ params: context.allParams,
558
+ };
559
+ const fragmentSql = value.buildSql(sqlBuildContext);
560
+ context.paramCounter = sqlBuildContext.paramCounter;
561
+ selectParts.push(`${fragmentSql} as "${alias}"`);
562
+ }
371
563
  }
372
564
  }
565
+ // Detect navigation property references in WHERE and add JOINs
566
+ const navigationJoins = [];
567
+ // Detect joins from the original selection (navigation properties used in select)
568
+ this.detectAndAddJoinsFromSelection(mockOriginalSelection, navigationJoins);
569
+ // Detect joins from WHERE condition
570
+ this.detectAndAddJoinsFromCondition(this.whereCond, navigationJoins);
373
571
  // Build FROM clause with JOINs
374
572
  let fromClause = `FROM "${this.schema.name}"`;
573
+ // Add navigation property JOINs first
574
+ for (const navJoin of navigationJoins) {
575
+ const joinType = navJoin.isMandatory ? 'INNER JOIN' : 'LEFT JOIN';
576
+ const targetTableName = navJoin.targetSchema
577
+ ? `"${navJoin.targetSchema}"."${navJoin.targetTable}"`
578
+ : `"${navJoin.targetTable}"`;
579
+ // Build join condition: source.foreignKey = target.match
580
+ const joinConditions = navJoin.foreignKeys.map((fk, i) => {
581
+ const targetCol = navJoin.matches[i] || 'id';
582
+ return `"${this.schema.name}"."${fk}" = "${navJoin.alias}"."${targetCol}"`;
583
+ });
584
+ fromClause += `\n${joinType} ${targetTableName} AS "${navJoin.alias}" ON ${joinConditions.join(' AND ')}`;
585
+ }
375
586
  // Add manual JOINs
376
587
  for (const manualJoin of this.manualJoins) {
377
588
  const joinTypeStr = manualJoin.type === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
@@ -436,9 +647,9 @@ class GroupedSelectQueryBuilder {
436
647
  */
437
648
  createMockRow() {
438
649
  const mock = {};
439
- // Add columns as FieldRef objects
440
- for (const [colName, colBuilder] of Object.entries(this.schema.columns)) {
441
- const dbColumnName = colBuilder.build().name;
650
+ // Add columns as FieldRef objects - use pre-computed column name map if available
651
+ const columnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(this.schema);
652
+ for (const [colName, dbColumnName] of columnNameMap) {
442
653
  Object.defineProperty(mock, colName, {
443
654
  get: () => ({
444
655
  __fieldName: colName,
@@ -449,13 +660,38 @@ class GroupedSelectQueryBuilder {
449
660
  configurable: true,
450
661
  });
451
662
  }
663
+ // Add navigation properties (collections and single references)
664
+ // Performance: Use pre-computed relation entries and cached schemas
665
+ const relationEntries = (0, query_builder_1.getRelationEntriesForSchema)(this.schema);
666
+ for (const [relName, relConfig] of relationEntries) {
667
+ const targetSchema = (0, query_builder_1.getTargetSchemaForRelation)(this.schema, relName, relConfig);
668
+ if (relConfig.type === 'many') {
669
+ Object.defineProperty(mock, relName, {
670
+ get: () => {
671
+ return new query_builder_1.CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema);
672
+ },
673
+ enumerable: true,
674
+ configurable: true,
675
+ });
676
+ }
677
+ else {
678
+ Object.defineProperty(mock, relName, {
679
+ get: () => {
680
+ const refBuilder = new query_builder_1.ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
681
+ return refBuilder.createMockTargetRow();
682
+ },
683
+ enumerable: true,
684
+ configurable: true,
685
+ });
686
+ }
687
+ }
452
688
  // Add columns from manually joined tables
453
689
  for (const join of this.manualJoins) {
454
690
  if (join.isSubquery || !join.schema) {
455
691
  continue;
456
692
  }
457
- for (const [colName, colBuilder] of Object.entries(join.schema.columns)) {
458
- const dbColumnName = colBuilder.build().name;
693
+ const joinColumnNameMap = (0, query_builder_1.getColumnNameMapForSchema)(join.schema);
694
+ for (const [colName, dbColumnName] of joinColumnNameMap) {
459
695
  if (!mock[join.alias]) {
460
696
  mock[join.alias] = {};
461
697
  }
@@ -498,6 +734,98 @@ class GroupedSelectQueryBuilder {
498
734
  },
499
735
  };
500
736
  }
737
+ /**
738
+ * Detect navigation property references in a WHERE condition and add necessary JOINs
739
+ */
740
+ detectAndAddJoinsFromCondition(condition, joins) {
741
+ if (!condition) {
742
+ return;
743
+ }
744
+ // Get all field references from the condition
745
+ const fieldRefs = condition.getFieldRefs();
746
+ for (const fieldRef of fieldRefs) {
747
+ if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
748
+ const tableAlias = fieldRef.__tableAlias;
749
+ // Check if this references a related table that isn't already joined
750
+ if (tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
751
+ // Find the relation config for this navigation
752
+ const relation = this.schema.relations[tableAlias];
753
+ if (relation && relation.type === 'one') {
754
+ // Get target schema from targetTableBuilder if available
755
+ let targetSchema;
756
+ if (relation.targetTableBuilder) {
757
+ const targetTableSchema = relation.targetTableBuilder.build();
758
+ targetSchema = targetTableSchema.schema;
759
+ }
760
+ // Add a JOIN for this reference
761
+ joins.push({
762
+ alias: tableAlias,
763
+ targetTable: relation.targetTable,
764
+ targetSchema,
765
+ foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
766
+ matches: relation.matches || [],
767
+ isMandatory: relation.isMandatory ?? false,
768
+ });
769
+ }
770
+ }
771
+ }
772
+ }
773
+ }
774
+ /**
775
+ * Detect navigation properties in a selection and add JOINs for them
776
+ */
777
+ detectAndAddJoinsFromSelection(selection, joins) {
778
+ if (!selection || typeof selection !== 'object') {
779
+ return;
780
+ }
781
+ for (const [, value] of Object.entries(selection)) {
782
+ if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
783
+ // This is a FieldRef with a table alias - check if it's from a related table
784
+ this.addJoinForFieldRef(value, joins);
785
+ }
786
+ else if (value instanceof conditions_1.SqlFragment) {
787
+ // SqlFragment may contain navigation property references - extract them
788
+ const fieldRefs = value.getFieldRefs();
789
+ for (const fieldRef of fieldRefs) {
790
+ this.addJoinForFieldRef(fieldRef, joins);
791
+ }
792
+ }
793
+ else if (value && typeof value === 'object' && !Array.isArray(value)) {
794
+ // Recursively check nested objects
795
+ this.detectAndAddJoinsFromSelection(value, joins);
796
+ }
797
+ }
798
+ }
799
+ /**
800
+ * Add a JOIN for a FieldRef if it references a related table
801
+ */
802
+ addJoinForFieldRef(fieldRef, joins) {
803
+ if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef) || !('__dbColumnName' in fieldRef)) {
804
+ return;
805
+ }
806
+ const tableAlias = fieldRef.__tableAlias;
807
+ if (tableAlias && tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
808
+ // This references a related table - find the relation and add a JOIN
809
+ const relation = this.schema.relations[tableAlias];
810
+ if (relation && relation.type === 'one') {
811
+ // Get target schema from targetTableBuilder if available
812
+ let targetSchema;
813
+ if (relation.targetTableBuilder) {
814
+ const targetTableSchema = relation.targetTableBuilder.build();
815
+ targetSchema = targetTableSchema.schema;
816
+ }
817
+ // Add a JOIN for this reference
818
+ joins.push({
819
+ alias: tableAlias,
820
+ targetTable: relation.targetTable,
821
+ targetSchema,
822
+ foreignKeys: relation.foreignKeys || [relation.foreignKey || ''],
823
+ matches: relation.matches || [],
824
+ isMandatory: relation.isMandatory ?? false,
825
+ });
826
+ }
827
+ }
828
+ }
501
829
  /**
502
830
  * Build HAVING condition SQL - handles aggregate field refs specially
503
831
  */
@@ -585,4 +913,208 @@ class GroupedSelectQueryBuilder {
585
913
  }
586
914
  }
587
915
  exports.GroupedSelectQueryBuilder = GroupedSelectQueryBuilder;
916
+ /**
917
+ * Query builder for grouped queries that have been joined
918
+ * This handles the case where a GroupedSelectQueryBuilder is joined with a CTE or subquery
919
+ */
920
+ class GroupedJoinedQueryBuilder {
921
+ constructor(schema, client, leftSubquery, leftAlias, rightSource, rightAlias, joinType, joinCondition, resultSelector, createLeftMock, createRightMock, executor, cte) {
922
+ this.orderByFields = [];
923
+ this.additionalJoins = [];
924
+ this.schema = schema;
925
+ this.client = client;
926
+ this.leftSubquery = leftSubquery;
927
+ this.leftAlias = leftAlias;
928
+ this.rightSource = rightSource;
929
+ this.rightAlias = rightAlias;
930
+ this.joinType = joinType;
931
+ this.joinCondition = joinCondition;
932
+ this.resultSelector = resultSelector;
933
+ this.createLeftMock = createLeftMock;
934
+ this.createRightMock = createRightMock;
935
+ this.executor = executor;
936
+ this.cte = cte;
937
+ }
938
+ /**
939
+ * Limit results
940
+ */
941
+ limit(count) {
942
+ this.limitValue = count;
943
+ return this;
944
+ }
945
+ /**
946
+ * Offset results
947
+ */
948
+ offset(count) {
949
+ this.offsetValue = count;
950
+ return this;
951
+ }
952
+ orderBy(selector) {
953
+ const mockLeft = this.createLeftMock();
954
+ const mockRight = this.createRightMock();
955
+ const mockResult = this.resultSelector(mockLeft, mockRight);
956
+ const result = selector(mockResult);
957
+ (0, query_utils_1.parseOrderBy)(result, this.orderByFields, query_utils_1.getQualifiedFieldName);
958
+ return this;
959
+ }
960
+ /**
961
+ * Execute query and return results
962
+ */
963
+ async toList() {
964
+ const context = {
965
+ ctes: new Map(),
966
+ cteCounter: 0,
967
+ paramCounter: 1,
968
+ allParams: [],
969
+ };
970
+ const { sql, params } = this.buildQuery(context);
971
+ const result = this.executor
972
+ ? await this.executor.query(sql, params)
973
+ : await this.client.query(sql, params);
974
+ return result.rows;
975
+ }
976
+ /**
977
+ * Execute query and return first result or null
978
+ */
979
+ async first() {
980
+ const results = await this.limit(1).toList();
981
+ return results.length > 0 ? results[0] : null;
982
+ }
983
+ /**
984
+ * Execute query and return first result or throw
985
+ */
986
+ async firstOrThrow() {
987
+ const result = await this.first();
988
+ if (!result) {
989
+ throw new Error('No results found');
990
+ }
991
+ return result;
992
+ }
993
+ /**
994
+ * Convert to subquery for use in other queries
995
+ */
996
+ asSubquery(mode = 'table') {
997
+ const sqlBuilder = (outerContext) => {
998
+ const context = {
999
+ ctes: new Map(),
1000
+ cteCounter: 0,
1001
+ paramCounter: outerContext.paramCounter,
1002
+ allParams: outerContext.params,
1003
+ };
1004
+ const { sql } = this.buildQuery(context);
1005
+ outerContext.paramCounter = context.paramCounter;
1006
+ return sql;
1007
+ };
1008
+ // Preserve selection metadata for mappers
1009
+ const mockLeft = this.createLeftMock();
1010
+ const mockRight = this.createRightMock();
1011
+ const selectionMetadata = this.resultSelector(mockLeft, mockRight);
1012
+ return new subquery_1.Subquery(sqlBuilder, mode, selectionMetadata);
1013
+ }
1014
+ /**
1015
+ * Get CTEs used by this query builder
1016
+ * @internal
1017
+ */
1018
+ getReferencedCtes() {
1019
+ return this.cte ? [this.cte] : [];
1020
+ }
1021
+ /**
1022
+ * Build SQL for use in CTEs - public interface for CTE builder
1023
+ * This returns SQL WITHOUT the WITH clause - CTEs should be extracted separately via getReferencedCtes()
1024
+ * @internal
1025
+ */
1026
+ buildCteQuery(queryContext) {
1027
+ return this.buildQuery(queryContext, true);
1028
+ }
1029
+ /**
1030
+ * Build the SQL query
1031
+ * @param skipCteClause If true, don't include WITH clause (for embedding in outer CTEs)
1032
+ */
1033
+ buildQuery(context, skipCteClause = false) {
1034
+ // Build CTE clause if needed (unless we're being embedded in another CTE)
1035
+ let cteClause = '';
1036
+ if (this.cte && !skipCteClause) {
1037
+ cteClause = `WITH "${this.cte.name}" AS (${this.cte.query})\n`;
1038
+ context.allParams.push(...this.cte.params);
1039
+ context.paramCounter += this.cte.params.length;
1040
+ }
1041
+ // Build SELECT clause from result selector
1042
+ const mockLeft = this.createLeftMock();
1043
+ const mockRight = this.createRightMock();
1044
+ const mockResult = this.resultSelector(mockLeft, mockRight);
1045
+ const selectParts = [];
1046
+ for (const [alias, value] of Object.entries(mockResult)) {
1047
+ if (typeof value === 'object' && value !== null && '__dbColumnName' in value) {
1048
+ const field = value;
1049
+ const tableAlias = field.__tableAlias;
1050
+ if (tableAlias) {
1051
+ selectParts.push(`"${tableAlias}"."${field.__dbColumnName}" as "${alias}"`);
1052
+ }
1053
+ else {
1054
+ selectParts.push(`"${field.__dbColumnName}" as "${alias}"`);
1055
+ }
1056
+ }
1057
+ else if (value instanceof conditions_1.SqlFragment) {
1058
+ const sqlBuildContext = {
1059
+ paramCounter: context.paramCounter,
1060
+ params: context.allParams,
1061
+ };
1062
+ const fragmentSql = value.buildSql(sqlBuildContext);
1063
+ context.paramCounter = sqlBuildContext.paramCounter;
1064
+ selectParts.push(`${fragmentSql} as "${alias}"`);
1065
+ }
1066
+ else {
1067
+ selectParts.push(`"${alias}"`);
1068
+ }
1069
+ }
1070
+ // Build FROM clause with the left subquery
1071
+ const leftSqlContext = {
1072
+ paramCounter: context.paramCounter,
1073
+ params: context.allParams,
1074
+ };
1075
+ const leftSql = this.leftSubquery.buildSql(leftSqlContext);
1076
+ context.paramCounter = leftSqlContext.paramCounter;
1077
+ let fromClause = `FROM (${leftSql}) AS "${this.leftAlias}"`;
1078
+ // Build JOIN clause
1079
+ const joinTypeStr = this.joinType === 'INNER' ? 'INNER JOIN' : 'LEFT JOIN';
1080
+ const condBuilder = new conditions_1.ConditionBuilder();
1081
+ const { sql: condSql, params: condParams } = condBuilder.build(this.joinCondition, context.paramCounter);
1082
+ context.paramCounter += condParams.length;
1083
+ context.allParams.push(...condParams);
1084
+ if (this.cte) {
1085
+ // Join to CTE
1086
+ fromClause += `\n${joinTypeStr} "${this.rightAlias}" ON ${condSql}`;
1087
+ }
1088
+ else {
1089
+ // Join to subquery
1090
+ const rightSqlContext = {
1091
+ paramCounter: context.paramCounter,
1092
+ params: context.allParams,
1093
+ };
1094
+ const rightSql = this.rightSource.buildSql(rightSqlContext);
1095
+ context.paramCounter = rightSqlContext.paramCounter;
1096
+ fromClause += `\n${joinTypeStr} (${rightSql}) AS "${this.rightAlias}" ON ${condSql}`;
1097
+ }
1098
+ // Build ORDER BY clause
1099
+ let orderByClause = '';
1100
+ if (this.orderByFields.length > 0) {
1101
+ const orderParts = this.orderByFields.map(({ field, direction }) => `${field} ${direction}`);
1102
+ orderByClause = `ORDER BY ${orderParts.join(', ')}`;
1103
+ }
1104
+ // Build LIMIT/OFFSET
1105
+ let limitClause = '';
1106
+ if (this.limitValue !== undefined) {
1107
+ limitClause = `LIMIT ${this.limitValue}`;
1108
+ }
1109
+ if (this.offsetValue !== undefined) {
1110
+ limitClause += ` OFFSET ${this.offsetValue}`;
1111
+ }
1112
+ const finalQuery = `${cteClause}SELECT ${selectParts.join(', ')}\n${fromClause}\n${orderByClause}\n${limitClause}`.trim();
1113
+ return {
1114
+ sql: finalQuery,
1115
+ params: context.allParams,
1116
+ };
1117
+ }
1118
+ }
1119
+ exports.GroupedJoinedQueryBuilder = GroupedJoinedQueryBuilder;
588
1120
  //# sourceMappingURL=grouped-query.js.map