linkgress-orm 0.4.76 → 0.4.79

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 (39) hide show
  1. package/dist/config/linkgress-config.d.ts +23 -0
  2. package/dist/config/linkgress-config.d.ts.map +1 -1
  3. package/dist/config/linkgress-config.js +29 -0
  4. package/dist/config/linkgress-config.js.map +1 -1
  5. package/dist/entity/db-context.d.ts +45 -0
  6. package/dist/entity/db-context.d.ts.map +1 -1
  7. package/dist/entity/db-context.js +59 -1
  8. package/dist/entity/db-context.js.map +1 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +6 -4
  12. package/dist/index.js.map +1 -1
  13. package/dist/query/collection-strategy.interface.d.ts +14 -0
  14. package/dist/query/collection-strategy.interface.d.ts.map +1 -1
  15. package/dist/query/conditions.d.ts +36 -0
  16. package/dist/query/conditions.d.ts.map +1 -1
  17. package/dist/query/conditions.js +78 -10
  18. package/dist/query/conditions.js.map +1 -1
  19. package/dist/query/lateral-sql-cache.d.ts +53 -0
  20. package/dist/query/lateral-sql-cache.d.ts.map +1 -0
  21. package/dist/query/lateral-sql-cache.js +63 -0
  22. package/dist/query/lateral-sql-cache.js.map +1 -0
  23. package/dist/query/mutation-batch.d.ts +22 -0
  24. package/dist/query/mutation-batch.d.ts.map +1 -1
  25. package/dist/query/mutation-batch.js +30 -1
  26. package/dist/query/mutation-batch.js.map +1 -1
  27. package/dist/query/query-builder.d.ts +18 -1
  28. package/dist/query/query-builder.d.ts.map +1 -1
  29. package/dist/query/query-builder.js +203 -59
  30. package/dist/query/query-builder.js.map +1 -1
  31. package/dist/query/sql-utils.d.ts +12 -0
  32. package/dist/query/sql-utils.d.ts.map +1 -1
  33. package/dist/query/sql-utils.js +22 -2
  34. package/dist/query/sql-utils.js.map +1 -1
  35. package/dist/query/strategies/lateral-collection-strategy.d.ts +28 -3
  36. package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
  37. package/dist/query/strategies/lateral-collection-strategy.js +161 -180
  38. package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
  39. package/package.json +1 -1
@@ -4,6 +4,7 @@ exports.CollectionQueryBuilder = exports.ReferenceQueryBuilder = exports.SelectQ
4
4
  exports.getColumnNameMapForSchema = getColumnNameMapForSchema;
5
5
  exports.getRelationEntriesForSchema = getRelationEntriesForSchema;
6
6
  exports.getSchemaColumnMeta = getSchemaColumnMeta;
7
+ exports.getDbToPropertyMapForSchema = getDbToPropertyMapForSchema;
7
8
  exports.getTargetSchemaForRelation = getTargetSchemaForRelation;
8
9
  exports.createNestedFieldRefProxy = createNestedFieldRefProxy;
9
10
  const conditions_1 = require("./conditions");
@@ -81,6 +82,23 @@ function getSchemaColumnMeta(schema) {
81
82
  }
82
83
  return meta;
83
84
  }
