linkgress-orm 0.1.17 → 0.1.19

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 (38) hide show
  1. package/dist/database/database-client.interface.d.ts +29 -0
  2. package/dist/database/database-client.interface.d.ts.map +1 -1
  3. package/dist/database/database-client.interface.js +45 -1
  4. package/dist/database/database-client.interface.js.map +1 -1
  5. package/dist/database/pg-client.d.ts +5 -0
  6. package/dist/database/pg-client.d.ts.map +1 -1
  7. package/dist/database/pg-client.js +27 -0
  8. package/dist/database/pg-client.js.map +1 -1
  9. package/dist/database/postgres-client.d.ts +6 -0
  10. package/dist/database/postgres-client.d.ts.map +1 -1
  11. package/dist/database/postgres-client.js +17 -0
  12. package/dist/database/postgres-client.js.map +1 -1
  13. package/dist/entity/db-column.d.ts +17 -0
  14. package/dist/entity/db-column.d.ts.map +1 -1
  15. package/dist/entity/db-column.js.map +1 -1
  16. package/dist/entity/db-context.d.ts +134 -25
  17. package/dist/entity/db-context.d.ts.map +1 -1
  18. package/dist/entity/db-context.js +237 -174
  19. package/dist/entity/db-context.js.map +1 -1
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/query/cte-builder.d.ts +1 -1
  24. package/dist/query/cte-builder.js +8 -8
  25. package/dist/query/cte-builder.js.map +1 -1
  26. package/dist/query/grouped-query.d.ts +8 -0
  27. package/dist/query/grouped-query.d.ts.map +1 -1
  28. package/dist/query/grouped-query.js +12 -0
  29. package/dist/query/grouped-query.js.map +1 -1
  30. package/dist/query/query-builder.d.ts +13 -2
  31. package/dist/query/query-builder.d.ts.map +1 -1
  32. package/dist/query/query-builder.js +128 -42
  33. package/dist/query/query-builder.js.map +1 -1
  34. package/dist/query/sql-utils.d.ts +2 -0
  35. package/dist/query/sql-utils.d.ts.map +1 -1
  36. package/dist/query/sql-utils.js +24 -7
  37. package/dist/query/sql-utils.js.map +1 -1
  38. package/package.json +1 -1
@@ -27,6 +27,7 @@ var FieldType;
27
27
  FieldType[FieldType["FIELD_REF_MAPPER"] = 6] = "FIELD_REF_MAPPER";
28
28
  FieldType[FieldType["FIELD_REF_NO_MAPPER"] = 7] = "FIELD_REF_NO_MAPPER";
29
29
  FieldType[FieldType["SIMPLE"] = 8] = "SIMPLE";
30
+ FieldType[FieldType["COLLECTION_SINGLE"] = 9] = "COLLECTION_SINGLE";
30
31
  })(FieldType || (FieldType = {}));
31
32
  /**
32
33
  * Performance utility: Get column name map from schema, using cached version if available
@@ -175,7 +176,7 @@ class QueryBuilder {
175
176
  * Multiple where() calls are chained with AND logic
176
177
  */
