linkgress-orm 0.4.67 → 0.4.69

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.
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CollectionQueryBuilder = exports.ReferenceQueryBuilder = exports.SelectQueryBuilder = exports.QueryBuilder = void 0;
3
+ exports.CollectionQueryBuilder = exports.ReferenceQueryBuilder = exports.SelectQueryBuilder = exports.QueryBuilder = exports.materializeMockSelection = void 0;
4
4
  exports.getColumnNameMapForSchema = getColumnNameMapForSchema;
5
5
  exports.getRelationEntriesForSchema = getRelationEntriesForSchema;
6
+ exports.getSchemaColumnMeta = getSchemaColumnMeta;
6
7
  exports.getTargetSchemaForRelation = getTargetSchemaForRelation;
7
8
  exports.createNestedFieldRefProxy = createNestedFieldRefProxy;
8
9
  const conditions_1 = require("./conditions");
@@ -16,6 +17,7 @@ const collection_strategy_factory_1 = require("./collection-strategy.factory");
16
17
  const union_builder_1 = require("./union-builder");
17
18
  const future_query_1 = require("./future-query");
18
19
  const mock_row_cache_1 = require("./mock-row-cache");
20
+ const navigation_path_cache_1 = require("./navigation-path-cache");
19
21
  const join_utils_1 = require("./join-utils");
20
22
  /**
21
23
  * Field type categories for optimized result transformation
@@ -58,6 +60,26 @@ function getRelationEntriesForSchema(schema) {
58
60
  // Fallback: build the array (for schemas that weren't built with the new TableBuilder)
59
61
  return Object.entries(schema.relations);
60
62
  }
63
+ /**
64
+ * Performance utility: per-schema column metadata (mapper + SQL type), computed once per
65
+ * schema object. `_createMockRow` and the collection mock-item builder both used to call
66
+ * `colBuilder.build()` for EVERY column on EVERY query build — pure waste for an immutable
67
+ * schema. WeakMap keyed by schema identity so schemas can be garbage-collected with their
68
+ * DbContext.
69
+ */
70
+ const schemaColumnMetaCache = new WeakMap();
71
+ function getSchemaColumnMeta(schema) {
72
+ let meta = schemaColumnMetaCache.get(schema);
73
+ if (meta == null) {
74
+ meta = new Map();
75
+ for (const [colName, colBuilder] of Object.entries(schema.columns)) {
76
+ const config = colBuilder.build();
77
+ meta.set(colName, { mapper: config.mapper, type: config.type, name: config.name, primaryKey: config.primaryKey });
78
+ }
79
+ schemaColumnMetaCache.set(schema, meta);
80
+ }
81
+ return meta;
82
+ }
61
83
  /**
62
84
  * Mock-row descriptor cache for {@link ReferenceQueryBuilder.createMockTargetRow}.
63
85
  *
@@ -76,14 +98,18 @@ function getRelationEntriesForSchema(schema) {
76
98
  */
77
99
  const MOCK_ROW_FIELD_REFS = Symbol('linkgressMockFieldRefs');
78
100
  const MOCK_ROW_NAV_CACHE = Symbol('linkgressMockNavCache');
101
+ const MOCK_ROW_CHAIN_ID = Symbol('linkgressMockChainId');
79
102
  const navigationPathSignature = (path) => path
80
103
  .map(step => `${step.alias}:${step.targetTable}:${(step.foreignKeys ?? []).join('+')}:${(step.matches ?? []).join('+')}:${step.isMandatory ? 1 : 0}:${step.sourceAlias ?? ''}`)
81
104
  .join('>');
82
105
  /**
83
- * Whether `value` is a reference mock row minted by `ReferenceQueryBuilder.createMockTargetRow`.
84
- * Those rows INHERIT their column/relation getters from a shared prototype (see MockRowCache),
85
- * so `Object.getPrototypeOf(row) !== Object.prototype` and own-property APIs see no columns
86
- * the row's own state slots are the reliable marker.
106
+ * Whether `value` is a mock row minted by the query builders (`createMockTargetRow`,
107
+ * `SelectQueryBuilder._createMockRow`, `CollectionQueryBuilder.createMockItem`). Every
108
+ * such row INHERITS its column/relation getters from a shared prototype (see MockRowCache),
109
+ * so `Object.getPrototypeOf(row) !== Object.prototype` and own-property APIs (`Object.keys`,
110
+ * `{...row}`) see no columns — the row's own state slots are the reliable marker. Callers
111
+ * must treat a mock row like the plain-object mock it replaced (walk it for revivals,
112
+ * read properties directly) but never enumerate it with own-property APIs.
87
113
  */
88
114
  const isReferenceMockRow = (value) => value != null && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, MOCK_ROW_FIELD_REFS);
89
115
  /** `Object.getOwnPropertyDescriptor` that walks the prototype chain (stops before Object.prototype). */
@@ -98,10 +124,32 @@ const findPropertyDescriptor = (value, key) => {
98
124
  }
99
125
  return undefined;
100
126
  };
127
+ /**
128
+ * Materializes a selector RESULT that is itself a mock row (identity selectors like
129
+ * `.select(p => p)` or `(l, r) => l`): such rows inherit their columns from the shared
130
+ * prototype, so own-property walkers (`Object.entries` / `keys` / `values`) would see
131
+ * NOTHING and silently project zero columns. Reading every enumerable column getter
132
+ * once into a plain object restores exactly what the pre-prototype mocks exposed (their
133
+ * own enumerable getters) — non-enumerable relation getters stay out, as before. No-op
134
+ * for every other result shape (FieldRef, SqlFragment, builder, plain object, array).
135
+ */
136
+ const materializeMockSelection = (result) => {
137
+ if (!isReferenceMockRow(result)) {
138
+ return result;
139
+ }
140
+ const out = {};
141
+ for (const key in result) {
142
+ if (findPropertyDescriptor(result, key)?.get != null) {
143
+ out[key] = result[key];
144
+ }
145
+ }
146
+ return out;
147
+ };
148
+ exports.materializeMockSelection = materializeMockSelection;
101
149
  /**
102
150
  * First enumerable key (own or inherited) backed by a getter — the "is this a mock row" probe
103
- * shared by the selection resolvers. Root mocks define their getters as own properties,
104
- * reference mocks inherit them; column getters are enumerable on both, so the first
151
+ * shared by the selection resolvers. All mock rows (root, collection-item, reference)
152
+ * inherit their getters from a shared prototype; column getters are enumerable, so the first
105
153
  * enumerable getter is always a column and `row[key]` yields its FieldRef.
106
154
  */
