linkgress-orm 0.4.75 → 0.4.78

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 (44) 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 +9 -0
  6. package/dist/entity/db-context.d.ts.map +1 -1
  7. package/dist/entity/db-context.js +76 -65
  8. package/dist/entity/db-context.js.map +1 -1
  9. package/dist/entity/entity-builder.d.ts.map +1 -1
  10. package/dist/entity/entity-builder.js +12 -1
  11. package/dist/entity/entity-builder.js.map +1 -1
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +6 -4
  15. package/dist/index.js.map +1 -1
  16. package/dist/query/collection-strategy.interface.d.ts +14 -0
  17. package/dist/query/collection-strategy.interface.d.ts.map +1 -1
  18. package/dist/query/conditions.d.ts +36 -0
  19. package/dist/query/conditions.d.ts.map +1 -1
  20. package/dist/query/conditions.js +78 -10
  21. package/dist/query/conditions.js.map +1 -1
  22. package/dist/query/lateral-sql-cache.d.ts +53 -0
  23. package/dist/query/lateral-sql-cache.d.ts.map +1 -0
  24. package/dist/query/lateral-sql-cache.js +63 -0
  25. package/dist/query/lateral-sql-cache.js.map +1 -0
  26. package/dist/query/query-builder.d.ts +18 -1
  27. package/dist/query/query-builder.d.ts.map +1 -1
  28. package/dist/query/query-builder.js +209 -64
  29. package/dist/query/query-builder.js.map +1 -1
  30. package/dist/query/query-utils.d.ts +1 -0
  31. package/dist/query/query-utils.d.ts.map +1 -1
  32. package/dist/query/query-utils.js +20 -0
  33. package/dist/query/query-utils.js.map +1 -1
  34. package/dist/query/strategies/cte-collection-strategy.d.ts.map +1 -1
  35. package/dist/query/strategies/cte-collection-strategy.js +105 -104
  36. package/dist/query/strategies/cte-collection-strategy.js.map +1 -1
  37. package/dist/query/strategies/lateral-collection-strategy.d.ts +28 -3
  38. package/dist/query/strategies/lateral-collection-strategy.d.ts.map +1 -1
  39. package/dist/query/strategies/lateral-collection-strategy.js +227 -230
  40. package/dist/query/strategies/lateral-collection-strategy.js.map +1 -1
  41. package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
  42. package/dist/query/strategies/temptable-collection-strategy.js +85 -84
  43. package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
  44. package/package.json +80 -80
@@ -4,12 +4,14 @@ 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");
11
+ const query_utils_1 = require("./query-utils");
10
12
  const prepared_query_1 = require("./prepared-query");
11
13
  const db_context_1 = require("../entity/db-context");
12
- const query_utils_1 = require("./query-utils");
14
+ const query_utils_2 = require("./query-utils");
13
15
  const subquery_1 = require("./subquery");
14
16
  const grouped_query_1 = require("./grouped-query");
15
17
  const cte_builder_1 = require("./cte-builder");
@@ -80,6 +82,23 @@ function getSchemaColumnMeta(schema) {
80
82
  }
81
83
  return meta;
