linkgress-orm 0.4.65 → 0.4.66

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.
@@ -15,6 +15,7 @@ const cte_builder_1 = require("./cte-builder");
15
15
  const collection_strategy_factory_1 = require("./collection-strategy.factory");
16
16
  const union_builder_1 = require("./union-builder");
17
17
  const future_query_1 = require("./future-query");
18
+ const mock_row_cache_1 = require("./mock-row-cache");
18
19
  const join_utils_1 = require("./join-utils");
19
20
  /**
20
21
  * Field type categories for optimized result transformation
@@ -57,6 +58,27 @@ function getRelationEntriesForSchema(schema) {
57
58
  // Fallback: build the array (for schemas that weren't built with the new TableBuilder)
58
59
  return Object.entries(schema.relations);
59
60
  }
61
+ /**
62
+ * Mock-row descriptor cache for {@link ReferenceQueryBuilder.createMockTargetRow}.
63
+ *
64
+ * Building a reference mock row costs O(columns + relations) `Object.defineProperty`
65
+ * calls plus fresh closures per row — and deep selectors (cart → items → product →
66
+ * price → …) rebuild that graph from scratch on EVERY query build. All of it is
67
+ * deterministic in (target schema, relation alias, navigation path), so the property
68
+ * descriptors are built once per signature and reused: each new mock row is a bare
69
+ * object + one `Object.defineProperties` call with the shared descriptor map.
70
+ *
71
+ * Per-instance state (the lazy FieldRef cache and the memoized navigation rows)
72
+ * lives in symbol-keyed slots read by the shared getters through `this`, so sharing
73
+ * descriptors across rows is safe. The navigation-path arrays captured at build
74
+ * time are shared by content — nothing downstream mutates them (they are always
75
+ * spread-copied when extended).
76
+ */
77
+ const MOCK_ROW_FIELD_REFS = Symbol('linkgressMockFieldRefs');
78
+ const MOCK_ROW_NAV_CACHE = Symbol('linkgressMockNavCache');
79
+ const navigationPathSignature = (path) => path
80
+ .map(step => `${step.alias}:${step.targetTable}:${(step.foreignKeys ?? []).join('+')}:${(step.matches ?? []).join('+')}:${step.isMandatory ? 1 : 0}:${step.sourceAlias ?? ''}`)
81
+ .join('>');
60
82
  /**
61
83
  * Performance utility: Get target schema for a relation, using cached version if available
62
84
  */
@@ -3172,11 +3194,11 @@ class SelectQueryBuilder {
3172
3194
  joinClauses.push(collection.joinClause);
3173
3195
  }
3174
3196
  // Build the final CTE query
