linkgress-orm 0.4.68 → 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");
@@ -59,6 +60,26 @@ function getRelationEntriesForSchema(schema) {
59
60
  // Fallback: build the array (for schemas that weren't built with the new TableBuilder)
60
61
  return Object.entries(schema.relations);
61
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
+ }
62
83
  /**
63
84
  * Mock-row descriptor cache for {@link ReferenceQueryBuilder.createMockTargetRow}.
64
85
  *
@@ -77,14 +98,18 @@ function getRelationEntriesForSchema(schema) {
77
98
  */
78
99
  const MOCK_ROW_FIELD_REFS = Symbol('linkgressMockFieldRefs');
79
100
  const MOCK_ROW_NAV_CACHE = Symbol('linkgressMockNavCache');
101
+ const MOCK_ROW_CHAIN_ID = Symbol('linkgressMockChainId');
80
102
  const navigationPathSignature = (path) => path
81
103
  .map(step => `${step.alias}:${step.targetTable}:${(step.foreignKeys ?? []).join('+')}:${(step.matches ?? []).join('+')}:${step.isMandatory ? 1 : 0}:${step.sourceAlias ?? ''}`)
82
104
  .join('>');
83
105
  /**
84
- * Whether `value` is a reference mock row minted by `ReferenceQueryBuilder.createMockTargetRow`.
85
- * Those rows INHERIT their column/relation getters from a shared prototype (see MockRowCache),
86
- * so `Object.getPrototypeOf(row) !== Object.prototype` and own-property APIs see no columns
87
- * 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.
88
113
  */
89
114
  const isReferenceMockRow = (value) => value != null && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, MOCK_ROW_FIELD_REFS);
90
115
  /** `Object.getOwnPropertyDescriptor` that walks the prototype chain (stops before Object.prototype). */
@@ -99,10 +124,32 @@ const findPropertyDescriptor = (value, key) => {
99
124
  }
100
125
  return undefined;
101
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;
102
149
  /**
103
150
  * First enumerable key (own or inherited) backed by a getter — the "is this a mock row" probe
104
- * shared by the selection resolvers. Root mocks define their getters as own properties,
105
- * 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
106
153
  * enumerable getter is always a column and `row[key]` yields its FieldRef.
107
154
  */