82
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
+ }
83
102
  /**
84
103
  * Mock-row descriptor cache for {@link ReferenceQueryBuilder.createMockTargetRow}.
85
104
  *
@@ -102,6 +121,13 @@ const MOCK_ROW_CHAIN_ID = Symbol('linkgressMockChainId');
102
121
  const navigationPathSignature = (path) => path
103
122
  .map(step => `${step.alias}:${step.targetTable}:${(step.foreignKeys ?? []).join('+')}:${(step.matches ?? []).join('+')}:${step.isMandatory ? 1 : 0}:${step.sourceAlias ?? ''}`)
104
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
+ };
105
131
  /**
106
132
  * Whether `value` is a mock row minted by the query builders (`createMockTargetRow`,
107
133
  * `SelectQueryBuilder._createMockRow`, `CollectionQueryBuilder.createMockItem`). Every
@@ -469,13 +495,25 @@ class QueryBuilder {
469
495
  // Single reference navigation (many-to-one, one-to-one)
470
496
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
471
497
  // with circular relations like User->Posts->User)
498
+ const holder = {};
472
499
  descriptors[relName] = {
473
- 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
+ }
474
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
475
513
  [], // Empty navigation path for first level navigation
476
514
  sourceTableName // Pass source table name for lateral join correlation
477
515
  );
478
- return refBuilder.createMockTargetRow();
516
+ return (navCache[relName] = refBuilder.createMockTargetRow(holder));
479
517
  },
480
518
  enumerable: false,
481
519
  configurable: true,
@@ -631,12 +669,20 @@ class QueryBuilder {
631
669
  // Single reference navigation
632
670
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
633
671
  Object.defineProperty(mock, relName, {
634
- 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
+ }
635
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
636
682
  [], // Empty navigation path for first level navigation
637
683
  schema.name // Pass source table name for lateral join correlation
638
684
  );
639
- return refBuilder.createMockTargetRow();
685
+ return (navCache[relName] = refBuilder.createMockTargetRow());
640
686
  },
641
687
  enumerable: false,
642
688
  configurable: true,
@@ -662,7 +708,7 @@ class QueryBuilder {
662
708
  orderBy(selector) {
663
709
  const mockRow = this._createMockRow();
664
710
  const result = (0, exports.materializeMockSelection)(selector(mockRow));
665
- (0, query_utils_1.parseOrderBy)(result, this.orderByFields);
711
+ (0, query_utils_2.parseOrderBy)(result, this.orderByFields);
666
712
  return this;
667
713
  }
668
714
  }
@@ -941,7 +987,7 @@ class SelectQueryBuilder {
941
987
  const result = selector(fieldRefProxy);
942
988
  // Clear previous orderBy - last one takes precedence
943
989
  this.orderByFields = [];
944
- (0, query_utils_1.parseOrderBy)(result, this.orderByFields);
990
+ (0, query_utils_2.parseOrderBy)(result, this.orderByFields);
945
991
  return this;
946
992
  }
947
993
  /**
@@ -1187,12 +1233,20 @@ class SelectQueryBuilder {
1187
1233
  // Single reference navigation
1188
1234
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
1189
1235
  Object.defineProperty(mock, relName, {
1190
- 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
+ }
1191
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
1192
1246
  [], // Empty navigation path for first level navigation
1193
1247
  schema.name // Pass source table name for lateral join correlation
1194
1248
  );
1195
- return refBuilder.createMockTargetRow();
1249
+ return (navCache[relName] = refBuilder.createMockTargetRow());
1196
1250
  },
1197
1251
  enumerable: false,
1198
1252
  configurable: true,
@@ -3645,15 +3699,27 @@ ${joinClauses.join('\n')}`;
3645
3699
  else {
3646
3700
  // For single reference (many-to-one), create a ReferenceQueryBuilder
3647
3701
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
3702
+ const holder = {};
3648
3703
  descriptors[relName] = {
3649
- 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
+ }
3650
3716
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, // Pass the target schema directly
3651
3717
  schemaRegistry, // Pass schema registry for nested resolution
3652
3718
  [], // Empty navigation path for first level navigation
3653
3719
  sourceTableName // Pass source table name for lateral join correlation
3654
3720
  );
3655
3721
  // Return a mock object that exposes the target table's columns
3656
- return refBuilder.createMockTargetRow();
3722
+ return (navCache[relName] = refBuilder.createMockTargetRow(holder));
3657
3723
  },
3658
3724
  enumerable: false,
3659
3725
  configurable: true,
@@ -4203,7 +4269,11 @@ ${joinClauses.join('\n')}`;
4203
4269
  }
4204
4270
  else {
4205
4271
  // Process selection object properties
4206
- 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];
4207
4277
  if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
4208
4278
  // Handle collection - delegate to strategy pattern via buildCTE
4209
4279
  // The strategy handles CTE/LATERAL specifics and returns necessary info
@@ -4677,7 +4747,11 @@ ${joinClauses.join('\n')}`;
4677
4747
  // - collections hit an explicit `continue` and were never emitted.
4678
4748
  // Both regressions are now covered by union-nested-select.test.ts and
4679
4749
  // union-collection-nav.test.ts.
4680
- 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];
4681
4755
  if (value instanceof CollectionQueryBuilder || (value && typeof value === 'object' && '__collectionResult' in value)) {
4682
4756
  // Collection projection inside a UNION leg — delegate to the
4683
4757
  // collection strategy (LATERAL by default) and emit per-row
@@ -5827,7 +5901,7 @@ class ReferenceQueryBuilder {
5827
5901
  * Create a mock object that exposes the target table's columns
5828
5902
  * This allows accessing related fields like: p.user.username
5829
5903
  */