107
155
  const findFirstGetterKey = (value) => {
@@ -298,37 +346,67 @@ class QueryBuilder {
298
346
  if (this._cachedMockRow) {
299
347
  return this._cachedMockRow;
300
348
  }
301
- const mock = {};
349
+ // Prototype-level cache — same pattern as ReferenceQueryBuilder.createMockTargetRow
350
+ // (see MockRowCache): every getter of a root mock row is fully determined by the
351
+ // schema object, so every query over the same table can share ONE prebuilt prototype
352
+ // carrying all column/relation getters; a new row is `Object.create` plus its own
353
+ // state slots — O(1) instead of one property definition per column+relation per
354
+ // query (measurable CPU under load: the root-mock walk was the ORM's largest
355
+ // per-query-build item after 0.4.69). Per-query state (`chainId`, the FieldRef
356
+ // cache) lives in symbol-keyed slots read through `this`; relation getters still
357
+ // construct FRESH builders per access (memoizing would fuse repeated
358
+ // `.where()` chains). Opt-in via the same static switch; OFF = fresh prototype
359
+ // per row (the pre-0.4.70 behaviour). Consumers must not probe mock rows with
360
+ // OWN-property APIs (`Object.keys`, `{...row}`) — see `isReferenceMockRow`.
361
+ const prototype = mock_row_cache_1.MockRowCache.getOrBuild(`select|${this.schema.name}`, () => Object.defineProperties({}, this.buildRootMockDescriptors()));
362
+ const mock = Object.create(prototype);
363
+ mock[MOCK_ROW_FIELD_REFS] = {};
364
+ mock[MOCK_ROW_CHAIN_ID] = this.chainId;
365
+ // Cache the mock for reuse
366
+ this._cachedMockRow = mock;
367
+ return mock;
368
+ }
369
+ /**
370
+ * Builds the shared property-descriptor map for {@link _createMockRow}'s cached path.
371
+ * The getters read per-row state through `this`-bound symbol slots, so one descriptor
372
+ * map serves every root mock row of the same schema. Values captured at build time
373
+ * (column mappers/types, target schemas, the registry) are schema-constants identical
374
+ * for every row of the signature.
375
+ */
376
+ buildRootMockDescriptors() {
302
377
  const tableAlias = this.schema.name;
303
- const chainId = this.chainId;
304
378
  // Performance: Use pre-computed column name map if available
305
379
  const columnNameMap = getColumnNameMapForSchema(this.schema);
306
- // Performance: Lazy-cache FieldRef objects - only create when first accessed
307
- const fieldRefCache = {};
308
380
  // Build a mapper lookup for columns (only when needed)
309
381
  const columnMappers = {};
310
382
  const columnSqlTypes = {};
311
- for (const [colName, colBuilder] of Object.entries(this.schema.columns)) {
312
- const config = colBuilder.build();
313
- if (config.mapper) {
314
- columnMappers[colName] = config.mapper;
383
+ // Per-schema mapper/type lookup, computed once — `colBuilder.build()` per column
384
+ // per row was measurable CPU under load (every query build walks every column
385
+ // twice: once here, once in the descriptors). Keyed by schema identity.
386
+ const schemaColumnMeta = getSchemaColumnMeta(this.schema);
387
+ for (const [colName, meta] of schemaColumnMeta) {
388
+ if (meta.mapper) {
389
+ columnMappers[colName] = meta.mapper;
315
390
  }
316
- if (config.type) {
317
- columnSqlTypes[colName] = config.type;
391
+ if (meta.type) {
392
+ columnSqlTypes[colName] = meta.type;
318
393
  }
319
394
  }
395
+ const descriptors = {};
320
396
  // Add columns as FieldRef objects - type-safe with property name and database column name
321
397
  for (const [colName, dbColumnName] of columnNameMap) {
322
398
  const mapper = columnMappers[colName];
323
- Object.defineProperty(mock, colName, {
399
+ descriptors[colName] = {
324
400
  get() {
401
+ const slots = this;
402
+ const fieldRefCache = slots[MOCK_ROW_FIELD_REFS] ?? (slots[MOCK_ROW_FIELD_REFS] = {});
325
403
  let cached = fieldRefCache[colName];
326
404
  if (!cached) {
327
405
  cached = fieldRefCache[colName] = {
328
406
  __fieldName: colName,
329
407
  __dbColumnName: dbColumnName,
330
408
  __tableAlias: tableAlias,
331
- __chainId: chainId,
409
+ __chainId: slots[MOCK_ROW_CHAIN_ID],
332
410
  // Include mapper for toDriver transformation in conditions
333
411
  __mapper: mapper,
334
412
  // Column SQL type — lets flag* emit width-exact mask casts
@@ -339,49 +417,51 @@ class QueryBuilder {
339
417
  },
340
418
  enumerable: true,
341
419
  configurable: true,
342
- });
420
+ };
343
421
  }
344
422
  // Performance: Use pre-computed relation entries and cached schemas
345
423
  const relationEntries = getRelationEntriesForSchema(this.schema);
424
+ // Values captured at descriptor-build time — identical for every row of this
425
+ // signature (the registry is the process-wide schema registry).
426
+ const schemaRegistry = this.schemaRegistry;
427
+ const sourceTableName = this.schema.name;
346
428
  // Add relations (both collections and single references)
347
429
  for (const [relName, relConfig] of relationEntries) {
348
430
  // Performance: Use cached target schema, but prefer registry lookup for full relations
349
- let targetSchema = this.schemaRegistry?.get(relConfig.targetTable);
431
+ let targetSchema = schemaRegistry?.get(relConfig.targetTable);
350
432
  if (!targetSchema) {
351
433
  targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
352
434
  }
353
435
  if (relConfig.type === 'many') {
354
436
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
355
- Object.defineProperty(mock, relName, {
437
+ descriptors[relName] = {
356
438
  get: () => {
357
- return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, this.schemaRegistry, // Pass schema registry for nested navigation resolution
439
+ return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', sourceTableName, targetSchema, schemaRegistry, // Pass schema registry for nested navigation resolution
358
440
  undefined, relConfig.foreignKeys, // Propagate composite FK / literal predicates
359
441
  relConfig.matches);
360
442
  },
361
443
  enumerable: false,
362
444
  configurable: true,
363
- });
445
+ };
364
446
  }
365
447
  else {
366
448
  // Single reference navigation (many-to-one, one-to-one)
367
449
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
368
450
  // with circular relations like User->Posts->User)
369
- Object.defineProperty(mock, relName, {
451
+ descriptors[relName] = {
370
452
  get: () => {
371
- 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
453
+ 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
372
454
  [], // Empty navigation path for first level navigation
373
- this.schema.name // Pass source table name for lateral join correlation
455
+ sourceTableName // Pass source table name for lateral join correlation
374
456
  );
375
457
  return refBuilder.createMockTargetRow();
376
458
  },
377
459
  enumerable: false,
378
460
  configurable: true,
379
- });
461
+ };
380
462
  }
381
463
  }
382
- // Cache the mock for reuse
383
- this._cachedMockRow = mock;
384
- return mock;
464
+ return descriptors;
385
465
  }
386
466
  /**
387
467
  * Add a LEFT JOIN to the query with a selector (supports both tables and subqueries)
@@ -425,7 +505,7 @@ class QueryBuilder {
425
505
  // Create fresh mocks for the selector invocation
426
506
  const freshMockLeft = createLeftMock();
427
507
  const freshMockRight = createRightMock();
428
- return selector(freshMockLeft, freshMockRight);
508
+ return (0, exports.materializeMockSelection)(selector(freshMockLeft, freshMockRight));
429
509
  };
430
510
  return new SelectQueryBuilder(this.schema, this.client, wrappedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, false, // isDistinct defaults to false
431
511
  undefined, // schemaRegistry
@@ -474,7 +554,7 @@ class QueryBuilder {
474
554
  // Create fresh mocks for the selector invocation
475
555
  const freshMockLeft = createLeftMock();
476
556
  const freshMockRight = createRightMock();
477
- return selector(freshMockLeft, freshMockRight);
557
+ return (0, exports.materializeMockSelection)(selector(freshMockLeft, freshMockRight));
478
558
  };
479
559
  return new SelectQueryBuilder(this.schema, this.client, wrappedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, updatedJoins, newJoinCounter, false, // isDistinct defaults to false
480
560
  undefined, // schemaRegistry
@@ -560,7 +640,7 @@ class QueryBuilder {
560
640
  }
561
641
  orderBy(selector) {
562
642
  const mockRow = this._createMockRow();
563
- const result = selector(mockRow);
643
+ const result = (0, exports.materializeMockSelection)(selector(mockRow));
564
644
  (0, query_utils_1.parseOrderBy)(result, this.orderByFields);
565
645
  return this;
566
646
  }
@@ -630,7 +710,7 @@ class SelectQueryBuilder {
630
710
  select(selector) {
631
711
  // Create a composed selector that applies both transformations
632
712
  const composedSelector = (row) => {
633
- const firstResult = this.selector(row);
713
+ const firstResult = (0, exports.materializeMockSelection)(this.selector(row));
634
714
  return selector(firstResult);
635
715
  };
636
716
  return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, this.isDistinct, this.schemaRegistry, this.ctes, this.collectionStrategy, this.chainId);
@@ -678,7 +758,7 @@ class SelectQueryBuilder {
678
758
  const rightAlias = `${rightSchema.name}_${this.joinCounter}`;
679
759
  this.joinCounter = this.joinCounter + 1;
680
760
  const mockRow = this._createMockRow();
681
- const selectedMock = this.selector(mockRow);
761
+ const selectedMock = (0, exports.materializeMockSelection)(this.selector(mockRow));
682
762
  const leftMock = this.createFieldRefProxy(selectedMock, true);
683
763
  const rightMock = this.createMockRowForTable(rightSchema, rightAlias);
684
764
  const onCondition = on(leftMock, rightMock);
@@ -707,7 +787,7 @@ class SelectQueryBuilder {
707
787
  */
708
788
  addCteFilterJoin(type, cte, on, filter) {
709
789
  const mockRow = this._createMockRow();
710
- const selectedMock = this.selector(mockRow);
790
+ const selectedMock = (0, exports.materializeMockSelection)(this.selector(mockRow));
711
791
  const leftMock = this.createFieldRefProxy(selectedMock, true);
712
792
  const rightMock = this.createMockRowForCte(cte);
713
793
  const onCondition = on(leftMock, rightMock);
@@ -734,7 +814,7 @@ class SelectQueryBuilder {
734
814
  where(condition) {
735
815
  const mockRow = this._createMockRow();
736
816
  // Apply the selector to get the selected shape that the user sees in the WHERE condition
737
- const selectedMock = this.selector(mockRow);
817
+ const selectedMock = (0, exports.materializeMockSelection)(this.selector(mockRow));
738
818
  // Wrap in proxy - for WHERE, we preserve original column names
739
819
  const fieldRefProxy = this.createFieldRefProxy(selectedMock, true);
740
820
  const newCondition = condition(fieldRefProxy);
@@ -824,7 +904,7 @@ class SelectQueryBuilder {
824
904
  }
825
905
  orderBy(selector) {
826
906
  const mockRow = this._createMockRow();
827
- const selectedMock = this.selector(mockRow);
907
+ const selectedMock = (0, exports.materializeMockSelection)(this.selector(mockRow));
828
908
  // Wrap selectedMock in a proxy that returns FieldRefs for property access
829
909
  const fieldRefProxy = this.createFieldRefProxy(selectedMock);
830
910
  const result = selector(fieldRefProxy);
@@ -856,7 +936,7 @@ class SelectQueryBuilder {
856
936
  const newJoinCounter = this.joinCounter + 1;
857
937
  // Create mock for the current selection (left side)
858
938
  const mockRow = this._createMockRow();
859
- const mockLeftSelection = this.selector(mockRow);
939
+ const mockLeftSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
860
940
  // Create mock for the subquery result (right side)
861
941
  // For subqueries, we create a mock based on the result type
862
942
  const mockRight = this.createMockRowForSubquery(alias, subquery);
@@ -874,7 +954,7 @@ class SelectQueryBuilder {
874
954
  }];
875
955
  // Create a new selector
876
956
  const composedSelector = (row) => {
877
- const leftResult = this.selector(row);
957
+ const leftResult = (0, exports.materializeMockSelection)(this.selector(row));
878
958
  const freshMockRight = this.createMockRowForSubquery(alias, subquery);
879
959
  return selector(leftResult, freshMockRight);
880
960
  };
@@ -900,7 +980,7 @@ class SelectQueryBuilder {
900
980
  // Create mock for the current selection (left side)
901
981
  // IMPORTANT: We call the selector with the mock row to get a result that contains FieldRef objects
902
982
  const mockRow = this._createMockRow();
903
- const mockLeftSelection = this.selector(mockRow);
983
+ const mockLeftSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
904
984
  // The mockLeftSelection now contains FieldRef objects (with __fieldName, __dbColumnName, __tableAlias)
905
985
  // These FieldRef objects preserve the table context
906
986
  // Create mock for the right table
@@ -918,7 +998,7 @@ class SelectQueryBuilder {
918
998
  }];
919
999
  // Create a new selector that first applies the current selector, then the new selector
920
1000
  const composedSelector = (row) => {
921
- const leftResult = this.selector(row);
1001
+ const leftResult = (0, exports.materializeMockSelection)(this.selector(row));
922
1002
  const freshMockRight = this.createMockRowForTable(rightSchema, rightAlias);
923
1003
  return selector(leftResult, freshMockRight);
924
1004
  };
@@ -931,7 +1011,7 @@ class SelectQueryBuilder {
931
1011
  const newJoinCounter = this.joinCounter + 1;
932
1012
  // Create mock for the current selection (left side)
933
1013
  const mockRow = this._createMockRow();
934
- const mockLeftSelection = this.selector(mockRow);
1014
+ const mockLeftSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
935
1015
  // Create mock for the CTE columns (right side)
936
1016
  const mockRight = this.createMockRowForCte(cte);
937
1017
  // Evaluate the join condition
@@ -947,7 +1027,7 @@ class SelectQueryBuilder {
947
1027
  }];
948
1028
  // Create a new selector
949
1029
  const composedSelector = (row) => {
950
- const leftResult = this.selector(row);
1030
+ const leftResult = (0, exports.materializeMockSelection)(this.selector(row));
951
1031
  const freshMockRight = this.createMockRowForCte(cte);
952
1032
  return selector(leftResult, freshMockRight);
953
1033
  };
@@ -964,7 +1044,7 @@ class SelectQueryBuilder {
964
1044
  const newJoinCounter = this.joinCounter + 1;
965
1045
  // Create mock for the current selection (left side)
966
1046
  const mockRow = this._createMockRow();
967
- const mockLeftSelection = this.selector(mockRow);
1047
+ const mockLeftSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
968
1048
  // Create mock for the subquery result (right side)
969
1049
  const mockRight = this.createMockRowForSubquery(alias, subquery);
970
1050
  // Evaluate the join condition
@@ -981,7 +1061,7 @@ class SelectQueryBuilder {
981
1061
  }];
982
1062
  // Create a new selector
983
1063
  const composedSelector = (row) => {
984
- const leftResult = this.selector(row);
1064
+ const leftResult = (0, exports.materializeMockSelection)(this.selector(row));
985
1065
  const freshMockRight = this.createMockRowForSubquery(alias, subquery);
986
1066
  return selector(leftResult, freshMockRight);
987
1067
  };
@@ -1006,7 +1086,7 @@ class SelectQueryBuilder {
1006
1086
  const newJoinCounter = this.joinCounter + 1;
1007
1087
  // Create mock for the current selection (left side)
1008
1088
  const mockRow = this._createMockRow();
1009
- const mockLeftSelection = this.selector(mockRow);
1089
+ const mockLeftSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
1010
1090
  // Create mock for the right table
1011
1091
  const mockRight = this.createMockRowForTable(rightSchema, rightAlias);
1012
1092
  // Evaluate the join condition
@@ -1021,7 +1101,7 @@ class SelectQueryBuilder {
1021
1101
  }];
1022
1102
  // Create a new selector that first applies the current selector, then the new selector
1023
1103
  const composedSelector = (row) => {
1024
- const leftResult = this.selector(row);
1104
+ const leftResult = (0, exports.materializeMockSelection)(this.selector(row));
1025
1105
  const freshMockRight = this.createMockRowForTable(rightSchema, rightAlias);
1026
1106
  return selector(leftResult, freshMockRight);
1027
1107
  };
@@ -1200,7 +1280,7 @@ class SelectQueryBuilder {
1200
1280
  */
1201
1281
  selectDistinct(selector) {
1202
1282
  const composedSelector = (row) => {
1203
- const firstResult = this.selector(row);
1283
+ const firstResult = (0, exports.materializeMockSelection)(this.selector(row));
1204
1284
  return selector(firstResult);
1205
1285
  };
1206
1286
  return new SelectQueryBuilder(this.schema, this.client, composedSelector, this.whereCond, this.limitValue, this.offsetValue, this.orderByFields, this.executor, this.manualJoins, this.joinCounter, true, // Set isDistinct to true
@@ -1222,13 +1302,13 @@ class SelectQueryBuilder {
1222
1302
  let fieldToAggregate;
1223
1303
  if (selector) {
1224
1304
  const mockRow = this._createMockRow();
1225
- const mockSelection = this.selector(mockRow);
1305
+ const mockSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
1226
1306
  fieldToAggregate = selector(mockSelection);
1227
1307
  }
1228
1308
  else {
1229
1309
  // No selector - use the current selection
1230
1310
  const mockRow = this._createMockRow();
1231
- fieldToAggregate = this.selector(mockRow);
1311
+ fieldToAggregate = (0, exports.materializeMockSelection)(this.selector(mockRow));
1232
1312
  }
1233
1313
  // Build aggregation query
1234
1314
  const { sql, params } = this.buildAggregationQuery('MIN', fieldToAggregate, context);
@@ -1254,13 +1334,13 @@ class SelectQueryBuilder {
1254
1334
  let fieldToAggregate;
1255
1335
  if (selector) {
1256
1336
  const mockRow = this._createMockRow();
1257
- const mockSelection = this.selector(mockRow);
1337
+ const mockSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
1258
1338
  fieldToAggregate = selector(mockSelection);
1259
1339
  }
1260
1340
  else {
1261
1341
  // No selector - use the current selection
1262
1342
  const mockRow = this._createMockRow();
1263
- fieldToAggregate = this.selector(mockRow);
1343
+ fieldToAggregate = (0, exports.materializeMockSelection)(this.selector(mockRow));
1264
1344
  }
1265
1345
  // Build aggregation query
1266
1346
  const { sql, params } = this.buildAggregationQuery('MAX', fieldToAggregate, context);
@@ -1286,13 +1366,13 @@ class SelectQueryBuilder {
1286
1366
  let fieldToAggregate;
1287
1367
  if (selector) {
1288
1368
  const mockRow = this._createMockRow();
1289
- const mockSelection = this.selector(mockRow);
1369
+ const mockSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
1290
1370
  fieldToAggregate = selector(mockSelection);
1291
1371
  }
1292
1372
  else {
1293
1373
  // No selector - use the current selection
1294
1374
  const mockRow = this._createMockRow();
1295
- fieldToAggregate = this.selector(mockRow);
1375
+ fieldToAggregate = (0, exports.materializeMockSelection)(this.selector(mockRow));
1296
1376
  }
1297
1377
  // Build aggregation query
1298
1378
  const { sql, params } = this.buildAggregationQuery('SUM', fieldToAggregate, context);
@@ -1340,7 +1420,7 @@ class SelectQueryBuilder {
1340
1420
  executor: this.executor,
1341
1421
  }));
1342
1422
  const mockRow = tracer.trace('createMockRow', () => this._createMockRow());
1343
- const selectionResult = tracer.trace('evaluateSelector', () => this.selector(mockRow));
1423
+ const selectionResult = tracer.trace('evaluateSelector', () => (0, exports.materializeMockSelection)(this.selector(mockRow)));
1344
1424
  const { sql, params, nestedPaths } = tracer.trace('buildQuery', () => this.buildQuery(selectionResult, context));
1345
1425
  tracer.endPhase();
1346
1426
  tracer.startPhase('queryExecution');
@@ -1448,7 +1528,7 @@ class SelectQueryBuilder {
1448
1528
  hoistedCteNames: context.hoistedCteNames,
1449
1529
  };
1450
1530
  const mockRow = this._createMockRow();
1451
- const selectionResult = this.selector(mockRow);
1531
+ const selectionResult = (0, exports.materializeMockSelection)(this.selector(mockRow));
1452
1532
  // Build query without ORDER BY, LIMIT, OFFSET for union component
1453
1533
  const { sql, nestedPaths } = this.buildQueryCore(selectionResult, queryContext, false);
1454
1534
  // Update context's param counter
@@ -1516,7 +1596,7 @@ class SelectQueryBuilder {
1516
1596
  executor: this.executor,
1517
1597
  };
1518
1598
  const mockRow = this._createMockRow();
1519
- const selectionResult = this.selector(mockRow);
1599
+ const selectionResult = (0, exports.materializeMockSelection)(this.selector(mockRow));
1520
1600
  const { sql, params, nestedPaths } = this.buildQuery(selectionResult, context);
1521
1601
  // Create transform function that captures current state
1522
1602
  const transformFn = (rows) => {
@@ -1563,7 +1643,7 @@ class SelectQueryBuilder {
1563
1643
  executor: this.executor,
1564
1644
  };
1565
1645
  const mockRow = this._createMockRow();
1566
- const selectionResult = this.selector(mockRow);
1646
+ const selectionResult = (0, exports.materializeMockSelection)(this.selector(mockRow));
1567
1647
  const { sql, params, nestedPaths } = this.buildQuery(selectionResult, context);
1568
1648
  // Restore original limit
1569
1649
  this.limitValue = originalLimit;
@@ -1792,7 +1872,7 @@ class SelectQueryBuilder {
1792
1872
  }));
1793
1873
  // Analyze the selector to extract nested queries
1794
1874
  const mockRow = tracer.trace('createMockRow', () => this._createMockRow());
1795
- const selectionResult = tracer.trace('evaluateSelector', () => this.selector(mockRow));
1875
+ const selectionResult = tracer.trace('evaluateSelector', () => (0, exports.materializeMockSelection)(this.selector(mockRow)));
1796
1876
  // Check if we're using temp table strategy and have collections
1797
1877
  const collections = tracer.trace('detectCollections', () => this.detectCollections(selectionResult));
1798
1878
  const useTempTableStrategy = this.collectionStrategy === 'temptable' && collections.length > 0;
@@ -2054,7 +2134,7 @@ class SelectQueryBuilder {
2054
2134
  'jsonb',
2055
2135
  ]);
2056
2136
  try {
2057
- const selected = b.selector(b.createMockItem());
2137
+ const selected = (0, exports.materializeMockSelection)(b.selector(b.createMockItem()));
2058
2138
  for (const value of Object.values(selected)) {
2059
2139
  if (!(value && typeof value === 'object' && '__dbColumnName' in value)) {
2060
2140
  return false;
@@ -2119,10 +2199,9 @@ class SelectQueryBuilder {
2119
2199
  // Find primary key column from schema, fallback to "id" if not found
2120
2200
  let pkColumn = null;
2121
2201
  if (targetSchema) {
2122
- for (const colBuilder of Object.values(targetSchema.columns)) {
2123
- const config = colBuilder.build();
2124
- if (config.primaryKey) {
2125
- pkColumn = config.name;
2202
+ for (const meta of getSchemaColumnMeta(targetSchema).values()) {
2203
+ if (meta.primaryKey && meta.name != null) {
2204
+ pkColumn = meta.name;
2126
2205
  break;
2127
2206
  }
2128
2207
  }
@@ -2503,7 +2582,7 @@ class SelectQueryBuilder {
2503
2582
  };
2504
2583
  // Analyze the selector to extract nested queries
2505
2584
  const mockRow = this._createMockRow();
2506
- const selectionResult = this.selector(mockRow);
2585
+ const selectionResult = (0, exports.materializeMockSelection)(this.selector(mockRow));
2507
2586
  // Build the query - this populates context.placeholders
2508
2587
  const { sql, nestedPaths } = this.buildQuery(selectionResult, context);
2509
2588
  // Create transform function (closure over schema, selection, nestedPaths)
@@ -2892,7 +2971,7 @@ class SelectQueryBuilder {
2892
2971
  }
2893
2972
  // Selector function - extract selected columns
2894
2973
  const mockRow = this._createMockRow();
2895
- const selectedMock = this.selector(mockRow);
2974
+ const selectedMock = (0, exports.materializeMockSelection)(this.selector(mockRow));
2896
2975
  const selection = returning(selectedMock);
2897
2976
  if (typeof selection === 'object' && selection !== null) {
2898
2977
  const columns = [];
@@ -2932,12 +3011,14 @@ class SelectQueryBuilder {
2932
3011
  mapDeleteReturningResults(rows, returning, fragmentMappers) {
2933
3012
  if (returning === true) {
2934
3013
  // Full entity mapping - apply fromDriver mappers
3014
+ // getSchemaColumnMeta: `colBuilder.build()` per column PER ROW was pure waste for an
3015
+ // immutable schema — the per-schema meta map is computed once.
3016
+ const columnMeta = getSchemaColumnMeta(this.schema);
2935
3017
  return rows.map(row => {
2936
3018
  const mapped = {};
2937
- for (const [propName, colBuilder] of Object.entries(this.schema.columns)) {
2938
- const config = colBuilder.build();
2939
- const dbValue = row[config.name];
2940
- mapped[propName] = config.mapper ? config.mapper.fromDriver(dbValue) : dbValue;
3019
+ for (const [propName, meta] of columnMeta) {
3020
+ const dbValue = row[meta.name];
3021
+ mapped[propName] = meta.mapper ? meta.mapper.fromDriver(dbValue) : dbValue;
2941
3022
  }
2942
3023
  return mapped;
2943
3024
  });
@@ -2953,15 +3034,13 @@ class SelectQueryBuilder {
2953
3034
  continue;
2954
3035
  }
2955
3036
  // Try to find column by alias or name
2956
- const colEntry = Object.entries(this.schema.columns).find(([propName, col]) => {
2957
- const config = col.build();
2958
- return propName === key || config.name === key;
3037
+ const colEntry = [...getSchemaColumnMeta(this.schema)].find(([propName, meta]) => {
3038
+ return propName === key || meta.name === key;
2959
3039
  });
2960
3040
  if (colEntry) {
2961
- const [, col] = colEntry;
2962
- const config = col.build();
3041
+ const [, meta] = colEntry;
2963
3042
  // Apply fromDriver mapper if present
2964
- mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
3043
+ mapped[key] = meta.mapper ? meta.mapper.fromDriver(value) : value;
2965
3044
  }
2966
3045
  else {
2967
3046
  mapped[key] = value;
@@ -2982,7 +3061,7 @@ class SelectQueryBuilder {
2982
3061
  }
2983
3062
  // Analyze the returning selector
2984
3063
  const mockRow = this._createMockRow();
2985
- const selectedMock = this.selector(mockRow);
3064
+ const selectedMock = (0, exports.materializeMockSelection)(this.selector(mockRow));
2986
3065
  const selection = returning(selectedMock);
2987
3066
  if (typeof selection !== 'object' || selection === null) {
2988
3067
  return null;
@@ -3070,7 +3149,7 @@ class SelectQueryBuilder {
3070
3149
  const selectParts = [];
3071
3150
  const nestedPaths = new Set();
3072
3151
  const mockRow = this._createMockRow();
3073
- const selectedMock = this.selector(mockRow);
3152
+ const selectedMock = (0, exports.materializeMockSelection)(this.selector(mockRow));
3074
3153
  const selection = returning(selectedMock);
3075
3154
  // Helper to get FK db column name from a schema
3076
3155
  const getFkDbColumnName = (sourceSchema, fkPropName) => {
@@ -3339,37 +3418,61 @@ ${joinClauses.join('\n')}`;
3339
3418
  * @internal
3340
3419
  */
3341
3420
  _createMockRow() {
3342
- const mock = {};
3421
+ // Prototype-level cache — same pattern as the SelectQueryBuilder mock (see
3422
+ // MockRowCache). The signature includes the manual-join shape because join
3423
+ // sub-objects are part of the mock's surface; joined queries carry a per-query
3424
+ // alias counter, so their signatures are effectively unique and they degrade to
3425
+ // the uncached cost (built and discarded per row), while the far more common
3426
+ // join-less `qb|<table>` signature hits the shared prototype. Per-query state
3427
+ // (`chainId`, FieldRef cache, join sub-objects) lives in symbol-keyed slots read
3428
+ // through `this`. Opt-in via the same static switch; OFF = fresh prototype per row
3429
+ // (the pre-0.4.70 behaviour).
3430
+ const joinSig = this.manualJoins
3431
+ .filter(j => !j.isSubquery && j.schema)
3432
+ .map(j => `${j.alias}:${j.schema.name}`)
3433
+ .join('+');
3434
+ const prototype = mock_row_cache_1.MockRowCache.getOrBuild(`qb|${this.schema.name}|${joinSig}`, () => Object.defineProperties({}, this.buildRootMockDescriptors()));
3435
+ const mock = Object.create(prototype);
3436
+ mock[MOCK_ROW_FIELD_REFS] = {};
3437
+ mock[MOCK_ROW_NAV_CACHE] = {};
3438
+ mock[MOCK_ROW_CHAIN_ID] = this.chainId;
3439
+ return mock;
3440
+ }
3441
+ /**
3442
+ * Builds the shared property-descriptor map for {@link _createMockRow}'s cached path.
3443
+ * The getters read per-row state through `this`-bound symbol slots; values captured at
3444
+ * build time are signature-constants identical for every row of the signature.
3445
+ */
3446
+ buildRootMockDescriptors() {
3343
3447
  const tableAlias = this.schema.name;
3344
- const chainId = this.chainId;
3345
3448
  // Performance: Use pre-computed column name map if available
3346
3449
  const columnNameMap = getColumnNameMapForSchema(this.schema);
3347
- // Performance: Lazy-cache FieldRef objects
3348
- const fieldRefCache = {};
3349
3450
  // Build a mapper lookup for columns (only when needed)
3350
3451
  const columnMappers = {};
3351
3452
  const columnSqlTypes = {};
3352
- for (const [colName, colBuilder] of Object.entries(this.schema.columns)) {
3353
- const config = colBuilder.build();
3354
- if (config.mapper) {
3355
- columnMappers[colName] = config.mapper;
3453
+ for (const [colName, meta] of getSchemaColumnMeta(this.schema)) {
3454
+ if (meta.mapper) {
3455
+ columnMappers[colName] = meta.mapper;
3356
3456
  }
3357
- if (config.type) {
3358
- columnSqlTypes[colName] = config.type;
3457
+ if (meta.type) {
3458
+ columnSqlTypes[colName] = meta.type;
3359
3459
  }
3360
3460
  }
3461
+ const descriptors = {};
3361
3462
  // Add columns as FieldRef objects - type-safe with property name and database column name
3362
3463
  for (const [colName, dbColumnName] of columnNameMap) {
3363
3464
  const mapper = columnMappers[colName];
3364
- Object.defineProperty(mock, colName, {
3465
+ descriptors[colName] = {
3365
3466
  get() {
3467
+ const slots = this;
3468
+ const fieldRefCache = slots[MOCK_ROW_FIELD_REFS] ?? (slots[MOCK_ROW_FIELD_REFS] = {});
3366
3469
  let cached = fieldRefCache[colName];
3367
3470
  if (!cached) {
3368
3471
  cached = fieldRefCache[colName] = {
3369
3472
  __fieldName: colName,
3370
3473
  __dbColumnName: dbColumnName,
3371
3474
  __tableAlias: tableAlias,
3372
- __chainId: chainId,
3475
+ __chainId: slots[MOCK_ROW_CHAIN_ID],
3373
3476
  // Include mapper for toDriver transformation in conditions
3374
3477
  __mapper: mapper,
3375
3478
  // Column SQL type — lets flag* emit width-exact mask casts
@@ -3380,48 +3483,66 @@ ${joinClauses.join('\n')}`;
3380
3483
  },
3381
3484
  enumerable: true,
3382
3485
  configurable: true,
3383
- });
3486
+ };
3384
3487
  }
3488
+ // Captured at descriptor-build time — identical for every row of this signature.
3489
+ const manualJoins = this.manualJoins;
3385
3490
  // Add columns from manually joined tables
3386
- for (const join of this.manualJoins) {
3491
+ for (const join of manualJoins) {
3387
3492
  // Skip subquery joins (they don't have a schema)
3388
3493
  if (join.isSubquery || !join.schema) {
3389
3494
  continue;
3390
3495
  }
3391
- // Performance: Use pre-computed column name map for joined schema
3392
- const joinColumnNameMap = getColumnNameMapForSchema(join.schema);
3393
- if (!mock[join.alias]) {
3394
- mock[join.alias] = {};
3395
- }
3396
- // Lazy-cache for joined table
3397
- const joinFieldRefCache = {};
3496
+ // The join sub-object is built lazily per row (its FieldRef cache and identity are
3497
+ // row-scoped, exactly as the pre-prototype own-property sub-object was) and
3498
+ // memoized in the row's navigation slot so repeated accesses share one object.
3499
+ const joinSchema = join.schema;
3398
3500
  const joinAlias = join.alias;
3399
- for (const [colName, dbColumnName] of joinColumnNameMap) {
3400
- Object.defineProperty(mock[join.alias], colName, {
3401
- get() {
3402
- let cached = joinFieldRefCache[colName];
3403
- if (!cached) {
3404
- cached = joinFieldRefCache[colName] = {
3405
- __fieldName: colName,
3406
- __dbColumnName: dbColumnName,
3407
- __tableAlias: joinAlias,
3408
- };
3501
+ descriptors[joinAlias] = {
3502
+ get() {
3503
+ const slots = this;
3504
+ const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
3505
+ let sub = navCache[joinAlias];
3506
+ if (sub === undefined) {
3507
+ sub = navCache[joinAlias] = {};
3508
+ const joinColumnNameMap = getColumnNameMapForSchema(joinSchema);
3509
+ const joinFieldRefCache = {};
3510
+ for (const [colName, dbColumnName] of joinColumnNameMap) {
3511
+ Object.defineProperty(sub, colName, {
3512
+ get() {
3513
+ let cached = joinFieldRefCache[colName];
3514
+ if (!cached) {
3515
+ cached = joinFieldRefCache[colName] = {
3516
+ __fieldName: colName,
3517
+ __dbColumnName: dbColumnName,
3518
+ __tableAlias: joinAlias,
3519
+ };
3520
+ }
3521
+ return cached;
3522
+ },
3523
+ enumerable: true,
3524
+ configurable: true,
3525
+ });
3409
3526
  }
3410
- return cached;
3411
- },
3412
- enumerable: true,
3413
- configurable: true,
3414
- });
3415
- }
3527
+ }
3528
+ return sub;
3529
+ },
3530
+ enumerable: true,
3531
+ configurable: true,
3532
+ };
3416
3533
  }
3417
3534
  // Performance: Use pre-computed relation entries
3418
3535
  const relationEntries = getRelationEntriesForSchema(this.schema);
3536
+ // Values captured at descriptor-build time — identical for every row of this
3537
+ // signature (the registry is the process-wide schema registry).
3538
+ const schemaRegistry = this.schemaRegistry;
3539
+ const sourceTableName = this.schema.name;
3419
3540
  // Add relations as CollectionQueryBuilder or ReferenceQueryBuilder
3420
3541
  for (const [relName, relConfig] of relationEntries) {
3421
3542
  // Try to get target schema from registry (preferred, has full relations) or cached schema
3422
3543
  let targetSchema;
3423
- if (this.schemaRegistry) {
3424
- targetSchema = this.schemaRegistry.get(relConfig.targetTable);
3544
+ if (schemaRegistry) {
3545
+ targetSchema = schemaRegistry.get(relConfig.targetTable);
3425
3546
  }
3426
3547
  if (!targetSchema) {
3427
3548
  // Performance: Use cached target schema
@@ -3429,36 +3550,36 @@ ${joinClauses.join('\n')}`;
3429
3550
  }
3430
3551
  if (relConfig.type === 'many') {
3431
3552
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
3432
- Object.defineProperty(mock, relName, {
3553
+ descriptors[relName] = {
3433
3554
  get: () => {
3434
- return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, // Pass the target schema directly
3435
- this.schemaRegistry, // Pass schema registry for nested resolution
3555
+ return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', sourceTableName, targetSchema, // Pass the target schema directly
3556
+ schemaRegistry, // Pass schema registry for nested resolution
3436
3557
  undefined, relConfig.foreignKeys, // Propagate composite FK / literal predicates
3437
3558
  relConfig.matches);
3438
3559
  },
3439
3560
  enumerable: false,
3440
3561
  configurable: true,
3441
- });
3562
+ };
3442
3563
  }
3443
3564
  else {
3444
3565
  // For single reference (many-to-one), create a ReferenceQueryBuilder
3445
3566
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
3446
- Object.defineProperty(mock, relName, {
3567
+ descriptors[relName] = {
3447
3568
  get: () => {
3448
3569
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, // Pass the target schema directly
3449
- this.schemaRegistry, // Pass schema registry for nested resolution
3570
+ schemaRegistry, // Pass schema registry for nested resolution
3450
3571
  [], // Empty navigation path for first level navigation
3451
- this.schema.name // Pass source table name for lateral join correlation
3572
+ sourceTableName // Pass source table name for lateral join correlation
3452
3573
  );
3453
3574
  // Return a mock object that exposes the target table's columns
3454
3575
  return refBuilder.createMockTargetRow();
3455
3576
  },
3456
3577
  enumerable: false,
3457
3578
  configurable: true,
3458
- });
3579
+ };
3459
3580
  }
3460
3581
  }
3461
- return mock;
3582
+ return descriptors;
3462
3583
  }
3463
3584
  /**
3464
3585
  * Create a proxy that wraps selected values and returns FieldRefs for property access
@@ -5503,7 +5624,7 @@ ${joinClauses.join('\n')}`;
5503
5624
  };
5504
5625
  // Analyze the selector to extract nested queries
5505
5626
  const mockRow = this._createMockRow();
5506
- const selectionResult = this.selector(mockRow);
5627
+ const selectionResult = (0, exports.materializeMockSelection)(this.selector(mockRow));
5507
5628
  // Build the query
5508
5629
  const { sql } = this.buildQuery(selectionResult, context);
5509
5630
  // Update the outer context's param counter
@@ -5664,13 +5785,12 @@ class ReferenceQueryBuilder {
5664
5785
  // Build a mapper lookup for columns (only when needed)
5665
5786
  const columnMappers = {};
5666
5787
  const columnSqlTypes = {};
5667
- for (const [colName, colBuilder] of Object.entries(this.targetTableSchema.columns)) {
5668
- const config = colBuilder.build();
5669
- if (config.mapper) {
5670
- columnMappers[colName] = config.mapper;
5788
+ for (const [colName, meta] of getSchemaColumnMeta(this.targetTableSchema)) {
5789
+ if (meta.mapper) {
5790
+ columnMappers[colName] = meta.mapper;
5671
5791
  }
5672
- if (config.type) {
5673
- columnSqlTypes[colName] = config.type;
5792
+ if (meta.type) {
5793
+ columnSqlTypes[colName] = meta.type;
5674
5794
  }
5675
5795
  }
5676
5796
  const sourceTable = this.targetTable; // Actual table name for schema lookup
@@ -5877,74 +5997,16 @@ class CollectionQueryBuilder {
5877
5997
  return this._cachedMockItem;
5878
5998
  }
5879
5999
  if (this.targetTableSchema) {
5880
- // If we have schema information, create a properly typed mock
5881
- const mock = {};
5882
- // Performance: Use pre-computed column name map if available
5883
- const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
5884
- // Performance: Lazy-cache FieldRef objects
5885
- const fieldRefCache = {};
5886
- // Add columns - include tableAlias for unambiguous column references in WHERE clauses
5887
- // Use a special marker alias for the collection's own table that can be rewritten later
5888
- // This allows distinguishing between outer table references and inner collection references
5889
- // when both target the same table (e.g., post.user.posts where both are "posts" table)
5890
- const tableAlias = `__collection_${this.targetTable}__`;
5891
- for (const [colName, dbColumnName] of columnNameMap) {
5892
- Object.defineProperty(mock, colName, {
5893
- get() {
5894
- let cached = fieldRefCache[colName];
5895
- if (!cached) {
5896
- cached = fieldRefCache[colName] = {
5897
- __fieldName: colName,
5898
- __dbColumnName: dbColumnName,
5899
- __tableAlias: tableAlias, // Include table alias for unambiguous references
5900
- };
5901
- }
5902
- return cached;
5903
- },
5904
- enumerable: true,
5905
- configurable: true,
5906
- });
5907
- }
5908
- // Add navigation properties (both collections and references)
5909
- if (this.targetTableSchema.relations) {
5910
- for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
5911
- if (relConfig.type === 'many') {
5912
- // Collection navigation
5913
- // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
5914
- Object.defineProperty(mock, relName, {
5915
- get: () => {
5916
- // Don't call build() - it returns schema without relations
5917
- const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
5918
- return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.targetTable, undefined, // Don't pass schema, force registry lookup
5919
- this.schemaRegistry, // Pass schema registry for nested resolution
5920
- // No navigation path needed here - direct collection access from parent
5921
- undefined, relConfig.foreignKeys, // Propagate composite FK / literal predicates
5922
- relConfig.matches);
5923
- },
5924
- enumerable: false,
5925
- configurable: true,
5926
- });
5927
- }
5928
- else {
5929
- // Reference navigation
5930
- // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
5931
- Object.defineProperty(mock, relName, {
5932
- get: () => {
5933
- // Don't call build() - it returns schema without relations
5934
- // Instead, pass undefined and let ReferenceQueryBuilder look it up from registry
5935
- const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, undefined, // Don't pass schema, force registry lookup
5936
- this.schemaRegistry, // Pass schema registry for nested resolution
5937
- [], // Empty navigation path - this is the first reference in the chain
5938
- this.targetTable // Source alias is this collection's target table
5939
- );
5940
- return refBuilder.createMockTargetRow();
5941
- },
5942
- enumerable: false,
5943
- configurable: true,
5944
- });
5945
- }
5946
- }
5947
- }
6000
+ // Prototype-level cache same pattern as the root mock and
6001
+ // ReferenceQueryBuilder.createMockTargetRow (see MockRowCache): the getters are
6002
+ // fully determined by the target schema, so every collection item mock of the same
6003
+ // table shares one prebuilt prototype and a new item is `Object.create` plus its
6004
+ // FieldRef slot O(1) instead of one property definition per column+relation per
6005
+ // `.where()`/`.select()`/`.orderBy()` call. Opt-in via the same static switch; OFF
6006
+ // = fresh prototype per item (the pre-0.4.70 behaviour).
6007
+ const prototype = mock_row_cache_1.MockRowCache.getOrBuild(`citem|${this.targetTable}`, () => Object.defineProperties({}, this.buildMockItemDescriptors()));
6008
+ const mock = Object.create(prototype);
6009
+ mock[MOCK_ROW_FIELD_REFS] = {};
5948
6010
  // Cache the mock for reuse
5949
6011
  this._cachedMockItem = mock;
5950
6012
  return mock;
@@ -5954,6 +6016,85 @@ class CollectionQueryBuilder {
5954
6016
  return createNestedFieldRefProxy(this.targetTable);
5955
6017
  }
5956
6018
  }
6019
+ /**
6020
+ * Builds the shared property-descriptor map for {@link createMockItem}'s cached path.
6021
+ * The getters read per-row state through `this`-bound symbol slots; values captured at
6022
+ * build time are signature-constants identical for every item mock of the table.
6023
+ */
6024
+ buildMockItemDescriptors() {
6025
+ // Performance: Use pre-computed column name map if available
6026
+ const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
6027
+ // Add columns - include tableAlias for unambiguous column references in WHERE clauses
6028
+ // Use a special marker alias for the collection's own table that can be rewritten later
6029
+ // This allows distinguishing between outer table references and inner collection references
6030
+ // when both target the same table (e.g., post.user.posts where both are "posts" table)
6031
+ const tableAlias = `__collection_${this.targetTable}__`;
6032
+ const descriptors = {};
6033
+ for (const [colName, dbColumnName] of columnNameMap) {
6034
+ descriptors[colName] = {
6035
+ get() {
6036
+ const slots = this;
6037
+ const fieldRefCache = slots[MOCK_ROW_FIELD_REFS] ?? (slots[MOCK_ROW_FIELD_REFS] = {});
6038
+ let cached = fieldRefCache[colName];
6039
+ if (!cached) {
6040
+ cached = fieldRefCache[colName] = {
6041
+ __fieldName: colName,
6042
+ __dbColumnName: dbColumnName,
6043
+ __tableAlias: tableAlias, // Include table alias for unambiguous references
6044
+ };
6045
+ }
6046
+ return cached;
6047
+ },
6048
+ enumerable: true,
6049
+ configurable: true,
6050
+ };
6051
+ }
6052
+ // Values captured at descriptor-build time — identical for every item of this
6053
+ // signature (the registry is the process-wide schema registry).
6054
+ const targetTable = this.targetTable;
6055
+ const schemaRegistry = this.schemaRegistry;
6056
+ // Add navigation properties (both collections and references)
6057
+ if (this.targetTableSchema.relations) {
6058
+ for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
6059
+ if (relConfig.type === 'many') {
6060
+ // Collection navigation
6061
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
6062
+ descriptors[relName] = {
6063
+ get: () => {
6064
+ // Don't call build() - it returns schema without relations
6065
+ const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
6066
+ return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, targetTable, undefined, // Don't pass schema, force registry lookup
6067
+ schemaRegistry, // Pass schema registry for nested resolution
6068
+ // No navigation path needed here - direct collection access from parent
6069
+ undefined, relConfig.foreignKeys, // Propagate composite FK / literal predicates
6070
+ relConfig.matches);
6071
+ },
6072
+ enumerable: false,
6073
+ configurable: true,
6074
+ };
6075
+ }
6076
+ else {
6077
+ // Reference navigation
6078
+ // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
6079
+ descriptors[relName] = {
6080
+ get: () => {
6081
+ // Don't call build() - it returns schema without relations
6082
+ // Instead, pass undefined and let ReferenceQueryBuilder look it up from registry
6083
+ const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, undefined, // Don't pass schema, force registry lookup
6084
+ schemaRegistry, // Pass schema registry for nested resolution
6085
+ [], // Empty navigation path - this is the first reference in the chain
6086
+ targetTable // Source alias is this collection's target table
6087
+ );
6088
+ return refBuilder.createMockTargetRow();
6089
+ },
6090
+ enumerable: false,
6091
+ configurable: true,
6092
+ };
6093
+ }
6094
+ }
6095
+ }
6096
+ return descriptors;
6097
+ }
5957
6098
  /**
5958
6099
  * Limit collection items
5959
6100
  */
@@ -6524,6 +6665,19 @@ class CollectionQueryBuilder {
6524
6665
  * Uses BFS to find the shortest path through the schema graph
6525
6666
  */
6526
6667
  findNavigationPath(targetAlias, joinedSchemas, _startSchema) {
6668
+ if (!this.schemaRegistry) {
6669
+ return [];
6670
+ }
6671
+ // Memoised per registry — the BFS below depends only on the target alias and the ORDERED
6672
+ // joined alias→table pairs (order decides which of several equal-length paths wins), so
6673
+ // the same query shape resolves to the same path on every build. See NavigationPathCache.
6674
+ const signature = `${targetAlias}|${[...joinedSchemas]
6675
+ .map(([alias, schema]) => `${alias}:${schema.schema ?? ''}.${schema.name}`)
6676
+ .join(',')}`;
6677
+ return navigation_path_cache_1.NavigationPathCache.getOrBuild(this.schemaRegistry, signature, () => this.computeNavigationPath(targetAlias, joinedSchemas));
6678
+ }
6679
+ /** The uncached BFS behind {@link findNavigationPath}. */
6680
+ computeNavigationPath(targetAlias, joinedSchemas) {
6527
6681
  if (!this.schemaRegistry) {
6528
6682
  return [];
6529
6683
  }
@@ -6745,7 +6899,7 @@ class CollectionQueryBuilder {
6745
6899
  // (field collection, aggregate-expression discovery, navigation-join detection) all
6746
6900
  // need to walk the same selection, and re-invoking the selector is expensive
6747
6901
  // (rebuilds proxy mocks and any nested CollectionQueryBuilder instances).
6748
- const selectorResult = this.selector ? this.selector(this.createMockItem()) : undefined;
6902
+ const selectorResult = this.selector ? (0, exports.materializeMockSelection)(this.selector(this.createMockItem())) : undefined;
6749
6903
  // Step 1: Build field selection configuration
6750
6904
  if (this.selector) {
6751
6905
  const selectedFields = selectorResult;
@@ -6822,9 +6976,8 @@ class CollectionQueryBuilder {
6822
6976
  let dbToPropertyMap = null;
6823
6977
  if (this.targetTableSchema) {
6824
6978
  dbToPropertyMap = new Map();
6825
- for (const [propName, colBuilder] of Object.entries(this.targetTableSchema.columns)) {
6826
- const config = colBuilder.build();
6827
- dbToPropertyMap.set(config.name, propName);
6979
+ for (const [propName, meta] of getSchemaColumnMeta(this.targetTableSchema)) {
6980
+ dbToPropertyMap.set(meta.name, propName);
6828
6981
  }
6829
6982
  }
6830
6983
  const orderPartsDb = this.orderByFields.map(({ field, direction }) => {