85
+ /**
86
+ * Reverse of the column-name map — db column name → property name — cached per schema.
87
+ * An ordered collection needs it on every build to render the alias form of its ORDER BY;
88
+ * rebuilding it walked every column of the schema per build.
89
+ */
90
+ const schemaDbToPropertyMapCache = new WeakMap();
91
+ function getDbToPropertyMapForSchema(schema) {
92
+ let map = schemaDbToPropertyMapCache.get(schema);
93
+ if (map == null) {
94
+ map = new Map();
95
+ for (const [propName, meta] of getSchemaColumnMeta(schema)) {
96
+ map.set(meta.name, propName);
97
+ }
98
+ schemaDbToPropertyMapCache.set(schema, map);
99
+ }
100
+ return map;
101
+ }
84
102
  /**
85
103
  * Mock-row descriptor cache for {@link ReferenceQueryBuilder.createMockTargetRow}.
86
104
  *
@@ -103,6 +121,13 @@ const MOCK_ROW_CHAIN_ID = Symbol('linkgressMockChainId');
103
121
  const navigationPathSignature = (path) => path
104
122
  .map(step => `${step.alias}:${step.targetTable}:${(step.foreignKeys ?? []).join('+')}:${(step.matches ?? []).join('+')}:${step.isMandatory ? 1 : 0}:${step.sourceAlias ?? ''}`)
105
123
  .join('>');
124
+ /** A reference mock row: `Object.create(prototype)` plus its two own state slots. */
125
+ const mintReferenceMockRow = (prototype) => {
126
+ const mock = Object.create(prototype);
127
+ mock[MOCK_ROW_FIELD_REFS] = {};
128
+ mock[MOCK_ROW_NAV_CACHE] = {};
129
+ return mock;
130
+ };
106
131
  /**
107
132
  * Whether `value` is a mock row minted by the query builders (`createMockTargetRow`,
108
133
  * `SelectQueryBuilder._createMockRow`, `CollectionQueryBuilder.createMockItem`). Every
@@ -470,13 +495,25 @@ class QueryBuilder {
470
495
  // Single reference navigation (many-to-one, one-to-one)
471
496
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
472
497
  // with circular relations like User->Posts->User)
498
+ const holder = {};
473
499
  descriptors[relName] = {
474
- get: () => {
500
+ get() {
501
+ // One mock target row per row and relation (a selector reading `p.user.*` several
502
+ // times used to mint a builder and a row per access)
503
+ const slots = this;
504
+ const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
505
+ const cachedRow = navCache[relName];
506
+ if (cachedRow !== undefined) {
507
+ return cachedRow;
508
+ }
509
+ if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
510
+ return (navCache[relName] = mintReferenceMockRow(holder.prototype));
511
+ }
475
512
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, schemaRegistry, // Pass schema registry for nested navigation resolution
476
513
  [], // Empty navigation path for first level navigation
477
514
  sourceTableName // Pass source table name for lateral join correlation
478
515
  );
479
- return refBuilder.createMockTargetRow();
516
+ return (navCache[relName] = refBuilder.createMockTargetRow(holder));
480
517
  },
481
518
  enumerable: false,
482
519
  configurable: true,
@@ -632,12 +669,20 @@ class QueryBuilder {
632
669
  // Single reference navigation
633
670
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
634
671
  Object.defineProperty(mock, relName, {
635
- get: () => {
672
+ get() {
673
+ // One mock target row per row and relation (a selector reading `p.user.*` several
674
+ // times used to mint a builder and a row per access)
675
+ const slots = this;
676
+ const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
677
+ const cachedRow = navCache[relName];
678
+ if (cachedRow !== undefined) {
679
+ return cachedRow;
680
+ }
636
681
  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
637
682
  [], // Empty navigation path for first level navigation
638
683
  schema.name // Pass source table name for lateral join correlation
639
684
  );
640
- return refBuilder.createMockTargetRow();
685
+ return (navCache[relName] = refBuilder.createMockTargetRow());
641
686
  },
642
687
  enumerable: false,
643
688
  configurable: true,
@@ -1188,12 +1233,20 @@ class SelectQueryBuilder {
1188
1233
  // Single reference navigation
1189
1234
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
1190
1235
  Object.defineProperty(mock, relName, {
1191
- get: () => {
1236
+ get() {
1237
+ // One mock target row per row and relation (a selector reading `p.user.*` several
1238
+ // times used to mint a builder and a row per access)
1239
+ const slots = this;
1240
+ const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
1241
+ const cachedRow = navCache[relName];
1242
+ if (cachedRow !== undefined) {
1243
+ return cachedRow;
1244
+ }
1192
1245
  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
1193
1246
  [], // Empty navigation path for first level navigation
1194
1247
  schema.name // Pass source table name for lateral join correlation
1195
1248
  );
1196
- return refBuilder.createMockTargetRow();
1249
+ return (navCache[relName] = refBuilder.createMockTargetRow());
1197
1250
  },
1198
1251
  enumerable: false,
1199
1252
  configurable: true,
@@ -3646,15 +3699,27 @@ ${joinClauses.join('\n')}`;
3646
3699
  else {
3647
3700
  // For single reference (many-to-one), create a ReferenceQueryBuilder
3648
3701
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
3702
+ const holder = {};
3649
3703
  descriptors[relName] = {
3650
- get: () => {
3704
+ get() {
3705
+ // One mock target row per row and relation (a selector reading `p.user.*` several
3706
+ // times used to mint a builder and a row per access)
3707
+ const slots = this;
3708
+ const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
3709
+ const cachedRow = navCache[relName];
3710
+ if (cachedRow !== undefined) {
3711
+ return cachedRow;
3712
+ }
3713
+ if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
3714
+ return (navCache[relName] = mintReferenceMockRow(holder.prototype));
3715
+ }
3651
3716
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, // Pass the target schema directly
3652
3717
  schemaRegistry, // Pass schema registry for nested resolution
3653
3718
  [], // Empty navigation path for first level navigation
3654
3719
  sourceTableName // Pass source table name for lateral join correlation
3655
3720
  );
3656
3721
  // Return a mock object that exposes the target table's columns
3657
- return refBuilder.createMockTargetRow();
3722
+ return (navCache[relName] = refBuilder.createMockTargetRow(holder));
3658
3723
  },
3659
3724
  enumerable: false,
3660
3725
  configurable: true,
@@ -4204,7 +4269,11 @@ ${joinClauses.join('\n')}`;
4204
4269
  }
4205
4270
  else {
4206
4271
  // Process selection object properties
4207
- for (const [key, value] of Object.entries(selection)) {
4272
+ for (const key in selection) {
4273
+ if (!Object.prototype.hasOwnProperty.call(selection, key)) {
4274
+ continue;
4275
+ }
4276
+ const value = selection[key];
4208
4277
  if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
4209
4278
  // Handle collection - delegate to strategy pattern via buildCTE
4210
4279
  // The strategy handles CTE/LATERAL specifics and returns necessary info
@@ -4678,7 +4747,11 @@ ${joinClauses.join('\n')}`;
4678
4747
  // - collections hit an explicit `continue` and were never emitted.
4679
4748
  // Both regressions are now covered by union-nested-select.test.ts and
4680
4749
  // union-collection-nav.test.ts.
4681
- for (const [key, value] of Object.entries(selection)) {
4750
+ for (const key in selection) {
4751
+ if (!Object.prototype.hasOwnProperty.call(selection, key)) {
4752
+ continue;
4753
+ }
4754
+ const value = selection[key];
4682
4755
  if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
4683
4756
  // Collection projection inside a UNION leg — delegate to the
4684
4757
  // collection strategy (LATERAL by default) and emit per-row
@@ -5828,7 +5901,7 @@ class ReferenceQueryBuilder {
5828
5901
  * Create a mock object that exposes the target table's columns
5829
5902
  * This allows accessing related fields like: p.user.username
5830
5903
  */