108
155
  const findFirstGetterKey = (value) => {
@@ -299,37 +346,67 @@ class QueryBuilder {
299
346
  if (this._cachedMockRow) {
300
347
  return this._cachedMockRow;
301
348
  }
302
- 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() {
303
377
  const tableAlias = this.schema.name;
304
- const chainId = this.chainId;
305
378
  // Performance: Use pre-computed column name map if available
306
379
  const columnNameMap = getColumnNameMapForSchema(this.schema);
307
- // Performance: Lazy-cache FieldRef objects - only create when first accessed
308
- const fieldRefCache = {};
309
380
  // Build a mapper lookup for columns (only when needed)
310
381
  const columnMappers = {};
311
382
  const columnSqlTypes = {};
312
- for (const [colName, colBuilder] of Object.entries(this.schema.columns)) {
313
- const config = colBuilder.build();
314
- if (config.mapper) {
315
- 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;
316
390
  }
317
- if (config.type) {
318
- columnSqlTypes[colName] = config.type;
391
+ if (meta.type) {
392
+ columnSqlTypes[colName] = meta.type;
319
393
  }
320
394
  }
395
+ const descriptors = {};
321
396
  // Add columns as FieldRef objects - type-safe with property name and database column name
322
397
  for (const [colName, dbColumnName] of columnNameMap) {
323
398
  const mapper = columnMappers[colName];
324
- Object.defineProperty(mock, colName, {
399
+ descriptors[colName] = {
325
400
  get() {
401
+ const slots = this;
402
+ const fieldRefCache = slots[MOCK_ROW_FIELD_REFS] ?? (slots[MOCK_ROW_FIELD_REFS] = {});
326
403
  let cached = fieldRefCache[colName];
327
404
  if (!cached) {
328
405
  cached = fieldRefCache[colName] = {
329
406
  __fieldName: colName,
330
407
  __dbColumnName: dbColumnName,
331
408
  __tableAlias: tableAlias,
332
- __chainId: chainId,
409
+ __chainId: slots[MOCK_ROW_CHAIN_ID],
333
410
  // Include mapper for toDriver transformation in conditions
334
411
  __mapper: mapper,
335
412
  // Column SQL type — lets flag* emit width-exact mask casts
@@ -340,49 +417,51 @@ class QueryBuilder {
340
417
  },
341
418
  enumerable: true,
342
419
  configurable: true,
343
- });
420
+ };
344
421
  }
345
422
  // Performance: Use pre-computed relation entries and cached schemas
346
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;
347
428
  // Add relations (both collections and single references)
348
429
  for (const [relName, relConfig] of relationEntries) {
349
430
  // Performance: Use cached target schema, but prefer registry lookup for full relations
350
- let targetSchema = this.schemaRegistry?.get(relConfig.targetTable);
431
+ let targetSchema = schemaRegistry?.get(relConfig.targetTable);
351
432
  if (!targetSchema) {
352
433
  targetSchema = getTargetSchemaForRelation(this.schema, relName, relConfig);
353
434
  }
354
435
  if (relConfig.type === 'many') {
355
436
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
356
- Object.defineProperty(mock, relName, {
437
+ descriptors[relName] = {
357
438
  get: () => {
358
- 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
359
440
  undefined, relConfig.foreignKeys, // Propagate composite FK / literal predicates
360
441
  relConfig.matches);
361
442
  },
362
443
  enumerable: false,
363
444
  configurable: true,
364
- });
445
+ };
365
446
  }
366
447
  else {
367
448
  // Single reference navigation (many-to-one, one-to-one)
368
449
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
369
450
  // with circular relations like User->Posts->User)
370
- Object.defineProperty(mock, relName, {
451
+ descriptors[relName] = {
371
452
  get: () => {
372
- 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
373
454
  [], // Empty navigation path for first level navigation
374
- this.schema.name // Pass source table name for lateral join correlation
455
+ sourceTableName // Pass source table name for lateral join correlation
375
456
  );
376
457
  return refBuilder.createMockTargetRow();
377
458
  },
378
459
  enumerable: false,
379
460
  configurable: true,
380
- });
461
+ };
381
462
  }
382
463
  }
383
- // Cache the mock for reuse
384
- this._cachedMockRow = mock;
385
- return mock;
464
+ return descriptors;
386
465
  }
387
466
  /**
388
467
  * Add a LEFT JOIN to the query with a selector (supports both tables and subqueries)
@@ -426,7 +505,7 @@ class QueryBuilder {
426
505
  // Create fresh mocks for the selector invocation
427
506
  const freshMockLeft = createLeftMock();
428
507
  const freshMockRight = createRightMock();
429
- return selector(freshMockLeft, freshMockRight);
508
+ return (0, exports.materializeMockSelection)(selector(freshMockLeft, freshMockRight));
430
509
  };
431
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
432
511
  undefined, // schemaRegistry
@@ -475,7 +554,7 @@ class QueryBuilder {
475
554
  // Create fresh mocks for the selector invocation
476
555
  const freshMockLeft = createLeftMock();
477
556
  const freshMockRight = createRightMock();
478
- return selector(freshMockLeft, freshMockRight);
557
+ return (0, exports.materializeMockSelection)(selector(freshMockLeft, freshMockRight));
479
558
  };
480
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
481
560
  undefined, // schemaRegistry
@@ -561,7 +640,7 @@ class QueryBuilder {
561
640
  }
562
641
  orderBy(selector) {
563
642
  const mockRow = this._createMockRow();
564
- const result = selector(mockRow);
643
+ const result = (0, exports.materializeMockSelection)(selector(mockRow));
565
644
  (0, query_utils_1.parseOrderBy)(result, this.orderByFields);
566
645
  return this;
567
646
  }
@@ -631,7 +710,7 @@ class SelectQueryBuilder {
631
710
  select(selector) {
632
711
  // Create a composed selector that applies both transformations
633
712
  const composedSelector = (row) => {
634
- const firstResult = this.selector(row);
713
+ const firstResult = (0, exports.materializeMockSelection)(this.selector(row));
635
714
  return selector(firstResult);
636
715
  };
637
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);
@@ -679,7 +758,7 @@ class SelectQueryBuilder {
679
758
  const rightAlias = `${rightSchema.name}_${this.joinCounter}`;
680
759
  this.joinCounter = this.joinCounter + 1;
681
760
  const mockRow = this._createMockRow();
682
- const selectedMock = this.selector(mockRow);
761
+ const selectedMock = (0, exports.materializeMockSelection)(this.selector(mockRow));
683
762
  const leftMock = this.createFieldRefProxy(selectedMock, true);
684
763
  const rightMock = this.createMockRowForTable(rightSchema, rightAlias);
685
764
  const onCondition = on(leftMock, rightMock);
@@ -708,7 +787,7 @@ class SelectQueryBuilder {
708
787
  */
709
788
  addCteFilterJoin(type, cte, on, filter) {
710
789
  const mockRow = this._createMockRow();
711
- const selectedMock = this.selector(mockRow);
790
+ const selectedMock = (0, exports.materializeMockSelection)(this.selector(mockRow));
712
791
  const leftMock = this.createFieldRefProxy(selectedMock, true);
713
792
  const rightMock = this.createMockRowForCte(cte);
714
793
  const onCondition = on(leftMock, rightMock);
@@ -735,7 +814,7 @@ class SelectQueryBuilder {
735
814
  where(condition) {
736
815
  const mockRow = this._createMockRow();
737
816
  // Apply the selector to get the selected shape that the user sees in the WHERE condition
738
- const selectedMock = this.selector(mockRow);
817
+ const selectedMock = (0, exports.materializeMockSelection)(this.selector(mockRow));
739
818
  // Wrap in proxy - for WHERE, we preserve original column names
740
819
  const fieldRefProxy = this.createFieldRefProxy(selectedMock, true);
741
820
  const newCondition = condition(fieldRefProxy);
@@ -825,7 +904,7 @@ class SelectQueryBuilder {
825
904
  }
826
905
  orderBy(selector) {
827
906
  const mockRow = this._createMockRow();
828
- const selectedMock = this.selector(mockRow);
907
+ const selectedMock = (0, exports.materializeMockSelection)(this.selector(mockRow));
829
908
  // Wrap selectedMock in a proxy that returns FieldRefs for property access
830
909
  const fieldRefProxy = this.createFieldRefProxy(selectedMock);
831
910
  const result = selector(fieldRefProxy);
@@ -857,7 +936,7 @@ class SelectQueryBuilder {
857
936
  const newJoinCounter = this.joinCounter + 1;
858
937
  // Create mock for the current selection (left side)
859
938
  const mockRow = this._createMockRow();
860
- const mockLeftSelection = this.selector(mockRow);
939
+ const mockLeftSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
861
940
  // Create mock for the subquery result (right side)
862
941
  // For subqueries, we create a mock based on the result type
863
942
  const mockRight = this.createMockRowForSubquery(alias, subquery);
@@ -875,7 +954,7 @@ class SelectQueryBuilder {
875
954
  }];
876
955
  // Create a new selector
877
956
  const composedSelector = (row) => {
878
- const leftResult = this.selector(row);
957
+ const leftResult = (0, exports.materializeMockSelection)(this.selector(row));
879
958
  const freshMockRight = this.createMockRowForSubquery(alias, subquery);
880
959
  return selector(leftResult, freshMockRight);
881
960
  };
@@ -901,7 +980,7 @@ class SelectQueryBuilder {
901
980
  // Create mock for the current selection (left side)
902
981
  // IMPORTANT: We call the selector with the mock row to get a result that contains FieldRef objects
903
982
  const mockRow = this._createMockRow();
904
- const mockLeftSelection = this.selector(mockRow);
983
+ const mockLeftSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
905
984
  // The mockLeftSelection now contains FieldRef objects (with __fieldName, __dbColumnName, __tableAlias)
906
985
  // These FieldRef objects preserve the table context
907
986
  // Create mock for the right table
@@ -919,7 +998,7 @@ class SelectQueryBuilder {
919
998
  }];
920
999
  // Create a new selector that first applies the current selector, then the new selector
921
1000
  const composedSelector = (row) => {
922
- const leftResult = this.selector(row);
1001
+ const leftResult = (0, exports.materializeMockSelection)(this.selector(row));
923
1002
  const freshMockRight = this.createMockRowForTable(rightSchema, rightAlias);
924
1003
  return selector(leftResult, freshMockRight);
925
1004
  };
@@ -932,7 +1011,7 @@ class SelectQueryBuilder {
932
1011
  const newJoinCounter = this.joinCounter + 1;
933
1012
  // Create mock for the current selection (left side)
934
1013
  const mockRow = this._createMockRow();
935
- const mockLeftSelection = this.selector(mockRow);
1014
+ const mockLeftSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
936
1015
  // Create mock for the CTE columns (right side)
937
1016
  const mockRight = this.createMockRowForCte(cte);
938
1017
  // Evaluate the join condition
@@ -948,7 +1027,7 @@ class SelectQueryBuilder {
948
1027
  }];
949
1028
  // Create a new selector
950
1029
  const composedSelector = (row) => {
951
- const leftResult = this.selector(row);
1030
+ const leftResult = (0, exports.materializeMockSelection)(this.selector(row));
952
1031
  const freshMockRight = this.createMockRowForCte(cte);
953
1032
  return selector(leftResult, freshMockRight);
954
1033
  };
@@ -965,7 +1044,7 @@ class SelectQueryBuilder {
965
1044
  const newJoinCounter = this.joinCounter + 1;
966
1045
  // Create mock for the current selection (left side)
967
1046
  const mockRow = this._createMockRow();
968
- const mockLeftSelection = this.selector(mockRow);
1047
+ const mockLeftSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
969
1048
  // Create mock for the subquery result (right side)
970
1049
  const mockRight = this.createMockRowForSubquery(alias, subquery);
971
1050
  // Evaluate the join condition
@@ -982,7 +1061,7 @@ class SelectQueryBuilder {
982
1061
  }];
983
1062
  // Create a new selector
984
1063
  const composedSelector = (row) => {
985
- const leftResult = this.selector(row);
1064
+ const leftResult = (0, exports.materializeMockSelection)(this.selector(row));
986
1065
  const freshMockRight = this.createMockRowForSubquery(alias, subquery);
987
1066
  return selector(leftResult, freshMockRight);
988
1067
  };
@@ -1007,7 +1086,7 @@ class SelectQueryBuilder {
1007
1086
  const newJoinCounter = this.joinCounter + 1;
1008
1087
  // Create mock for the current selection (left side)
1009
1088
  const mockRow = this._createMockRow();
1010
- const mockLeftSelection = this.selector(mockRow);
1089
+ const mockLeftSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
1011
1090
  // Create mock for the right table
1012
1091
  const mockRight = this.createMockRowForTable(rightSchema, rightAlias);
1013
1092
  // Evaluate the join condition
@@ -1022,7 +1101,7 @@ class SelectQueryBuilder {
1022
1101
  }];
1023
1102
  // Create a new selector that first applies the current selector, then the new selector
1024
1103
  const composedSelector = (row) => {
1025
- const leftResult = this.selector(row);
1104
+ const leftResult = (0, exports.materializeMockSelection)(this.selector(row));
1026
1105
  const freshMockRight = this.createMockRowForTable(rightSchema, rightAlias);
1027
1106
  return selector(leftResult, freshMockRight);
1028
1107
  };
@@ -1201,7 +1280,7 @@ class SelectQueryBuilder {
1201
1280
  */
1202
1281
  selectDistinct(selector) {
1203
1282
  const composedSelector = (row) => {
1204
- const firstResult = this.selector(row);
1283
+ const firstResult = (0, exports.materializeMockSelection)(this.selector(row));
1205
1284
  return selector(firstResult);
1206
1285
  };
1207
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
@@ -1223,13 +1302,13 @@ class SelectQueryBuilder {
1223
1302
  let fieldToAggregate;
1224
1303
  if (selector) {
1225
1304
  const mockRow = this._createMockRow();
1226
- const mockSelection = this.selector(mockRow);
1305
+ const mockSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
1227
1306
  fieldToAggregate = selector(mockSelection);
1228
1307
  }
1229
1308
  else {
1230
1309
  // No selector - use the current selection
1231
1310
  const mockRow = this._createMockRow();
1232
- fieldToAggregate = this.selector(mockRow);
1311
+ fieldToAggregate = (0, exports.materializeMockSelection)(this.selector(mockRow));
1233
1312
  }
1234
1313
  // Build aggregation query
1235
1314
  const { sql, params } = this.buildAggregationQuery('MIN', fieldToAggregate, context);
@@ -1255,13 +1334,13 @@ class SelectQueryBuilder {
1255
1334
  let fieldToAggregate;
1256
1335
  if (selector) {
1257
1336
  const mockRow = this._createMockRow();
1258
- const mockSelection = this.selector(mockRow);
1337
+ const mockSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
1259
1338
  fieldToAggregate = selector(mockSelection);
1260
1339
  }
1261
1340
  else {
1262
1341
  // No selector - use the current selection
1263
1342
  const mockRow = this._createMockRow();
1264
- fieldToAggregate = this.selector(mockRow);
1343
+ fieldToAggregate = (0, exports.materializeMockSelection)(this.selector(mockRow));
1265
1344
  }
1266
1345
  // Build aggregation query
1267
1346
  const { sql, params } = this.buildAggregationQuery('MAX', fieldToAggregate, context);
@@ -1287,13 +1366,13 @@ class SelectQueryBuilder {
1287
1366
  let fieldToAggregate;
1288
1367
  if (selector) {
1289
1368
  const mockRow = this._createMockRow();
1290
- const mockSelection = this.selector(mockRow);
1369
+ const mockSelection = (0, exports.materializeMockSelection)(this.selector(mockRow));
1291
1370
  fieldToAggregate = selector(mockSelection);
1292
1371
  }
1293
1372
  else {
1294
1373
  // No selector - use the current selection
1295
1374
  const mockRow = this._createMockRow();
1296
- fieldToAggregate = this.selector(mockRow);
1375
+ fieldToAggregate = (0, exports.materializeMockSelection)(this.selector(mockRow));
1297
1376
  }
1298
1377
  // Build aggregation query
1299
1378
  const { sql, params } = this.buildAggregationQuery('SUM', fieldToAggregate, context);
@@ -1341,7 +1420,7 @@ class SelectQueryBuilder {
1341
1420
  executor: this.executor,
1342
1421
  }));
1343
1422
  const mockRow = tracer.trace('createMockRow', () => this._createMockRow());
1344
- const selectionResult = tracer.trace('evaluateSelector', () => this.selector(mockRow));
1423
+ const selectionResult = tracer.trace('evaluateSelector', () => (0, exports.materializeMockSelection)(this.selector(mockRow)));
1345
1424
  const { sql, params, nestedPaths } = tracer.trace('buildQuery', () => this.buildQuery(selectionResult, context));
1346
1425
  tracer.endPhase();
1347
1426
  tracer.startPhase('queryExecution');
@@ -1449,7 +1528,7 @@ class SelectQueryBuilder {
1449
1528
  hoistedCteNames: context.hoistedCteNames,
1450
1529
  };
1451
1530
  const mockRow = this._createMockRow();
1452
- const selectionResult = this.selector(mockRow);
1531
+ const selectionResult = (0, exports.materializeMockSelection)(this.selector(mockRow));
1453
1532
  // Build query without ORDER BY, LIMIT, OFFSET for union component
1454
1533
  const { sql, nestedPaths } = this.buildQueryCore(selectionResult, queryContext, false);
1455
1534
  // Update context's param counter
@@ -1517,7 +1596,7 @@ class SelectQueryBuilder {
1517
1596
  executor: this.executor,
1518
1597
  };
1519
1598
  const mockRow = this._createMockRow();
1520
- const selectionResult = this.selector(mockRow);
1599
+ const selectionResult = (0, exports.materializeMockSelection)(this.selector(mockRow));
1521
1600
  const { sql, params, nestedPaths } = this.buildQuery(selectionResult, context);
1522
1601
  // Create transform function that captures current state
1523
1602
  const transformFn = (rows) => {
@@ -1564,7 +1643,7 @@ class SelectQueryBuilder {
1564
1643
  executor: this.executor,
1565
1644
  };
1566
1645
  const mockRow = this._createMockRow();
1567
- const selectionResult = this.selector(mockRow);
1646
+ const selectionResult = (0, exports.materializeMockSelection)(this.selector(mockRow));
1568
1647
  const { sql, params, nestedPaths } = this.buildQuery(selectionResult, context);
1569
1648
  // Restore original limit
1570
1649
  this.limitValue = originalLimit;
@@ -1793,7 +1872,7 @@ class SelectQueryBuilder {
1793
1872
  }));
1794
1873
  // Analyze the selector to extract nested queries
1795
1874
  const mockRow = tracer.trace('createMockRow', () => this._createMockRow());
1796
- const selectionResult = tracer.trace('evaluateSelector', () => this.selector(mockRow));
1875
+ const selectionResult = tracer.trace('evaluateSelector', () => (0, exports.materializeMockSelection)(this.selector(mockRow)));
1797
1876
  // Check if we're using temp table strategy and have collections
1798
1877
  const collections = tracer.trace('detectCollections', () => this.detectCollections(selectionResult));
1799
1878
  const useTempTableStrategy = this.collectionStrategy === 'temptable' && collections.length > 0;
@@ -2055,7 +2134,7 @@ class SelectQueryBuilder {
2055
2134
  'jsonb',
2056
2135
  ]);
2057
2136
  try {
2058
- const selected = b.selector(b.createMockItem());
2137
+ const selected = (0, exports.materializeMockSelection)(b.selector(b.createMockItem()));
2059
2138
  for (const value of Object.values(selected)) {
2060
2139
  if (!(value && typeof value === 'object' && '__dbColumnName' in value)) {
2061
2140
  return false;
@@ -2120,10 +2199,9 @@ class SelectQueryBuilder {
2120
2199
  // Find primary key column from schema, fallback to "id" if not found
2121
2200
  let pkColumn = null;
2122
2201
  if (targetSchema) {
2123
- for (const colBuilder of Object.values(targetSchema.columns)) {
2124
- const config = colBuilder.build();
2125
- if (config.primaryKey) {
2126
- pkColumn = config.name;
2202
+ for (const meta of getSchemaColumnMeta(targetSchema).values()) {
2203
+ if (meta.primaryKey && meta.name != null) {
2204
+ pkColumn = meta.name;
2127
2205
  break;
2128
2206
  }
2129
2207
  }
@@ -2504,7 +2582,7 @@ class SelectQueryBuilder {
2504
2582
  };
2505
2583
  // Analyze the selector to extract nested queries
2506
2584
  const mockRow = this._createMockRow();
2507
- const selectionResult = this.selector(mockRow);
2585
+ const selectionResult = (0, exports.materializeMockSelection)(this.selector(mockRow));
2508
2586
  // Build the query - this populates context.placeholders
2509
2587
  const { sql, nestedPaths } = this.buildQuery(selectionResult, context);
2510
2588
  // Create transform function (closure over schema, selection, nestedPaths)
@@ -2893,7 +2971,7 @@ class SelectQueryBuilder {
2893
2971
  }
2894
2972
  // Selector function - extract selected columns
2895
2973
  const mockRow = this._createMockRow();
2896
- const selectedMock = this.selector(mockRow);
2974
+ const selectedMock = (0, exports.materializeMockSelection)(this.selector(mockRow));
2897
2975
  const selection = returning(selectedMock);
2898
2976
  if (typeof selection === 'object' && selection !== null) {
2899
2977
  const columns = [];
@@ -2933,12 +3011,14 @@ class SelectQueryBuilder {
2933
3011
  mapDeleteReturningResults(rows, returning, fragmentMappers) {
2934
3012
  if (returning === true) {
2935
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);
2936
3017
  return rows.map(row => {
2937
3018
  const mapped = {};
2938
- for (const [propName, colBuilder] of Object.entries(this.schema.columns)) {
2939
- const config = colBuilder.build();
2940
- const dbValue = row[config.name];
2941
- 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;
2942
3022
  }
2943
3023
  return mapped;
2944
3024
  });
@@ -2954,15 +3034,13 @@ class SelectQueryBuilder {
2954
3034
  continue;
2955
3035
  }
2956
3036
  // Try to find column by alias or name
2957
- const colEntry = Object.entries(this.schema.columns).find(([propName, col]) => {
2958
- const config = col.build();
2959
- return propName === key || config.name === key;
3037
+ const colEntry = [...getSchemaColumnMeta(this.schema)].find(([propName, meta]) => {
3038
+ return propName === key || meta.name === key;
2960
3039
  });
2961
3040
  if (colEntry) {
2962
- const [, col] = colEntry;
2963
- const config = col.build();
3041
+ const [, meta] = colEntry;
2964
3042
  // Apply fromDriver mapper if present
2965
- mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
3043
+ mapped[key] = meta.mapper ? meta.mapper.fromDriver(value) : value;
2966
3044
  }
2967
3045
  else {
2968
3046
  mapped[key] = value;
@@ -2983,7 +3061,7 @@ class SelectQueryBuilder {
2983
3061
  }
2984
3062
  // Analyze the returning selector
2985
3063
  const mockRow = this._createMockRow();
2986
- const selectedMock = this.selector(mockRow);
3064
+ const selectedMock = (0, exports.materializeMockSelection)(this.selector(mockRow));
2987
3065
  const selection = returning(selectedMock);
2988
3066
  if (typeof selection !== 'object' || selection === null) {
2989
3067
  return null;
@@ -3071,7 +3149,7 @@ class SelectQueryBuilder {
3071
3149
  const selectParts = [];
3072
3150
  const nestedPaths = new Set();
3073
3151
  const mockRow = this._createMockRow();
3074
- const selectedMock = this.selector(mockRow);
3152
+ const selectedMock = (0, exports.materializeMockSelection)(this.selector(mockRow));
3075
3153
  const selection = returning(selectedMock);
3076
3154
  // Helper to get FK db column name from a schema
3077
3155
  const getFkDbColumnName = (sourceSchema, fkPropName) => {
@@ -3340,37 +3418,61 @@ ${joinClauses.join('\n')}`;
3340
3418
  * @internal
3341
3419
  */
3342
3420
  _createMockRow() {
3343
- 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() {
3344
3447
  const tableAlias = this.schema.name;
3345
- const chainId = this.chainId;
3346
3448
  // Performance: Use pre-computed column name map if available
3347
3449
  const columnNameMap = getColumnNameMapForSchema(this.schema);
3348
- // Performance: Lazy-cache FieldRef objects
3349
- const fieldRefCache = {};
3350
3450
  // Build a mapper lookup for columns (only when needed)
3351
3451
  const columnMappers = {};
3352
3452
  const columnSqlTypes = {};
3353
- for (const [colName, colBuilder] of Object.entries(this.schema.columns)) {
3354
- const config = colBuilder.build();
3355
- if (config.mapper) {
3356
- columnMappers[colName] = config.mapper;
3453
+ for (const [colName, meta] of getSchemaColumnMeta(this.schema)) {
3454
+ if (meta.mapper) {
3455
+ columnMappers[colName] = meta.mapper;
3357
3456
  }
3358
- if (config.type) {
3359
- columnSqlTypes[colName] = config.type;
3457
+ if (meta.type) {
3458
+ columnSqlTypes[colName] = meta.type;
3360
3459
  }
3361
3460
  }
3461
+ const descriptors = {};
3362
3462
  // Add columns as FieldRef objects - type-safe with property name and database column name
3363
3463
  for (const [colName, dbColumnName] of columnNameMap) {
3364
3464
  const mapper = columnMappers[colName];
3365
- Object.defineProperty(mock, colName, {
3465
+ descriptors[colName] = {
3366
3466
  get() {
3467
+ const slots = this;
3468
+ const fieldRefCache = slots[MOCK_ROW_FIELD_REFS] ?? (slots[MOCK_ROW_FIELD_REFS] = {});
3367
3469
  let cached = fieldRefCache[colName];
3368
3470
  if (!cached) {
3369
3471
  cached = fieldRefCache[colName] = {
3370
3472
  __fieldName: colName,
3371
3473
  __dbColumnName: dbColumnName,
3372
3474
  __tableAlias: tableAlias,
3373
- __chainId: chainId,
3475
+ __chainId: slots[MOCK_ROW_CHAIN_ID],
3374
3476
  // Include mapper for toDriver transformation in conditions
3375
3477
  __mapper: mapper,
3376
3478
  // Column SQL type — lets flag* emit width-exact mask casts
@@ -3381,48 +3483,66 @@ ${joinClauses.join('\n')}`;
3381
3483
  },
3382
3484
  enumerable: true,
3383
3485
  configurable: true,
3384
- });
3486
+ };
3385
3487
  }
3488
+ // Captured at descriptor-build time — identical for every row of this signature.
3489
+ const manualJoins = this.manualJoins;
3386
3490
  // Add columns from manually joined tables
3387
- for (const join of this.manualJoins) {
3491
+ for (const join of manualJoins) {
3388
3492
  // Skip subquery joins (they don't have a schema)
3389
3493
  if (join.isSubquery || !join.schema) {
3390
3494
  continue;
3391
3495
  }
3392
- // Performance: Use pre-computed column name map for joined schema
3393
- const joinColumnNameMap = getColumnNameMapForSchema(join.schema);
3394
- if (!mock[join.alias]) {
3395
- mock[join.alias] = {};
3396
- }
3397
- // Lazy-cache for joined table
3398
- 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;
3399
3500
  const joinAlias = join.alias;
3400
- for (const [colName, dbColumnName] of joinColumnNameMap) {
3401
- Object.defineProperty(mock[join.alias], colName, {
3402
- get() {
3403
- let cached = joinFieldRefCache[colName];
3404
- if (!cached) {
3405
- cached = joinFieldRefCache[colName] = {
3406
- __fieldName: colName,
3407
- __dbColumnName: dbColumnName,
3408
- __tableAlias: joinAlias,
3409
- };
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
+ });
3410
3526
  }
3411
- return cached;
3412
- },
3413
- enumerable: true,
3414
- configurable: true,
3415
- });
3416
- }
3527
+ }
3528
+ return sub;
3529
+ },
3530
+ enumerable: true,
3531
+ configurable: true,
3532
+ };
3417
3533
  }
3418
3534
  // Performance: Use pre-computed relation entries
3419
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;
3420
3540
  // Add relations as CollectionQueryBuilder or ReferenceQueryBuilder
3421
3541
  for (const [relName, relConfig] of relationEntries) {
3422
3542
  // Try to get target schema from registry (preferred, has full relations) or cached schema
3423
3543
  let targetSchema;
3424
- if (this.schemaRegistry) {
3425
- targetSchema = this.schemaRegistry.get(relConfig.targetTable);
3544
+ if (schemaRegistry) {
3545
+ targetSchema = schemaRegistry.get(relConfig.targetTable);
3426
3546
  }
3427
3547
  if (!targetSchema) {
3428
3548
  // Performance: Use cached target schema
@@ -3430,36 +3550,36 @@ ${joinClauses.join('\n')}`;
3430
3550
  }
3431
3551
  if (relConfig.type === 'many') {
3432
3552
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
3433
- Object.defineProperty(mock, relName, {
3553
+ descriptors[relName] = {
3434
3554
  get: () => {
3435
- return new CollectionQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKey || relConfig.foreignKeys?.[0] || '', this.schema.name, targetSchema, // Pass the target schema directly
3436
- 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
3437
3557
  undefined, relConfig.foreignKeys, // Propagate composite FK / literal predicates
3438
3558
  relConfig.matches);
3439
3559
  },
3440
3560
  enumerable: false,
3441
3561
  configurable: true,
3442
- });
3562
+ };
3443
3563
  }
3444
3564
  else {
3445
3565
  // For single reference (many-to-one), create a ReferenceQueryBuilder
3446
3566
  // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
3447
- Object.defineProperty(mock, relName, {
3567
+ descriptors[relName] = {
3448
3568
  get: () => {
3449
3569
  const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, // Pass the target schema directly
3450
- this.schemaRegistry, // Pass schema registry for nested resolution
3570
+ schemaRegistry, // Pass schema registry for nested resolution
3451
3571
  [], // Empty navigation path for first level navigation
3452
- this.schema.name // Pass source table name for lateral join correlation
3572
+ sourceTableName // Pass source table name for lateral join correlation
3453
3573
  );
3454
3574
  // Return a mock object that exposes the target table's columns
3455
3575
  return refBuilder.createMockTargetRow();
3456
3576
  },
3457
3577
  enumerable: false,
3458
3578
  configurable: true,
3459
- });
3579
+ };
3460
3580
  }
3461
3581
  }
3462
- return mock;
3582
+ return descriptors;
3463
3583
  }
3464
3584
  /**
3465
3585
  * Create a proxy that wraps selected values and returns FieldRefs for property access
@@ -5504,7 +5624,7 @@ ${joinClauses.join('\n')}`;
5504
5624
  };
5505
5625
  // Analyze the selector to extract nested queries
5506
5626
  const mockRow = this._createMockRow();
5507
- const selectionResult = this.selector(mockRow);
5627
+ const selectionResult = (0, exports.materializeMockSelection)(this.selector(mockRow));
5508
5628
  // Build the query
5509
5629
  const { sql } = this.buildQuery(selectionResult, context);
5510
5630
  // Update the outer context's param counter
@@ -5665,13 +5785,12 @@ class ReferenceQueryBuilder {
5665
5785
  // Build a mapper lookup for columns (only when needed)
5666
5786
  const columnMappers = {};
5667
5787
  const columnSqlTypes = {};
5668
- for (const [colName, colBuilder] of Object.entries(this.targetTableSchema.columns)) {
5669
- const config = colBuilder.build();
5670
- if (config.mapper) {
5671
- columnMappers[colName] = config.mapper;
5788
+ for (const [colName, meta] of getSchemaColumnMeta(this.targetTableSchema)) {
5789
+ if (meta.mapper) {
5790
+ columnMappers[colName] = meta.mapper;
5672
5791
  }
5673
- if (config.type) {
5674
- columnSqlTypes[colName] = config.type;
5792
+ if (meta.type) {
5793
+ columnSqlTypes[colName] = meta.type;
5675
5794
  }
5676
5795
  }
5677
5796
  const sourceTable = this.targetTable; // Actual table name for schema lookup
@@ -5878,74 +5997,16 @@ class CollectionQueryBuilder {
5878
5997
  return this._cachedMockItem;
5879
5998
  }
5880
5999
  if (this.targetTableSchema) {
5881
- // If we have schema information, create a properly typed mock
5882
- const mock = {};
5883
- // Performance: Use pre-computed column name map if available
5884
- const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
5885
- // Performance: Lazy-cache FieldRef objects
5886
- const fieldRefCache = {};
5887
- // Add columns - include tableAlias for unambiguous column references in WHERE clauses
5888
- // Use a special marker alias for the collection's own table that can be rewritten later
5889
- // This allows distinguishing between outer table references and inner collection references
5890
- // when both target the same table (e.g., post.user.posts where both are "posts" table)
5891
- const tableAlias = `__collection_${this.targetTable}__`;
5892
- for (const [colName, dbColumnName] of columnNameMap) {
5893
- Object.defineProperty(mock, colName, {
5894
- get() {
5895
- let cached = fieldRefCache[colName];
5896
- if (!cached) {
5897
- cached = fieldRefCache[colName] = {
5898
- __fieldName: colName,
5899
- __dbColumnName: dbColumnName,
5900
- __tableAlias: tableAlias, // Include table alias for unambiguous references
5901
- };
5902
- }
5903
- return cached;
5904
- },
5905
- enumerable: true,
5906
- configurable: true,
5907
- });
5908
- }
5909
- // Add navigation properties (both collections and references)
5910
- if (this.targetTableSchema.relations) {
5911
- for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
5912
- if (relConfig.type === 'many') {
5913
- // Collection navigation
5914
- // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
5915
- Object.defineProperty(mock, relName, {
5916
- get: () => {
5917
- // Don't call build() - it returns schema without relations
5918
- const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
5919
- return new CollectionQueryBuilder(relName, relConfig.targetTable, fk, this.targetTable, undefined, // Don't pass schema, force registry lookup
5920
- this.schemaRegistry, // Pass schema registry for nested resolution
5921
- // No navigation path needed here - direct collection access from parent
5922
- undefined, relConfig.foreignKeys, // Propagate composite FK / literal predicates
5923
- relConfig.matches);
5924
- },
5925
- enumerable: false,
5926
- configurable: true,
5927
- });
5928
- }
5929
- else {
5930
- // Reference navigation
5931
- // Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
5932
- Object.defineProperty(mock, relName, {
5933
- get: () => {
5934
- // Don't call build() - it returns schema without relations
5935
- // Instead, pass undefined and let ReferenceQueryBuilder look it up from registry
5936
- const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, undefined, // Don't pass schema, force registry lookup
5937
- this.schemaRegistry, // Pass schema registry for nested resolution
5938
- [], // Empty navigation path - this is the first reference in the chain
5939
- this.targetTable // Source alias is this collection's target table
5940
- );
5941
- return refBuilder.createMockTargetRow();
5942
- },
5943
- enumerable: false,
5944
- configurable: true,
5945
- });
5946
- }
5947
- }
5948
- }
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] = {};
5949
6010
  // Cache the mock for reuse
5950
6011
  this._cachedMockItem = mock;
5951
6012
  return mock;
@@ -5955,6 +6016,85 @@ class CollectionQueryBuilder {
5955
6016
  return createNestedFieldRefProxy(this.targetTable);
5956
6017
  }
5957
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
+ }
5958
6098
  /**
5959
6099
  * Limit collection items
5960
6100
  */
@@ -6759,7 +6899,7 @@ class CollectionQueryBuilder {
6759
6899
  // (field collection, aggregate-expression discovery, navigation-join detection) all
6760
6900
  // need to walk the same selection, and re-invoking the selector is expensive
6761
6901
  // (rebuilds proxy mocks and any nested CollectionQueryBuilder instances).
6762
- const selectorResult = this.selector ? this.selector(this.createMockItem()) : undefined;
6902
+ const selectorResult = this.selector ? (0, exports.materializeMockSelection)(this.selector(this.createMockItem())) : undefined;
6763
6903
  // Step 1: Build field selection configuration
6764
6904
  if (this.selector) {
6765
6905
  const selectedFields = selectorResult;
@@ -6836,9 +6976,8 @@ class CollectionQueryBuilder {
6836
6976
  let dbToPropertyMap = null;
6837
6977
  if (this.targetTableSchema) {
6838
6978
  dbToPropertyMap = new Map();
6839
- for (const [propName, colBuilder] of Object.entries(this.targetTableSchema.columns)) {
6840
- const config = colBuilder.build();
6841
- dbToPropertyMap.set(config.name, propName);
6979
+ for (const [propName, meta] of getSchemaColumnMeta(this.targetTableSchema)) {
6980
+ dbToPropertyMap.set(meta.name, propName);
6842
6981
  }
6843
6982
  }
6844
6983
  const orderPartsDb = this.orderByFields.map(({ field, direction }) => {