5830
- createMockTargetRow() {
5904
+ createMockTargetRow(holder) {
5831
5905
  if (this.targetTableSchema) {
5832
5906
  // Prototype-level cache — see MockRowCache's doc. Everything the getters close over
5833
5907
  // is fully determined by (target schema object identity, relationName, sourceAlias,
@@ -5843,11 +5917,18 @@ class ReferenceQueryBuilder {
5843
5917
  // Consumers must not probe these rows with OWN-property APIs (`Object.keys`,
5844
5918
  // `getOwnPropertyNames`, `getOwnPropertyDescriptor` on the row itself) — the getters
5845
5919
  // are inherited. Use `isReferenceMockRow` / `findFirstGetterKey` instead.
5846
- const prototype = mock_row_cache_1.MockRowCache.getOrBuild(`${this.targetTable}|${this.relationName}|${this.sourceAlias ?? ''}|${navigationPathSignature(this.navigationPath)}`, () => Object.defineProperties({}, this.buildMockRowDescriptors()));
5847
- const mock = Object.create(prototype);
5848
- mock[MOCK_ROW_FIELD_REFS] = {};
5849
- mock[MOCK_ROW_NAV_CACHE] = {};
5850
- 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);
5851
5932
  }
5852
5933
  else {
5853
5934
  // Fallback: use the shared nested proxy that supports deep property access
@@ -5966,18 +6047,24 @@ class ReferenceQueryBuilder {
5966
6047
  // Reference navigation
5967
6048
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
5968
6049
  // with circular relations like User->Posts->User)
6050
+ const holder = {};
5969
6051
  descriptors[relName] = {
5970
6052
  get() {
5971
6053
  const slots = this;
5972
6054
  const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
5973
6055
  let cached = navCache[relName];
5974
6056
  if (cached === undefined) {
5975
- const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
5976
- schemaRegistry, // Pass schema registry for nested resolution
5977
- extendedNavPath, // Pass navigation path for nested collections
5978
- parentSourceAlias ? tableAlias : '' // Only set source if tracking path
5979
- );
5980
- 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
+ }
5981
6068
  }
5982
6069
  return cached;
5983
6070
  },
@@ -6157,16 +6244,28 @@ class CollectionQueryBuilder {
6157
6244
  else {
6158
6245
  // Reference navigation
6159
6246
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
6247
+ const holder = {};
6160
6248
  descriptors[relName] = {
6161
- get: () => {
6249
+ get() {
6162
6250
  // Don't call build() - it returns schema without relations
6163
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
+ }
6164
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
6165
6264
  schemaRegistry, // Pass schema registry for nested resolution
6166
6265
  [], // Empty navigation path - this is the first reference in the chain
6167
6266
  targetTable // Source alias is this collection's target table
6168
6267
  );
6169
- return refBuilder.createMockTargetRow();
6268
+ return (navCache[relName] = refBuilder.createMockTargetRow(holder));
6170
6269
  },
6171
6270
  enumerable: false,
6172
6271
  configurable: true,
@@ -6193,7 +6292,7 @@ class CollectionQueryBuilder {
6193
6292
  orderBy(selector) {
6194
6293
  const mockItem = this.createMockItem();
6195
6294
  const result = selector(mockItem);
6196
- (0, query_utils_1.parseOrderBy)(result, this.orderByFields);
6295
+ (0, query_utils_2.parseOrderBy)(result, this.orderByFields);
6197
6296
  return this;
6198
6297
  }
6199
6298
  /**
@@ -6508,7 +6607,7 @@ class CollectionQueryBuilder {
6508
6607
  context.placeholders = placeholders;
6509
6608
  }
6510
6609
  // Rewrite collection marker aliases to actual table names
6511
- const markerPattern = new RegExp(`"__collection_${targetTable}__"`, 'g');
6610
+ const markerPattern = (0, query_utils_1.collectionMarkerPattern)(targetTable, false);
6512
6611
  const rewrittenCondSql = condSql.replace(markerPattern, `"${targetTable}"`);
6513
6612
  whereSQL += ` AND ${rewrittenCondSql}`;
6514
6613
  }
@@ -6528,6 +6627,12 @@ class CollectionQueryBuilder {
6528
6627
  }
6529
6628
  // Collect all table aliases referenced in the selection
6530
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
+ }
6531
6636
  // Helper to collect from a single selection
6532
6637
  const collectFromSelection = (sel) => {
6533
6638
  if (!sel || typeof sel !== 'object') {
@@ -6535,20 +6640,26 @@ class CollectionQueryBuilder {
6535
6640
  }
6536
6641
  // Handle single FieldRef
6537
6642
  if ('__tableAlias' in sel && '__dbColumnName' in sel) {
6538
- this.addNavigationJoinForFieldRef(sel, joins, currentSourceAlias, currentSchema, allTableAliases);
6643
+ this.addNavigationJoinForFieldRef(sel, joins, currentSourceAlias, currentSchema, allTableAliases, joinedAliases);
6539
6644
  return;
6540
6645
  }
6541
6646
  // Handle object with multiple fields
6542
- 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];
6543
6654
  if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
6544
6655
  // This is a FieldRef with a table alias
6545
- this.addNavigationJoinForFieldRef(value, joins, currentSourceAlias, currentSchema, allTableAliases);
6656
+ this.addNavigationJoinForFieldRef(value, joins, currentSourceAlias, currentSchema, allTableAliases, joinedAliases);
6546
6657
  }
6547
6658
  else if (value instanceof conditions_1.SqlFragment) {
6548
6659
  // SqlFragment may contain navigation property references
6549
6660
  const fieldRefs = value.getFieldRefs();
6550
6661
  for (const fieldRef of fieldRefs) {
6551
- this.addNavigationJoinForFieldRef(fieldRef, joins, currentSourceAlias, currentSchema, allTableAliases);
6662
+ this.addNavigationJoinForFieldRef(fieldRef, joins, currentSourceAlias, currentSchema, allTableAliases, joinedAliases);
6552
6663
  }
6553
6664
  }
6554
6665
  else if (value instanceof CollectionQueryBuilder) {
@@ -6559,8 +6670,9 @@ class CollectionQueryBuilder {
6559
6670
  // in its own FROM for the correlation to resolve.
6560
6671
  const nestedPath = value.getNavigationPath();
6561
6672
  for (const step of nestedPath) {
6562
- if (!joins.some(j => j.alias === step.alias)) {
6673
+ if (!joinedAliases.has(step.alias)) {
6563
6674
  joins.push(step);
6675
+ joinedAliases.add(step.alias);
6564
6676
  }
6565
6677
  }
6566
6678
  }
@@ -6574,14 +6686,14 @@ class CollectionQueryBuilder {
6574
6686
  collectFromSelection(selection);
6575
6687
  // Second pass: resolve all navigation joins by finding the correct path through schemas
6576
6688
  if (allTableAliases.size > 0) {
6577
- this.resolveNavigationJoins(allTableAliases, joins, currentSchema);
6689
+ this.resolveNavigationJoins(allTableAliases, joins, currentSchema, joinedAliases);
6578
6690
  }
6579
6691
  }
6580
6692
  /**
6581
6693
  * Add a navigation JOIN for a FieldRef if it references a related table
6582
6694
  * Handles multi-level navigation by recursively resolving the join chain
6583
6695
  */
6584
- addNavigationJoinForFieldRef(fieldRef, joins, sourceAlias, sourceSchema, allTableAliases) {
6696
+ addNavigationJoinForFieldRef(fieldRef, joins, sourceAlias, sourceSchema, allTableAliases, joinedAliases) {
6585
6697
  if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef)) {
6586
6698
  return;
6587
6699
  }
@@ -6610,23 +6722,24 @@ class CollectionQueryBuilder {
6610
6722
  // Collect this table alias for later resolution
6611
6723
  allTableAliases.add(tableAlias);
6612
6724
  // Check if we already have this join
6613
- if (joins.some(j => j.alias === tableAlias)) {
6725
+ if (joinedAliases !== undefined ? joinedAliases.has(tableAlias) : joins.some(j => j.alias === tableAlias)) {
6614
6726
  return;
6615
6727
  }
6616
6728
  // Find the relation in the current schema
6617
6729
  const relation = sourceSchema.relations?.[tableAlias];
6618
6730
  if (relation && relation.type === 'one') {
6619
- this.addNavigationJoin(tableAlias, relation, joins, sourceAlias);
6731
+ this.addNavigationJoin(tableAlias, relation, joins, sourceAlias, joinedAliases);
6620
6732
  }
6621
6733
  }
6622
6734
  /**
6623
6735
  * Add a navigation join and return the target schema
6624
6736
  */
6625
- addNavigationJoin(alias, relation, joins, sourceAlias) {
6737
+ addNavigationJoin(alias, relation, joins, sourceAlias, joinedAliases) {
6626
6738
  // Check if already added
6627
- if (joins.some(j => j.alias === alias)) {
6739
+ if (joinedAliases !== undefined ? joinedAliases.has(alias) : joins.some(j => j.alias === alias)) {
6628
6740
  return undefined;
6629
6741
  }
6742
+ joinedAliases?.add(alias);
6630
6743
  // Get the target table schema
6631
6744
  let targetSchema;
6632
6745
  let targetSchemaName;
@@ -6656,8 +6769,28 @@ class CollectionQueryBuilder {
6656
6769
  * Resolve all navigation joins by finding the correct path through the schema graph
6657
6770
  * This handles multi-level navigation like task.level.createdBy
6658
6771
  */
6659
- 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
+ }
6660
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
+ }
6661
6794
  let resolved = new Set();
6662
6795
  let lastResolvedCount = -1;
6663
6796
  let maxIterations = 100; // Prevent infinite loops
@@ -6695,7 +6828,7 @@ class CollectionQueryBuilder {
6695
6828
  if (resolved.has(alias)) {
6696
6829
  continue;
6697
6830
  }
6698
- if (joins.some(j => j.alias === alias)) {
6831
+ if (joinedAliases.has(alias)) {
6699
6832
  resolved.add(alias);
6700
6833
  continue;
6701
6834
  }
@@ -6705,7 +6838,7 @@ class CollectionQueryBuilder {
6705
6838
  if (!relation || relation.type !== 'one') {
6706
6839
  continue;
6707
6840
  }
6708
- const targetSchema = this.addNavigationJoin(alias, relation, joins, schemaAlias);
6841
+ const targetSchema = this.addNavigationJoin(alias, relation, joins, schemaAlias, joinedAliases);
6709
6842
  if (targetSchema) {
6710
6843
  // Keep the anchor map current so the alias we just joined can serve as the
6711
6844
  // source for further direct lookups in the next fixpoint round
@@ -6730,8 +6863,8 @@ class CollectionQueryBuilder {
6730
6863
  }
6731
6864
  // Add all intermediate joins
6732
6865
  for (const step of path) {
6733
- if (!joins.some(j => j.alias === step.alias)) {
6734
- 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);
6735
6868
  if (stepSchema) {
6736
6869
  joinedSchemas.set(step.alias, stepSchema);
6737
6870
  }
@@ -6752,9 +6885,12 @@ class CollectionQueryBuilder {
6752
6885
  // Memoised per registry — the BFS below depends only on the target alias and the ORDERED
6753
6886
  // joined alias→table pairs (order decides which of several equal-length paths wins), so
6754
6887
  // the same query shape resolves to the same path on every build. See NavigationPathCache.
6755
- const signature = `${targetAlias}|${[...joinedSchemas]
6756
- .map(([alias, schema]) => `${alias}:${schema.schema ?? ''}.${schema.name}`)
6757
- .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
+ }
6758
6894
  return navigation_path_cache_1.NavigationPathCache.getOrBuild(this.schemaRegistry, signature, () => this.computeNavigationPath(targetAlias, joinedSchemas));
6759
6895
  }
6760
6896
  /** The uncached BFS behind {@link findNavigationPath}. */
@@ -6841,6 +6977,7 @@ class CollectionQueryBuilder {
6841
6977
  val.constructor === Object;
6842
6978
  };
6843
6979
  // Helper function to recursively process fields and build SelectedField structures
6980
+ const collectionMarkerAlias = `__collection_${this.targetTable}__`;
6844
6981
  const processField = (alias, field) => {
6845
6982
  if (field instanceof conditions_1.SqlFragment) {
6846
6983
  // SQL Fragment - build the SQL expression
@@ -6908,6 +7045,7 @@ class CollectionQueryBuilder {
6908
7045
  nestedCteJoin: {
6909
7046
  cteName: nestedResult.tableName,
6910
7047
  joinClause: nestedJoinClause,
7048
+ memoId: nestedResult.memoId,
6911
7049
  },
6912
7050
  // Store nested collection info for recursive mapper transformation
6913
7051
  nestedCollectionInfo: {
@@ -6950,11 +7088,10 @@ class CollectionQueryBuilder {
6950
7088
  const sourceTable = field.__sourceTable; // Actual table name for schema lookup
6951
7089
  // If tableAlias differs from the target table (or its collection marker), it's a navigation property reference
6952
7090
  // The collection marker is `__collection_tableName__` and should be treated as the target table
6953
- const collectionMarker = `__collection_${this.targetTable}__`;
6954
- if (tableAlias && tableAlias !== this.targetTable && tableAlias !== collectionMarker) {
7091
+ if (tableAlias && tableAlias !== this.targetTable && tableAlias !== collectionMarkerAlias) {
6955
7092
  return { alias, expression: `"${tableAlias}"."${dbColumnName}"`, propertyName: fieldName, sourceTable };
6956
7093
  }
6957
- return { alias, expression: `"${dbColumnName}"`, propertyName: fieldName };
7094
+ return { alias, expression: `"${dbColumnName}"`, propertyName: fieldName, isColumn: true };
6958
7095
  }
6959
7096
  else if (typeof field === 'string') {
6960
7097
  // Simple string reference (for backward compatibility)
@@ -6963,8 +7100,10 @@ class CollectionQueryBuilder {
6963
7100
  else if (isPlainObject(field)) {
6964
7101
  // Nested object - recursively process its fields
6965
7102
  const nestedFields = [];
6966
- for (const [nestedAlias, nestedField] of Object.entries(field)) {
6967
- 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
+ }
6968
7107
  }
6969
7108
  return { alias, nested: nestedFields };
6970
7109
  }
@@ -6994,6 +7133,7 @@ class CollectionQueryBuilder {
6994
7133
  alias: dbColumnName,
6995
7134
  expression: `"${dbColumnName}"`,
6996
7135
  propertyName: fieldName,
7136
+ isColumn: true,
6997
7137
  });
6998
7138
  }
6999
7139
  else if (selectedFields instanceof CollectionQueryBuilder || selectedFields instanceof conditions_1.SqlFragment) {
@@ -7002,8 +7142,11 @@ class CollectionQueryBuilder {
7002
7142
  }
7003
7143
  else {
7004
7144
  // Object selection - extract each field (with support for nested objects)
7005
- for (const [alias, field] of Object.entries(selectedFields)) {
7006
- 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
+ }
7007
7150
  }
7008
7151
  }
7009
7152
  }
@@ -7017,6 +7160,7 @@ class CollectionQueryBuilder {
7017
7160
  alias: colName,
7018
7161
  expression: `"${dbColumnName}"`,
7019
7162
  propertyName: colName, // Same as alias when selecting all fields
7163
+ isColumn: true,
7020
7164
  });
7021
7165
  }
7022
7166
  }
@@ -7054,13 +7198,7 @@ class CollectionQueryBuilder {
7054
7198
  let orderByClauseAlias;
7055
7199
  if (this.orderByFields.length > 0) {
7056
7200
  // Build reverse lookup: db column name -> property name
7057
- let dbToPropertyMap = null;
7058
- if (this.targetTableSchema) {
7059
- dbToPropertyMap = new Map();
7060
- for (const [propName, meta] of getSchemaColumnMeta(this.targetTableSchema)) {
7061
- dbToPropertyMap.set(meta.name, propName);
7062
- }
7063
- }
7201
+ const dbToPropertyMap = this.targetTableSchema ? getDbToPropertyMapForSchema(this.targetTableSchema) : null;
7064
7202
  const orderPartsDb = this.orderByFields.map(({ field, direction }) => {
7065
7203
  // field is already the database column name
7066
7204
  return `"${field}" ${direction}`;
@@ -7148,8 +7286,14 @@ class CollectionQueryBuilder {
7148
7286
  // which must be included in the lateral subquery for correlation
7149
7287
  // Include selectMany joins in both all and selector navigation joins
7150
7288
  // selectMany joins are structural (from flattening) and needed by both CTE and LATERAL
7151
- const allNavigationJoins = [...this.navigationPath, ...this.selectManyJoins, ...navigationJoins];
7152
- 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];
7153
7297
  // Step 6: Build CollectionAggregationConfig object
7154
7298
  const config = {
7155
7299
  relationName: this.relationName,
@@ -7214,6 +7358,7 @@ class CollectionQueryBuilder {
7214
7358
  joinClause: result.joinClause,
7215
7359
  selectExpression: result.selectExpression,
7216
7360
  tableName: result.tableName,
7361
+ memoId: result.memoId,
7217
7362
  };
7218
7363
  }
7219
7364
  }