3175
- const sql = `WITH "__mutation__" AS (
3176
- ${mutationWithReturning}
3177
- )
3178
- SELECT ${selectParts.join(', ')}
3179
- FROM "__mutation__"
3197
+ const sql = `WITH "__mutation__" AS (
3198
+ ${mutationWithReturning}
3199
+ )
3200
+ SELECT ${selectParts.join(', ')}
3201
+ FROM "__mutation__"
3180
3202
  ${joinClauses.join('\n')}`;
3181
3203
  return { sql, params: allParams, nestedPaths };
3182
3204
  }
@@ -5576,120 +5598,162 @@ class ReferenceQueryBuilder {
5576
5598
  */
5577
5599
  createMockTargetRow() {
5578
5600
  if (this.targetTableSchema) {
5579
- const mock = {};
5580
- // Add columns - use pre-computed column name map if available
5581
- const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
5582
- // Performance: Lazy-cache FieldRef objects
5583
- const fieldRefCache = {};
5584
- const tableAlias = this.relationName;
5585
- // Build a mapper lookup for columns (only when needed)
5586
- const columnMappers = {};
5587
- const columnSqlTypes = {};
5588
- for (const [colName, colBuilder] of Object.entries(this.targetTableSchema.columns)) {
5589
- const config = colBuilder.build();
5590
- if (config.mapper) {
5591
- columnMappers[colName] = config.mapper;
5592
- }
5593
- if (config.type) {
5594
- columnSqlTypes[colName] = config.type;
5595
- }
5596
- }
5597
- const sourceTable = this.targetTable; // Actual table name for schema lookup
5598
- // Collect all navigation aliases from the path leading to this reference
5599
- // This is needed for WHERE conditions that use multi-level navigation (e.g., task.level.name)
5600
- const navigationAliases = this.navigationPath.map(nav => nav.alias);
5601
- for (const [colName, dbColumnName] of columnNameMap) {
5602
- const mapper = columnMappers[colName];
5603
- Object.defineProperty(mock, colName, {
5604
- get() {
5605
- let cached = fieldRefCache[colName];
5606
- if (!cached) {
5607
- cached = fieldRefCache[colName] = {
5608
- __fieldName: colName,
5609
- __dbColumnName: dbColumnName,
5610
- __tableAlias: tableAlias, // Alias for SQL generation
5611
- __sourceTable: sourceTable, // Actual table name for mapper lookup
5612
- __mapper: mapper, // Include mapper for toDriver transformation in conditions
5613
- __sqlType: columnSqlTypes[colName], // Column SQL type — lets flag* emit width-exact mask casts
5614
- __navigationAliases: navigationAliases, // All intermediate navigation aliases for JOIN resolution
5615
- };
5616
- }
5617
- return cached;
5618
- },
5619
- enumerable: true,
5620
- configurable: true,
5621
- });
5601
+ // Descriptor-level cache — see MockRowCache's doc. Everything the descriptors close
5602
+ // over is fully determined by (target schema object identity, relationName,
5603
+ // sourceAlias, navigation-path content), so rows with the same signature can share
5604
+ // one prebuilt PropertyDescriptorMap. The cross-row cache is OPT-IN via the static
5605
+ // switch (MockRowCache.setEnabled — the host app flips it from its own config):
5606
+ // with it off, each row gets a FRESH descriptor set (the pre-0.4.66 memory profile
5607
+ // nothing retained beyond the row's lifetime), while the per-row FieldRef and
5608
+ // navigation slots below stay enabled either way (they are row-scoped).
5609
+ const descriptors = mock_row_cache_1.MockRowCache.getOrBuild(`${this.targetTable}|${this.relationName}|${this.sourceAlias ?? ''}|${navigationPathSignature(this.navigationPath)}`, () => this.buildMockRowDescriptors());
5610
+ const mock = {
5611
+ [MOCK_ROW_FIELD_REFS]: {},
5612
+ [MOCK_ROW_NAV_CACHE]: {},
5613
+ };
5614
+ Object.defineProperties(mock, descriptors);
5615
+ return mock;
5616
+ }
5617
+ else {
5618
+ // Fallback: use the shared nested proxy that supports deep property access
5619
+ return createNestedFieldRefProxy(this.relationName);
5620
+ }
5621
+ }
5622
+ /**
5623
+ * Builds the shared property-descriptor map for {@link createMockTargetRow}'s cached path.
5624
+ * The getters read per-row state through `this`-bound symbol slots, so one descriptor
5625
+ * map serves every row of the same signature.
5626
+ */
5627
+ buildMockRowDescriptors() {
5628
+ // Add columns - use pre-computed column name map if available
5629
+ const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
5630
+ const tableAlias = this.relationName;
5631
+ // Build a mapper lookup for columns (only when needed)
5632
+ const columnMappers = {};
5633
+ const columnSqlTypes = {};
5634
+ for (const [colName, colBuilder] of Object.entries(this.targetTableSchema.columns)) {
5635
+ const config = colBuilder.build();
5636
+ if (config.mapper) {
5637
+ columnMappers[colName] = config.mapper;
5622
5638
  }
5623
- // Build extended navigation path for nested collections
5624
- // Only build navigation path if we have a sourceAlias (meaning we're inside a collection's selector)
5625
- // If sourceAlias is empty, we're in the main query and references are joined in the FROM clause
5626
- let extendedNavPath = [];
5627
- if (this.sourceAlias) {
5628
- // Build the current navigation step to include in path for nested collections
5629
- // This represents the join from sourceAlias to this.relationName (this.targetTable)
5630
- const currentNavStep = {
5631
- alias: this.relationName,
5632
- targetTable: this.targetTable,
5633
- foreignKeys: this.foreignKeys,
5634
- matches: this.matches.length > 0 ? this.matches : ['id'], // Default to 'id' if not specified
5635
- isMandatory: this.isMandatory,
5636
- sourceAlias: this.sourceAlias,
5637
- };
5638
- extendedNavPath = [...this.navigationPath, currentNavStep];
5639
+ if (config.type) {
5640
+ columnSqlTypes[colName] = config.type;
5639
5641
  }
5640
- // Add navigation properties (both collections and references)
5641
- if (this.targetTableSchema.relations) {
5642
- for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
5643
- // Try to get target schema from registry (preferred, has full relations) or targetTableBuilder
5644
- let nestedTargetSchema;
5645
- if (this.schemaRegistry) {
5646
- nestedTargetSchema = this.schemaRegistry.get(relConfig.targetTable);
5647
- }
5648
- if (!nestedTargetSchema && relConfig.targetTableBuilder) {
5649
- nestedTargetSchema = relConfig.targetTableBuilder.build();
5642
+ }
5643
+ const sourceTable = this.targetTable; // Actual table name for schema lookup
5644
+ // Collect all navigation aliases from the path leading to this reference
5645
+ // This is needed for WHERE conditions that use multi-level navigation (e.g., task.level.name)
5646
+ const navigationAliases = this.navigationPath.map(nav => nav.alias);
5647
+ const descriptors = {};
5648
+ for (const [colName, dbColumnName] of columnNameMap) {
5649
+ const mapper = columnMappers[colName];
5650
+ descriptors[colName] = {
5651
+ get() {
5652
+ const slots = this;
5653
+ const fieldRefCache = slots[MOCK_ROW_FIELD_REFS] ?? (slots[MOCK_ROW_FIELD_REFS] = {});
5654
+ let cached = fieldRefCache[colName];
5655
+ if (!cached) {
5656
+ cached = fieldRefCache[colName] = {
5657
+ __fieldName: colName,
5658
+ __dbColumnName: dbColumnName,
5659
+ __tableAlias: tableAlias, // Alias for SQL generation
5660
+ __sourceTable: sourceTable, // Actual table name for mapper lookup
5661
+ __mapper: mapper, // Include mapper for toDriver transformation in conditions
5662
+ __sqlType: columnSqlTypes[colName], // Column SQL type — lets flag* emit width-exact mask casts
5663
+ __navigationAliases: navigationAliases, // All intermediate navigation aliases for JOIN resolution
5664
+ };
5650
5665
  }
5651
- if (relConfig.type === 'many') {
5652
- // Collection navigation
5653
- // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
5654
- Object.defineProperty(mock, relName, {
5655
- get: () => {
5666
+ return cached;
5667
+ },
5668
+ enumerable: true,
5669
+ configurable: true,
5670
+ };
5671
+ }
5672
+ // Build extended navigation path for nested collections
5673
+ // Only build navigation path if we have a sourceAlias (meaning we're inside a collection's selector)
5674
+ // If sourceAlias is empty, we're in the main query and references are joined in the FROM clause
5675
+ let extendedNavPath = [];
5676
+ if (this.sourceAlias) {
5677
+ // Build the current navigation step to include in path for nested collections
5678
+ // This represents the join from sourceAlias to this.relationName (this.targetTable)
5679
+ const currentNavStep = {
5680
+ alias: this.relationName,
5681
+ targetTable: this.targetTable,
5682
+ foreignKeys: this.foreignKeys,
5683
+ matches: this.matches.length > 0 ? this.matches : ['id'], // Default to 'id' if not specified
5684
+ isMandatory: this.isMandatory,
5685
+ sourceAlias: this.sourceAlias,
5686
+ };
5687
+ extendedNavPath = [...this.navigationPath, currentNavStep];
5688
+ }
5689
+ // Values captured at descriptor-build time — identical for every row of this
5690
+ // signature (the registry is the process-wide schema registry; `sourceAlias`
5691
+ // determines whether nested references track their join path).
5692
+ const schemaRegistry = this.schemaRegistry;
5693
+ const parentSourceAlias = this.sourceAlias;
5694
+ // Add navigation properties (both collections and references)
5695
+ if (this.targetTableSchema.relations) {
5696
+ for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
5697
+ // Try to get target schema from registry (preferred, has full relations) or targetTableBuilder
5698
+ let nestedTargetSchema;
5699
+ if (this.schemaRegistry) {
5700
+ nestedTargetSchema = this.schemaRegistry.get(relConfig.targetTable);
5701
+ }
5702
+ if (!nestedTargetSchema && relConfig.targetTableBuilder) {
5703
+ nestedTargetSchema = relConfig.targetTableBuilder.build();
5704
+ }
5705
+ if (relConfig.type === 'many') {
5706
+ // Collection navigation
5707
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
5708
+ descriptors[relName] = {
5709
+ get() {
5710
+ const slots = this;
5711
+ const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
5712
+ // Memoize per row: selectors revisit the same navigation repeatedly
5713
+ // (aggregates, predicates), and each fresh visit used to rebuild the
5714
+ // whole sub-graph.
5715
+ let cached = navCache[relName];
5716
+ if (cached === undefined) {
5656
5717
  const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
5657
- return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.relationName, // Use alias (relationName) for correlation in lateral joins
5718
+ cached = navCache[relName] = new CollectionQueryBuilder(relName, relConfig.targetTable, fk, tableAlias, // Use alias (relationName) for correlation in lateral joins
5658
5719
  nestedTargetSchema, // Pass the target schema directly
5659
- this.schemaRegistry, // Pass schema registry for nested resolution
5720
+ schemaRegistry, // Pass schema registry for nested resolution
5660
5721
  extendedNavPath, // Pass navigation path for intermediate joins (empty if main query)
5661
5722
  relConfig.foreignKeys, // Propagate composite FK / literal predicates
5662
5723
  relConfig.matches);
5663
- },
5664
- enumerable: false,
5665
- configurable: true,
5666
- });
5667
- }
5668
- else {
5669
- // Reference navigation
5670
- // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
5671
- // with circular relations like User->Posts->User)
5672
- Object.defineProperty(mock, relName, {
5673
- get: () => {
5724
+ }
5725
+ return cached;
5726
+ },
5727
+ enumerable: false,
5728
+ configurable: true,
5729
+ };
5730
+ }
5731
+ else {
5732
+ // Reference navigation
5733
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
5734
+ // with circular relations like User->Posts->User)
5735
+ descriptors[relName] = {
5736
+ get() {
5737
+ const slots = this;
5738
+ const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
5739
+ let cached = navCache[relName];
5740
+ if (cached === undefined) {
5674
5741
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
5675
- this.schemaRegistry, // Pass schema registry for nested resolution
5742
+ schemaRegistry, // Pass schema registry for nested resolution
5676
5743
  extendedNavPath, // Pass navigation path for nested collections
5677
- this.sourceAlias ? this.relationName : '' // Only set source if tracking path
5744
+ parentSourceAlias ? tableAlias : '' // Only set source if tracking path
5678
5745
  );
5679
- return refBuilder.createMockTargetRow();
5680
- },
5681
- enumerable: false,
5682
- configurable: true,
5683
- });
5684
- }
5746
+ cached = navCache[relName] = refBuilder.createMockTargetRow();
5747
+ }
5748
+ return cached;
5749
+ },
5750
+ enumerable: false,
5751
+ configurable: true,
5752
+ };
5685
5753
  }
5686
5754
  }
5687
- return mock;
5688
- }
5689
- else {
5690
- // Fallback: use the shared nested proxy that supports deep property access
5691
- return createNestedFieldRefProxy(this.relationName);
5692
5755
  }
5756
+ return descriptors;
5693
5757
  }
5694
5758
  }
5695
5759
  exports.ReferenceQueryBuilder = ReferenceQueryBuilder;