5831
- createMockTargetRow() {
5904
+ createMockTargetRow(holder) {
5832
5905
  if (this.targetTableSchema) {
5833
5906
  // Prototype-level cache — see MockRowCache's doc. Everything the getters close over
5834
5907
  // is fully determined by (target schema object identity, relationName, sourceAlias,
@@ -5844,11 +5917,18 @@ class ReferenceQueryBuilder {
5844
5917
  // Consumers must not probe these rows with OWN-property APIs (`Object.keys`,
5845
5918
  // `getOwnPropertyNames`, `getOwnPropertyDescriptor` on the row itself) — the getters
5846
5919
  // are inherited. Use `isReferenceMockRow` / `findFirstGetterKey` instead.
5847
- const prototype = mock_row_cache_1.MockRowCache.getOrBuild(`${this.targetTable}|${this.relationName}|${this.sourceAlias ?? ''}|${navigationPathSignature(this.navigationPath)}`, () => Object.defineProperties({}, this.buildMockRowDescriptors()));
5848
- const mock = Object.create(prototype);
5849
- mock[MOCK_ROW_FIELD_REFS] = {};
5850
- mock[MOCK_ROW_NAV_CACHE] = {};
5851
- return mock;
5920
+ //
5921
+ // The getter that minted this builder may hand in its MockPrototypeHolder: a filled holder
5922
+ // short-circuits the key build and the lookup, and a miss fills it (switch on only).
5923
+ const enabled = mock_row_cache_1.MockRowCache.isEnabled();
5924
+ let prototype = enabled ? holder?.prototype : undefined;
5925
+ if (prototype === undefined) {
5926
+ prototype = mock_row_cache_1.MockRowCache.getOrBuild(enabled ? `${this.targetTable}|${this.relationName}|${this.sourceAlias ?? ''}|${navigationPathSignature(this.navigationPath)}` : '', () => Object.defineProperties({}, this.buildMockRowDescriptors()));
5927
+ if (enabled && holder !== undefined) {
5928
+ holder.prototype = prototype;
5929
+ }
5930
+ }
5931
+ return mintReferenceMockRow(prototype);
5852
5932
  }
5853
5933
  else {
5854
5934
  // Fallback: use the shared nested proxy that supports deep property access
@@ -5967,18 +6047,24 @@ class ReferenceQueryBuilder {
5967
6047
  // Reference navigation
5968
6048
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
5969
6049
  // with circular relations like User->Posts->User)
6050
+ const holder = {};
5970
6051
  descriptors[relName] = {
5971
6052
  get() {
5972
6053
  const slots = this;
5973
6054
  const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
5974
6055
  let cached = navCache[relName];
5975
6056
  if (cached === undefined) {
5976
- const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
5977
- schemaRegistry, // Pass schema registry for nested resolution
5978
- extendedNavPath, // Pass navigation path for nested collections
5979
- parentSourceAlias ? tableAlias : '' // Only set source if tracking path
5980
- );
5981
- cached = navCache[relName] = refBuilder.createMockTargetRow();
6057
+ if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
6058
+ cached = navCache[relName] = mintReferenceMockRow(holder.prototype);
6059
+ }
6060
+ else {
6061
+ const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
6062
+ schemaRegistry, // Pass schema registry for nested resolution
6063
+ extendedNavPath, // Pass navigation path for nested collections
6064
+ parentSourceAlias ? tableAlias : '' // Only set source if tracking path
6065
+ );
6066
+ cached = navCache[relName] = refBuilder.createMockTargetRow(holder);
6067
+ }
5982
6068
  }
5983
6069
  return cached;
5984
6070
  },
@@ -6158,16 +6244,28 @@ class CollectionQueryBuilder {
6158
6244
  else {
6159
6245
  // Reference navigation
6160
6246
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
6247
+ const holder = {};
6161
6248
  descriptors[relName] = {
6162
- get: () => {
6249
+ get() {
6163
6250
  // Don't call build() - it returns schema without relations
6164
6251
  // Instead, pass undefined and let ReferenceQueryBuilder look it up from registry
6252
+ // One mock target row per ITEM row and relation: a selector that reads
6253
+ // `it.product.*` fifteen times used to mint fifteen builders and rows.
6254
+ const slots = this;
6255
+ const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
6256
+ const cachedRow = navCache[relName];
6257
+ if (cachedRow !== undefined) {
6258
+ return cachedRow;
6259
+ }
6260
+ if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
6261
+ return (navCache[relName] = mintReferenceMockRow(holder.prototype));
6262
+ }
6165
6263
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, undefined, // Don't pass schema, force registry lookup
6166
6264
  schemaRegistry, // Pass schema registry for nested resolution
6167
6265
  [], // Empty navigation path - this is the first reference in the chain
6168
6266
  targetTable // Source alias is this collection's target table
6169
6267
  );
6170
- return refBuilder.createMockTargetRow();
6268
+ return (navCache[relName] = refBuilder.createMockTargetRow(holder));
6171
6269
  },
6172
6270
  enumerable: false,
6173
6271
  configurable: true,
@@ -6529,6 +6627,12 @@ class CollectionQueryBuilder {
6529
6627
  }
6530
6628
  // Collect all table aliases referenced in the selection
6531
6629
  const allTableAliases = new Set();
6630
+ // Aliases already joined — kept in step with `joins` so membership is O(1) instead of a
6631
+ // `joins.some(...)` scan per field ref (a wide projection asks this for every field)
6632
+ const joinedAliases = new Set();
6633
+ for (const join of joins) {
6634
+ joinedAliases.add(join.alias);
6635
+ }
6532
6636
  // Helper to collect from a single selection
6533
6637
  const collectFromSelection = (sel) => {
6534
6638
  if (!sel || typeof sel !== 'object') {
@@ -6536,20 +6640,26 @@ class CollectionQueryBuilder {
6536
6640
  }
6537
6641
  // Handle single FieldRef
6538
6642
  if ('__tableAlias' in sel && '__dbColumnName' in sel) {
6539
- this.addNavigationJoinForFieldRef(sel, joins, currentSourceAlias, currentSchema, allTableAliases);
6643
+ this.addNavigationJoinForFieldRef(sel, joins, currentSourceAlias, currentSchema, allTableAliases, joinedAliases);
6540
6644
  return;
6541
6645
  }
6542
6646
  // Handle object with multiple fields
6543
- for (const [_key, value] of Object.entries(sel)) {
6647
+ // Own enumerable keys only (the set Object.entries walks), without allocating the pairs:
6648
+ // a wide collection projection runs this for every field on every build.
6649
+ for (const key in sel) {
6650
+ if (!Object.prototype.hasOwnProperty.call(sel, key)) {
6651
+ continue;
6652
+ }
6653
+ const value = sel[key];
6544
6654
  if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
6545
6655
  // This is a FieldRef with a table alias
6546
- this.addNavigationJoinForFieldRef(value, joins, currentSourceAlias, currentSchema, allTableAliases);
6656
+ this.addNavigationJoinForFieldRef(value, joins, currentSourceAlias, currentSchema, allTableAliases, joinedAliases);
6547
6657
  }
6548
6658
  else if (value instanceof conditions_1.SqlFragment) {
6549
6659
  // SqlFragment may contain navigation property references
6550
6660
  const fieldRefs = value.getFieldRefs();
6551
6661
  for (const fieldRef of fieldRefs) {
6552
- this.addNavigationJoinForFieldRef(fieldRef, joins, currentSourceAlias, currentSchema, allTableAliases);
6662
+ this.addNavigationJoinForFieldRef(fieldRef, joins, currentSourceAlias, currentSchema, allTableAliases, joinedAliases);
6553
6663
  }
6554
6664
  }
6555
6665
  else if (value instanceof CollectionQueryBuilder) {
@@ -6560,8 +6670,9 @@ class CollectionQueryBuilder {
6560
6670
  // in its own FROM for the correlation to resolve.
6561
6671
  const nestedPath = value.getNavigationPath();
6562
6672
  for (const step of nestedPath) {
6563
- if (!joins.some(j => j.alias === step.alias)) {
6673
+ if (!joinedAliases.has(step.alias)) {
6564
6674
  joins.push(step);
6675
+ joinedAliases.add(step.alias);
6565
6676
  }
6566
6677
  }
6567
6678
  }
@@ -6575,14 +6686,14 @@ class CollectionQueryBuilder {
6575
6686
  collectFromSelection(selection);
6576
6687
  // Second pass: resolve all navigation joins by finding the correct path through schemas
6577
6688
  if (allTableAliases.size > 0) {
6578
- this.resolveNavigationJoins(allTableAliases, joins, currentSchema);
6689
+ this.resolveNavigationJoins(allTableAliases, joins, currentSchema, joinedAliases);
6579
6690
  }
6580
6691
  }
6581
6692
  /**
6582
6693
  * Add a navigation JOIN for a FieldRef if it references a related table
6583
6694
  * Handles multi-level navigation by recursively resolving the join chain
6584
6695
  */
6585
- addNavigationJoinForFieldRef(fieldRef, joins, sourceAlias, sourceSchema, allTableAliases) {
6696
+ addNavigationJoinForFieldRef(fieldRef, joins, sourceAlias, sourceSchema, allTableAliases, joinedAliases) {
6586
6697
  if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef)) {
6587
6698
  return;
6588
6699
  }
@@ -6611,23 +6722,24 @@ class CollectionQueryBuilder {
6611
6722
  // Collect this table alias for later resolution
6612
6723
  allTableAliases.add(tableAlias);
6613
6724
  // Check if we already have this join
6614
- if (joins.some(j => j.alias === tableAlias)) {
6725
+ if (joinedAliases !== undefined ? joinedAliases.has(tableAlias) : joins.some(j => j.alias === tableAlias)) {
6615
6726
  return;
6616
6727
  }
6617
6728
  // Find the relation in the current schema
6618
6729
  const relation = sourceSchema.relations?.[tableAlias];
6619
6730
  if (relation && relation.type === 'one') {
6620
- this.addNavigationJoin(tableAlias, relation, joins, sourceAlias);
6731
+ this.addNavigationJoin(tableAlias, relation, joins, sourceAlias, joinedAliases);
6621
6732
  }
6622
6733
  }
6623
6734
  /**
6624
6735
  * Add a navigation join and return the target schema
6625
6736
  */
6626
- addNavigationJoin(alias, relation, joins, sourceAlias) {
6737
+ addNavigationJoin(alias, relation, joins, sourceAlias, joinedAliases) {
6627
6738
  // Check if already added
6628
- if (joins.some(j => j.alias === alias)) {
6739
+ if (joinedAliases !== undefined ? joinedAliases.has(alias) : joins.some(j => j.alias === alias)) {
6629
6740
  return undefined;
6630
6741
  }
6742
+ joinedAliases?.add(alias);
6631
6743
  // Get the target table schema
6632
6744
  let targetSchema;
6633
6745
  let targetSchemaName;
@@ -6657,8 +6769,28 @@ class CollectionQueryBuilder {
6657
6769
  * Resolve all navigation joins by finding the correct path through the schema graph
6658
6770
  * This handles multi-level navigation like task.level.createdBy
6659
6771
  */
6660
- resolveNavigationJoins(allTableAliases, joins, startSchema) {
6772
+ resolveNavigationJoins(allTableAliases, joins, startSchema, joinedAliases) {
6773
+ // Aliases already joined, kept in step with `joins` (O(1) membership; the caller may hand
6774
+ // in the set it maintained while collecting the field refs)
6775
+ if (joinedAliases === undefined) {
6776
+ joinedAliases = new Set();
6777
+ for (const join of joins) {
6778
+ joinedAliases.add(join.alias);
6779
+ }
6780
+ }
6661
6781
  // Keep resolving until we've resolved all aliases or can't make progress
6782
+ // Fast path: every referenced alias is already joined (direct relations of the target table
6783
+ // were added while the field refs were collected) — the loop below would only mark them.
6784
+ let allJoined = true;
6785
+ for (const alias of allTableAliases) {
6786
+ if (!joinedAliases.has(alias)) {
6787
+ allJoined = false;
6788
+ break;
6789
+ }
6790
+ }
6791
+ if (allJoined) {
6792
+ return;
6793
+ }
6662
6794
  let resolved = new Set();
6663
6795
  let lastResolvedCount = -1;
6664
6796
  let maxIterations = 100; // Prevent infinite loops
@@ -6696,7 +6828,7 @@ class CollectionQueryBuilder {
6696
6828
  if (resolved.has(alias)) {
6697
6829
  continue;
6698
6830
  }
6699
- if (joins.some(j => j.alias === alias)) {
6831
+ if (joinedAliases.has(alias)) {
6700
6832
  resolved.add(alias);
6701
6833
  continue;
6702
6834
  }
@@ -6706,7 +6838,7 @@ class CollectionQueryBuilder {
6706
6838
  if (!relation || relation.type !== 'one') {
6707
6839
  continue;
6708
6840
  }
6709
- const targetSchema = this.addNavigationJoin(alias, relation, joins, schemaAlias);
6841
+ const targetSchema = this.addNavigationJoin(alias, relation, joins, schemaAlias, joinedAliases);
6710
6842
  if (targetSchema) {
6711
6843
  // Keep the anchor map current so the alias we just joined can serve as the
6712
6844
  // source for further direct lookups in the next fixpoint round
@@ -6731,8 +6863,8 @@ class CollectionQueryBuilder {
6731
6863
  }
6732
6864
  // Add all intermediate joins
6733
6865
  for (const step of path) {
6734
- if (!joins.some(j => j.alias === step.alias)) {
6735
- const stepSchema = this.addNavigationJoin(step.alias, step.relation, joins, step.sourceAlias);
6866
+ if (!joinedAliases.has(step.alias)) {
6867
+ const stepSchema = this.addNavigationJoin(step.alias, step.relation, joins, step.sourceAlias, joinedAliases);
6736
6868
  if (stepSchema) {
6737
6869
  joinedSchemas.set(step.alias, stepSchema);
6738
6870
  }
@@ -6753,9 +6885,12 @@ class CollectionQueryBuilder {
6753
6885
  // Memoised per registry — the BFS below depends only on the target alias and the ORDERED
6754
6886
  // joined alias→table pairs (order decides which of several equal-length paths wins), so
6755
6887
  // the same query shape resolves to the same path on every build. See NavigationPathCache.
6756
- const signature = `${targetAlias}|${[...joinedSchemas]
6757
- .map(([alias, schema]) => `${alias}:${schema.schema ?? ''}.${schema.name}`)
6758
- .join(',')}`;
6888
+ // Built by concatenation over the map (no spread / map / join allocations — this runs on every
6889
+ // build of every projection that reaches an alias through a second hop)
6890
+ let signature = targetAlias + '|';
6891
+ for (const [alias, schema] of joinedSchemas) {
6892
+ signature += alias + ':' + (schema.schema ?? '') + '.' + schema.name + ',';
6893
+ }
6759
6894
  return navigation_path_cache_1.NavigationPathCache.getOrBuild(this.schemaRegistry, signature, () => this.computeNavigationPath(targetAlias, joinedSchemas));
6760
6895
  }
6761
6896
  /** The uncached BFS behind {@link findNavigationPath}. */
@@ -6842,6 +6977,7 @@ class CollectionQueryBuilder {
6842
6977
  val.constructor === Object;
6843
6978
  };
6844
6979
  // Helper function to recursively process fields and build SelectedField structures
6980
+ const collectionMarkerAlias = `__collection_${this.targetTable}__`;
6845
6981
  const processField = (alias, field) => {
6846
6982
  if (field instanceof conditions_1.SqlFragment) {
6847
6983
  // SQL Fragment - build the SQL expression
@@ -6909,6 +7045,7 @@ class CollectionQueryBuilder {
6909
7045
  nestedCteJoin: {
6910
7046
  cteName: nestedResult.tableName,
6911
7047
  joinClause: nestedJoinClause,
7048
+ memoId: nestedResult.memoId,
6912
7049
  },
6913
7050
  // Store nested collection info for recursive mapper transformation
6914
7051
  nestedCollectionInfo: {
@@ -6951,11 +7088,10 @@ class CollectionQueryBuilder {
6951
7088
  const sourceTable = field.__sourceTable; // Actual table name for schema lookup
6952
7089
  // If tableAlias differs from the target table (or its collection marker), it's a navigation property reference
6953
7090
  // The collection marker is `__collection_tableName__` and should be treated as the target table
6954
- const collectionMarker = `__collection_${this.targetTable}__`;
6955
- if (tableAlias && tableAlias !== this.targetTable && tableAlias !== collectionMarker) {
7091
+ if (tableAlias && tableAlias !== this.targetTable && tableAlias !== collectionMarkerAlias) {
6956
7092
  return { alias, expression: `"${tableAlias}"."${dbColumnName}"`, propertyName: fieldName, sourceTable };
6957
7093
  }
6958
- return { alias, expression: `"${dbColumnName}"`, propertyName: fieldName };
7094
+ return { alias, expression: `"${dbColumnName}"`, propertyName: fieldName, isColumn: true };
6959
7095
  }
6960
7096
  else if (typeof field === 'string') {
6961
7097
  // Simple string reference (for backward compatibility)
@@ -6964,8 +7100,10 @@ class CollectionQueryBuilder {
6964
7100
  else if (isPlainObject(field)) {
6965
7101
  // Nested object - recursively process its fields
6966
7102
  const nestedFields = [];
6967
- for (const [nestedAlias, nestedField] of Object.entries(field)) {
6968
- nestedFields.push(processField(nestedAlias, nestedField));
7103
+ for (const nestedAlias in field) {
7104
+ if (Object.prototype.hasOwnProperty.call(field, nestedAlias)) {
7105
+ nestedFields.push(processField(nestedAlias, field[nestedAlias]));
7106
+ }
6969
7107
  }
6970
7108
  return { alias, nested: nestedFields };
6971
7109
  }
@@ -6995,6 +7133,7 @@ class CollectionQueryBuilder {
6995
7133
  alias: dbColumnName,
6996
7134
  expression: `"${dbColumnName}"`,
6997
7135
  propertyName: fieldName,
7136
+ isColumn: true,
6998
7137
  });
6999
7138
  }
7000
7139
  else if (selectedFields instanceof CollectionQueryBuilder || selectedFields instanceof conditions_1.SqlFragment) {
@@ -7003,8 +7142,11 @@ class CollectionQueryBuilder {
7003
7142
  }
7004
7143
  else {
7005
7144
  // Object selection - extract each field (with support for nested objects)
7006
- for (const [alias, field] of Object.entries(selectedFields)) {
7007
- selectedFieldConfigs.push(processField(alias, field));
7145
+ // Own enumerable keys, without allocating the entry pairs (this runs per field per build)
7146
+ for (const alias in selectedFields) {
7147
+ if (Object.prototype.hasOwnProperty.call(selectedFields, alias)) {
7148
+ selectedFieldConfigs.push(processField(alias, selectedFields[alias]));
7149
+ }
7008
7150
  }
7009
7151
  }
7010
7152
  }
@@ -7018,6 +7160,7 @@ class CollectionQueryBuilder {
7018
7160
  alias: colName,
7019
7161
  expression: `"${dbColumnName}"`,
7020
7162
  propertyName: colName, // Same as alias when selecting all fields
7163
+ isColumn: true,
7021
7164
  });
7022
7165
  }
7023
7166
  }
@@ -7055,13 +7198,7 @@ class CollectionQueryBuilder {
7055
7198
  let orderByClauseAlias;
7056
7199
  if (this.orderByFields.length > 0) {
7057
7200
  // Build reverse lookup: db column name -> property name
7058
- let dbToPropertyMap = null;
7059
- if (this.targetTableSchema) {
7060
- dbToPropertyMap = new Map();
7061
- for (const [propName, meta] of getSchemaColumnMeta(this.targetTableSchema)) {
7062
- dbToPropertyMap.set(meta.name, propName);
7063
- }
7064
- }
7201
+ const dbToPropertyMap = this.targetTableSchema ? getDbToPropertyMapForSchema(this.targetTableSchema) : null;
7065
7202
  const orderPartsDb = this.orderByFields.map(({ field, direction }) => {
7066
7203
  // field is already the database column name
7067
7204
  return `"${field}" ${direction}`;
@@ -7149,8 +7286,14 @@ class CollectionQueryBuilder {
7149
7286
  // which must be included in the lateral subquery for correlation
7150
7287
  // Include selectMany joins in both all and selector navigation joins
7151
7288
  // selectMany joins are structural (from flattening) and needed by both CTE and LATERAL
7152
- const allNavigationJoins = [...this.navigationPath, ...this.selectManyJoins, ...navigationJoins];
7153
- const allSelectorJoins = [...this.selectManyJoins, ...navigationJoins];
7289
+ // (the common case — no navigation path, no selectMany — reuses the detected array instead of
7290
+ // spreading it twice; strategies only read these lists)
7291
+ const allNavigationJoins = this.navigationPath.length === 0 && this.selectManyJoins.length === 0
7292
+ ? navigationJoins
7293
+ : [...this.navigationPath, ...this.selectManyJoins, ...navigationJoins];
7294
+ const allSelectorJoins = this.selectManyJoins.length === 0
7295
+ ? navigationJoins
7296
+ : [...this.selectManyJoins, ...navigationJoins];
7154
7297
  // Step 6: Build CollectionAggregationConfig object
7155
7298
  const config = {
7156
7299
  relationName: this.relationName,
@@ -7215,6 +7358,7 @@ class CollectionQueryBuilder {
7215
7358
  joinClause: result.joinClause,
7216
7359
  selectExpression: result.selectExpression,
7217
7360
  tableName: result.tableName,
7361
+ memoId: result.memoId,
7218
7362
  };
7219
7363
  }
7220
7364
  }