177
178
  where(condition) {
178
- const mockRow = this.createMockRow();
179
+ const mockRow = this._createMockRow();
179
180
  const newCondition = condition(mockRow);
180
181
  if (this.whereCond) {
181
182
  this.whereCond = (0, conditions_1.and)(this.whereCond, newCondition);
@@ -194,8 +195,9 @@ class QueryBuilder {
194
195
  }
195
196
  /**
196
197
  * Create mock row for analysis
198
+ * @internal - Also used by DbEntityTable.props() to avoid code duplication
197
199
  */
198
- createMockRow() {
200
+ _createMockRow() {
199
201
  // Performance: Return cached mock if available
200
202
  if (this._cachedMockRow) {
201
203
  return this._cachedMockRow;
@@ -245,24 +247,27 @@ class QueryBuilder {
245
247
  targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
246
248
  }
247
249
  if (relConfig.type === 'many') {
250
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
248
251
  Object.defineProperty(mock, relName, {
249
252
  get: () => {
250
253
  return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
251
254
  );
252
255
  },
253
- enumerable: true,
256
+ enumerable: false,
254
257
  configurable: true,
255
258
  });
256
259
  }
257
260
  else {
258
261
  // Single reference navigation (many-to-one, one-to-one)
262
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
263
+ // with circular relations like User->Posts->User)
259
264
  Object.defineProperty(mock, relName, {
260
265
  get: () => {
261
266
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, this.schemaRegistry // Pass schema registry for nested navigation resolution
262
267
  );
263
268
  return refBuilder.createMockTargetRow();
264
269
  },
265
- enumerable: true,
270
+ enumerable: false,
266
271
  configurable: true,
267
272
  });
268
273
  }
@@ -293,7 +298,7 @@ class QueryBuilder {
293
298
  const rightAlias = `${rightSchema.name}_${this.joinCounter}`;
294
299
  const newJoinCounter = this.joinCounter + 1;
295
300
  // Create mock rows for condition evaluation
296
- const mockLeft = this.createMockRow();
301
+ const mockLeft = this._createMockRow();
297
302
  const mockRight = this.createMockRowForTable(rightSchema, rightAlias);
298
303
  const joinCondition = condition(mockLeft, mockRight);
299
304
  // Add the join to the list
@@ -306,7 +311,7 @@ class QueryBuilder {
306
311
  }];
307
312
  // Store schemas for creating fresh mocks in the selector
308
313
  const leftSchema = this.schema;
309
- const createLeftMock = () => this.createMockRow();
314
+ const createLeftMock = () => this._createMockRow();
310
315
  const createRightMock = () => this.createMockRowForTable(rightSchema, rightAlias);
311
316
  // Create a selector wrapper that generates fresh mocks and calls the user's selector
312
317
  const wrappedSelector = (row) => {
@@ -342,7 +347,7 @@ class QueryBuilder {
342
347
  const rightAlias = `${rightSchema.name}_${this.joinCounter}`;
343
348
  const newJoinCounter = this.joinCounter + 1;
344
349
  // Create mock rows for condition evaluation
345
- const mockLeft = this.createMockRow();
350
+ const mockLeft = this._createMockRow();
346
351
  const mockRight = this.createMockRowForTable(rightSchema, rightAlias);
347
352
  const joinCondition = condition(mockLeft, mockRight);
348
353
  // Add the join to the list
@@ -355,7 +360,7 @@ class QueryBuilder {
355
360
  }];
356
361
  // Store schemas for creating fresh mocks in the selector
357
362
  const leftSchema = this.schema;
358
- const createLeftMock = () => this.createMockRow();
363
+ const createLeftMock = () => this._createMockRow();
359
364
  const createRightMock = () => this.createMockRowForTable(rightSchema, rightAlias);
360
365
  // Create a selector wrapper that generates fresh mocks and calls the user's selector
361
366
  const wrappedSelector = (row) => {
@@ -404,22 +409,24 @@ class QueryBuilder {
404
409
  const targetSchema = getTargetSchemaForRelation(schema, relName, relConfig);
405
410
  if (relConfig.type === 'many') {
406
411
  // Collection navigation
412
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
407
413
  Object.defineProperty(mock, relName, {
408
414
  get: () => {
409
415
  return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
410
416
  },
411
- enumerable: true,
417
+ enumerable: false,
412
418
  configurable: true,
413
419
  });
414
420
  }
415
421
  else {
416
422
  // Single reference navigation
423
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
417
424
  Object.defineProperty(mock, relName, {
418
425
  get: () => {
419
426
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
420
427
  return refBuilder.createMockTargetRow();
421
428
  },
422
- enumerable: true,
429
+ enumerable: false,
423
430
  configurable: true,
424
431
  });
425
432
  }
@@ -441,7 +448,7 @@ class QueryBuilder {
441
448
  return this;
442
449
  }
443
450
  orderBy(selector) {
444
- const mockRow = this.createMockRow();
451
+ const mockRow = this._createMockRow();
445
452
  const result = selector(mockRow);
446
453
  (0, query_utils_1.parseOrderBy)(result, this.orderByFields);
447
454
  return this;
@@ -497,7 +504,7 @@ class SelectQueryBuilder {
497
504
  * Note: The row parameter represents the selected shape (after select())
498
505
  */
499
506
  where(condition) {
500
- const mockRow = this.createMockRow();
507
+ const mockRow = this._createMockRow();
501
508
  // Apply the selector to get the selected shape that the user sees in the WHERE condition
502
509
  const selectedMock = this.selector(mockRow);
503
510
  // Wrap in proxy - for WHERE, we preserve original column names
@@ -545,7 +552,7 @@ class SelectQueryBuilder {
545
552
  return this;
546
553
  }
547
554
  orderBy(selector) {
548
- const mockRow = this.createMockRow();
555
+ const mockRow = this._createMockRow();
549
556
  const selectedMock = this.selector(mockRow);
550
557
  // Wrap selectedMock in a proxy that returns FieldRefs for property access
551
558
  const fieldRefProxy = this.createFieldRefProxy(selectedMock);
@@ -577,7 +584,7 @@ class SelectQueryBuilder {
577
584
  leftJoinSubquery(subquery, alias, condition, selector) {
578
585
  const newJoinCounter = this.joinCounter + 1;
579
586
  // Create mock for the current selection (left side)
580
- const mockRow = this.createMockRow();
587
+ const mockRow = this._createMockRow();
581
588
  const mockLeftSelection = this.selector(mockRow);
582
589
  // Create mock for the subquery result (right side)
583
590
  // For subqueries, we create a mock based on the result type
@@ -621,7 +628,7 @@ class SelectQueryBuilder {
621
628
  const newJoinCounter = this.joinCounter + 1;
622
629
  // Create mock for the current selection (left side)
623
630
  // IMPORTANT: We call the selector with the mock row to get a result that contains FieldRef objects
624
- const mockRow = this.createMockRow();
631
+ const mockRow = this._createMockRow();
625
632
  const mockLeftSelection = this.selector(mockRow);
626
633
  // The mockLeftSelection now contains FieldRef objects (with __fieldName, __dbColumnName, __tableAlias)
627
634
  // These FieldRef objects preserve the table context
@@ -652,7 +659,7 @@ class SelectQueryBuilder {
652
659
  leftJoinCte(cte, condition, selector) {
653
660
  const newJoinCounter = this.joinCounter + 1;
654
661
  // Create mock for the current selection (left side)
655
- const mockRow = this.createMockRow();
662
+ const mockRow = this._createMockRow();
656
663
  const mockLeftSelection = this.selector(mockRow);
657
664
  // Create mock for the CTE columns (right side)
658
665
  const mockRight = this.createMockRowForCte(cte);
@@ -685,7 +692,7 @@ class SelectQueryBuilder {
685
692
  innerJoinSubquery(subquery, alias, condition, selector) {
686
693
  const newJoinCounter = this.joinCounter + 1;
687
694
  // Create mock for the current selection (left side)
688
- const mockRow = this.createMockRow();
695
+ const mockRow = this._createMockRow();
689
696
  const mockLeftSelection = this.selector(mockRow);
690
697
  // Create mock for the subquery result (right side)
691
698
  const mockRight = this.createMockRowForSubquery(alias, subquery);
@@ -727,7 +734,7 @@ class SelectQueryBuilder {
727
734
  const rightAlias = `${rightSchema.name}_${this.joinCounter}`;
728
735
  const newJoinCounter = this.joinCounter + 1;
729
736
  // Create mock for the current selection (left side)
730
- const mockRow = this.createMockRow();
737
+ const mockRow = this._createMockRow();
731
738
  const mockLeftSelection = this.selector(mockRow);
732
739
  // Create mock for the right table
733
740
  const mockRight = this.createMockRowForTable(rightSchema, rightAlias);
@@ -784,22 +791,24 @@ class SelectQueryBuilder {
784
791
  const targetSchema = getTargetSchemaForRelation(schema, relName, relConfig);
785
792
  if (relConfig.type === 'many') {
786
793
  // Collection navigation
794
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
787
795
  Object.defineProperty(mock, relName, {
788
796
  get: () => {
789
797
  return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', schema.name, targetSchema);
790
798
  },
791
- enumerable: true,
799
+ enumerable: false,
792
800
  configurable: true,
793
801
  });
794
802
  }
795
803
  else {
796
804
  // Single reference navigation
805
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
797
806
  Object.defineProperty(mock, relName, {
798
807
  get: () => {
799
808
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema);
800
809
  return refBuilder.createMockTargetRow();
801
810
  },
802
- enumerable: true,
811
+ enumerable: false,
803
812
  configurable: true,
804
813
  });
805
814
  }
@@ -936,13 +945,13 @@ class SelectQueryBuilder {
936
945
  // If selector is provided, apply it to determine the field
937
946
  let fieldToAggregate;
938
947
  if (selector) {
939
- const mockRow = this.createMockRow();
948
+ const mockRow = this._createMockRow();
940
949
  const mockSelection = this.selector(mockRow);
941
950
  fieldToAggregate = selector(mockSelection);
942
951
  }
943
952
  else {
944
953
  // No selector - use the current selection
945
- const mockRow = this.createMockRow();
954
+ const mockRow = this._createMockRow();
946
955
  fieldToAggregate = this.selector(mockRow);
947
956
  }
948
957
  // Build aggregation query
@@ -967,13 +976,13 @@ class SelectQueryBuilder {
967
976
  // If selector is provided, apply it to determine the field
968
977
  let fieldToAggregate;
969
978
  if (selector) {
970
- const mockRow = this.createMockRow();
979
+ const mockRow = this._createMockRow();
971
980
  const mockSelection = this.selector(mockRow);
972
981
  fieldToAggregate = selector(mockSelection);
973
982
  }
974
983
  else {
975
984
  // No selector - use the current selection
976
- const mockRow = this.createMockRow();
985
+ const mockRow = this._createMockRow();
977
986
  fieldToAggregate = this.selector(mockRow);
978
987
  }
979
988
  // Build aggregation query
@@ -998,13 +1007,13 @@ class SelectQueryBuilder {
998
1007
  // If selector is provided, apply it to determine the field
999
1008
  let fieldToAggregate;
1000
1009
  if (selector) {
1001
- const mockRow = this.createMockRow();
1010
+ const mockRow = this._createMockRow();
1002
1011
  const mockSelection = this.selector(mockRow);
1003
1012
  fieldToAggregate = selector(mockSelection);
1004
1013
  }
1005
1014
  else {
1006
1015
  // No selector - use the current selection
1007
- const mockRow = this.createMockRow();
1016
+ const mockRow = this._createMockRow();
1008
1017
  fieldToAggregate = this.selector(mockRow);
1009
1018
  }
1010
1019
  // Build aggregation query
@@ -1052,7 +1061,7 @@ class SelectQueryBuilder {
1052
1061
  executor: this.executor,
1053
1062
  }));
1054
1063
  // Analyze the selector to extract nested queries
1055
- const mockRow = tracer.trace('createMockRow', () => this.createMockRow());
1064
+ const mockRow = tracer.trace('createMockRow', () => this._createMockRow());
1056
1065
  const selectionResult = tracer.trace('evaluateSelector', () => this.selector(mockRow));
1057
1066
  // Check if we're using temp table strategy and have collections
1058
1067
  const collections = tracer.trace('detectCollections', () => this.detectCollections(selectionResult));
@@ -1319,7 +1328,7 @@ class SelectQueryBuilder {
1319
1328
  const baseSelection = {};
1320
1329
  const collectionNames = new Set(collections.map(c => c.name));
1321
1330
  // Always ensure we have the primary key in the base selection with a known alias
1322
- const mockRow = this.createMockRow();
1331
+ const mockRow = this._createMockRow();
1323
1332
  baseSelection['__pk_id'] = mockRow.id; // Add primary key with a known alias
1324
1333
  for (const [key, value] of Object.entries(selection)) {
1325
1334
  if (!collectionNames.has(key)) {
@@ -1413,6 +1422,10 @@ class SelectQueryBuilder {
1413
1422
  // Array aggregation
1414
1423
  return [];
1415
1424
  }
1425
+ else if (builder.isSingleResult()) {
1426
+ // firstOrDefault() - single item
1427
+ return null;
1428
+ }
1416
1429
  else {
1417
1430
  // JSONB aggregation (object array)
1418
1431
  return [];
@@ -1467,8 +1480,10 @@ class SelectQueryBuilder {
1467
1480
  }
1468
1481
  const condBuilder = new conditions_1.ConditionBuilder();
1469
1482
  const { sql: whereSql, params: whereParams } = condBuilder.build(queryBuilder.whereCond, 1);
1470
- // Build RETURNING clause
1471
- const returningClause = queryBuilder.buildDeleteReturningClause(returning);
1483
+ // Build RETURNING clause (not needed for count-only)
1484
+ const returningClause = returning !== 'count'
1485
+ ? queryBuilder.buildDeleteReturningClause(returning)
1486
+ : undefined;
1472
1487
  const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
1473
1488
  let sql = `DELETE FROM ${qualifiedTableName} WHERE ${whereSql}`;
1474
1489
  if (returningClause) {
@@ -1477,6 +1492,10 @@ class SelectQueryBuilder {
1477
1492
  const result = queryBuilder.executor
1478
1493
  ? await queryBuilder.executor.query(sql, whereParams)
1479
1494
  : await queryBuilder.client.query(sql, whereParams);
1495
+ // Return affected count
1496
+ if (returning === 'count') {
1497
+ return result.rowCount ?? 0;
1498
+ }
1480
1499
  if (!returningClause) {
1481
1500
  return undefined;
1482
1501
  }
@@ -1486,6 +1505,13 @@ class SelectQueryBuilder {
1486
1505
  then(onfulfilled, onrejected) {
1487
1506
  return executeDelete(undefined).then(onfulfilled, onrejected);
1488
1507
  },
1508
+ affectedCount() {
1509
+ return {
1510
+ then(onfulfilled, onrejected) {
1511
+ return executeDelete('count').then(onfulfilled, onrejected);
1512
+ }
1513
+ };
1514
+ },
1489
1515
  returning(selector) {
1490
1516
  const returningConfig = selector ?? true;
1491
1517
  return {
@@ -1544,8 +1570,10 @@ class SelectQueryBuilder {
1544
1570
  const condBuilder = new conditions_1.ConditionBuilder();
1545
1571
  const { sql: whereSql, params: whereParams } = condBuilder.build(queryBuilder.whereCond, paramIndex);
1546
1572
  values.push(...whereParams);
1547
- // Build RETURNING clause
1548
- const returningClause = queryBuilder.buildDeleteReturningClause(returning);
1573
+ // Build RETURNING clause (not needed for count-only)
1574
+ const returningClause = returning !== 'count'
1575
+ ? queryBuilder.buildDeleteReturningClause(returning)
1576
+ : undefined;
1549
1577
  const qualifiedTableName = queryBuilder.getQualifiedTableName(queryBuilder.schema.name, queryBuilder.schema.schema);
1550
1578
  let sql = `UPDATE ${qualifiedTableName} SET ${setClauses.join(', ')} WHERE ${whereSql}`;
1551
1579
  if (returningClause) {
@@ -1554,6 +1582,10 @@ class SelectQueryBuilder {
1554
1582
  const result = queryBuilder.executor
1555
1583
  ? await queryBuilder.executor.query(sql, values)
1556
1584
  : await queryBuilder.client.query(sql, values);
1585
+ // Return affected count
1586
+ if (returning === 'count') {
1587
+ return result.rowCount ?? 0;
1588
+ }
1557
1589
  if (!returningClause) {
1558
1590
  return undefined;
1559
1591
  }
@@ -1563,6 +1595,13 @@ class SelectQueryBuilder {
1563
1595
  then(onfulfilled, onrejected) {
1564
1596
  return executeUpdate(undefined).then(onfulfilled, onrejected);
1565
1597
  },
1598
+ affectedCount() {
1599
+ return {
1600
+ then(onfulfilled, onrejected) {
1601
+ return executeUpdate('count').then(onfulfilled, onrejected);
1602
+ }
1603
+ };
1604
+ },
1566
1605
  returning(selector) {
1567
1606
  const returningConfig = selector ?? true;
1568
1607
  return {
@@ -1588,7 +1627,7 @@ class SelectQueryBuilder {
1588
1627
  return { sql, columns };
1589
1628
  }
1590
1629
  // Selector function - extract selected columns
1591
- const mockRow = this.createMockRow();
1630
+ const mockRow = this._createMockRow();
1592
1631
  const selectedMock = this.selector(mockRow);
1593
1632
  const selection = returning(selectedMock);
1594
1633
  if (typeof selection === 'object' && selection !== null) {
@@ -1647,8 +1686,9 @@ class SelectQueryBuilder {
1647
1686
  }
1648
1687
  /**
1649
1688
  * Create mock row for analysis
1689
+ * @internal
1650
1690
  */
1651
- createMockRow() {
1691
+ _createMockRow() {
1652
1692
  const mock = {};
1653
1693
  const tableAlias = this.schema.name;
1654
1694
  // Performance: Use pre-computed column name map if available
@@ -1730,18 +1770,20 @@ class SelectQueryBuilder {
1730
1770
  targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
1731
1771
  }
1732
1772
  if (relConfig.type === 'many') {
1773
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
1733
1774
  Object.defineProperty(mock, relName, {
1734
1775
  get: () => {
1735
1776
  return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, // Pass the target schema directly
1736
1777
  this.schemaRegistry // Pass schema registry for nested resolution
1737
1778
  );
1738
1779
  },
1739
- enumerable: true,
1780
+ enumerable: false,
1740
1781
  configurable: true,
1741
1782
  });
1742
1783
  }
1743
1784
  else {
1744
1785
  // For single reference (many-to-one), create a ReferenceQueryBuilder
1786
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
1745
1787
  Object.defineProperty(mock, relName, {
1746
1788
  get: () => {
1747
1789
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, // Pass the target schema directly
@@ -1750,7 +1792,7 @@ class SelectQueryBuilder {
1750
1792
  // Return a mock object that exposes the target table's columns
1751
1793
  return refBuilder.createMockTargetRow();
1752
1794
  },
1753
- enumerable: true,
1795
+ enumerable: false,
1754
1796
  configurable: true,
1755
1797
  });
1756
1798
  }
@@ -2436,9 +2478,18 @@ class SelectQueryBuilder {
2436
2478
  }
2437
2479
  else {
2438
2480
  const isArrayAgg = value && typeof value === 'object' && 'isArrayAggregation' in value && value.isArrayAggregation();
2481
+ const isSingleResult = value instanceof CollectionQueryBuilder && value.isSingleResult();
2439
2482
  if (isArrayAgg) {
2440
2483
  fieldConfigs.push({ key, type: 2 /* FieldType.COLLECTION_ARRAY */, value });
2441
2484
  }
2485
+ else if (isSingleResult) {
2486
+ fieldConfigs.push({
2487
+ key,
2488
+ type: 9 /* FieldType.COLLECTION_SINGLE */,
2489
+ value,
2490
+ collectionBuilder: value
2491
+ });
2492
+ }
2442
2493
  else {
2443
2494
  fieldConfigs.push({
2444
2495
  key,
@@ -2549,6 +2600,18 @@ class SelectQueryBuilder {
2549
2600
  }
2550
2601
  break;
2551
2602
  }
2603
+ case 9 /* FieldType.COLLECTION_SINGLE */: {
2604
+ // firstOrDefault() - return first item or null
2605
+ const items = rawValue || [];
2606
+ if (config.collectionBuilder && items.length > 0) {
2607
+ const transformedItems = this.transformCollectionItems(items, config.collectionBuilder);
2608
+ result[key] = transformedItems[0] ?? null;
2609
+ }
2610
+ else {
2611
+ result[key] = items[0] ?? null;
2612
+ }
2613
+ break;
2614
+ }
2552
2615
  case 4 /* FieldType.CTE_AGGREGATION */: {
2553
2616
  const items = rawValue || [];
2554
2617
  if (config.innerMetadata && !disableMappers) {
@@ -2851,7 +2914,7 @@ class SelectQueryBuilder {
2851
2914
  executor: this.executor,
2852
2915
  };
2853
2916
  // Analyze the selector to extract nested queries
2854
- const mockRow = this.createMockRow();
2917
+ const mockRow = this._createMockRow();
2855
2918
  const selectionResult = this.selector(mockRow);
2856
2919
  // Build the query
2857
2920
  const { sql } = this.buildQuery(selectionResult, context);
@@ -2862,7 +2925,7 @@ class SelectQueryBuilder {
2862
2925
  // For table subqueries, preserve the selection metadata (includes SqlFragments with mappers)
2863
2926
  let selectionMetadata;
2864
2927
  if (mode === 'table') {
2865
- const mockRow = this.createMockRow();
2928
+ const mockRow = this._createMockRow();
2866
2929
  selectionMetadata = this.selector(mockRow);
2867
2930
  }
2868
2931
  // Extract outer field refs from the WHERE condition
@@ -3005,6 +3068,7 @@ class ReferenceQueryBuilder {
3005
3068
  }
3006
3069
  if (relConfig.type === 'many') {
3007
3070
  // Collection navigation
3071
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
3008
3072
  Object.defineProperty(mock, relName, {
3009
3073
  get: () => {
3010
3074
  const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
@@ -3012,12 +3076,14 @@ class ReferenceQueryBuilder {
3012
3076
  this.schemaRegistry // Pass schema registry for nested resolution
3013
3077
  );
3014
3078
  },
3015
- enumerable: true,
3079
+ enumerable: false,
3016
3080
  configurable: true,
3017
3081
  });
3018
3082
  }
3019
3083
  else {
3020
3084
  // Reference navigation
3085
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
3086
+ // with circular relations like User->Posts->User)
3021
3087
  Object.defineProperty(mock, relName, {
3022
3088
  get: () => {
3023
3089
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
@@ -3025,7 +3091,7 @@ class ReferenceQueryBuilder {
3025
3091
  );
3026
3092
  return refBuilder.createMockTargetRow();
3027
3093
  },
3028
- enumerable: true,
3094
+ enumerable: false,
3029
3095
  configurable: true,
3030
3096
  });
3031
3097
  }
@@ -3142,6 +3208,7 @@ class CollectionQueryBuilder {
3142
3208
  for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
3143
3209
  if (relConfig.type === 'many') {
3144
3210
  // Collection navigation
3211
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
3145
3212
  Object.defineProperty(mock, relName, {
3146
3213
  get: () => {
3147
3214
  // Don't call build() - it returns schema without relations
@@ -3150,12 +3217,13 @@ class CollectionQueryBuilder {
3150
3217
  this.schemaRegistry // Pass schema registry for nested resolution
3151
3218
  );
3152
3219
  },
3153
- enumerable: true,
3220
+ enumerable: false,
3154
3221
  configurable: true,
3155
3222
  });
3156
3223
  }
3157
3224
  else {
3158
3225
  // Reference navigation
3226
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
3159
3227
  Object.defineProperty(mock, relName, {
3160
3228
  get: () => {
3161
3229
  // Don't call build() - it returns schema without relations
@@ -3165,7 +3233,7 @@ class CollectionQueryBuilder {
3165
3233
  );
3166
3234
  return refBuilder.createMockTargetRow();
3167
3235
  },
3168
- enumerable: true,
3236
+ enumerable: false,
3169
3237
  configurable: true,
3170
3238
  });
3171
3239
  }
@@ -3282,6 +3350,18 @@ class CollectionQueryBuilder {
3282
3350
  // At runtime, this is still a CollectionQueryBuilder, but TypeScript sees it as CollectionResult
3283
3351
  return this;
3284
3352
  }
3353
+ /**
3354
+ * Get first item from collection or null if empty
3355
+ * Automatically applies LIMIT 1 and returns a single item instead of array
3356
+ */
3357
+ firstOrDefault(name) {
3358
+ if (name) {
3359
+ this.asName = name;
3360
+ }
3361
+ this.limitValue = 1;
3362
+ this.isMarkedAsList = false; // Single item, not a list
3363
+ return this;
3364
+ }
3285
3365
  /**
3286
3366
  * Get target table schema
3287
3367
  */
@@ -3300,6 +3380,12 @@ class CollectionQueryBuilder {
3300
3380
  isScalarAggregation() {
3301
3381
  return this.aggregationType !== undefined;
3302
3382
  }
3383
+ /**
3384
+ * Check if this is a single item result (firstOrDefault)
3385
+ */
3386
+ isSingleResult() {
3387
+ return !this.isMarkedAsList && this.limitValue === 1;
3388
+ }
3303
3389
  /**
3304
3390
  * Get the aggregation type
3305
3391
  */