joist-core 2.2.0-next.2 → 2.2.0-next.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/build/BaseEntity.js +1 -1
  2. package/build/BaseEntity.js.map +1 -1
  3. package/build/EntityManager.d.ts +3 -0
  4. package/build/EntityManager.d.ts.map +1 -1
  5. package/build/EntityManager.js +164 -87
  6. package/build/EntityManager.js.map +1 -1
  7. package/build/EntityMetadata.d.ts +17 -1
  8. package/build/EntityMetadata.d.ts.map +1 -1
  9. package/build/EntityMetadata.js.map +1 -1
  10. package/build/IndexManager.d.ts +5 -5
  11. package/build/IndexManager.d.ts.map +1 -1
  12. package/build/IndexManager.js +48 -32
  13. package/build/IndexManager.js.map +1 -1
  14. package/build/InstanceData.d.ts +16 -2
  15. package/build/InstanceData.d.ts.map +1 -1
  16. package/build/InstanceData.js +36 -3
  17. package/build/InstanceData.js.map +1 -1
  18. package/build/IsLoadedCache.d.ts.map +1 -1
  19. package/build/IsLoadedCache.js +31 -25
  20. package/build/IsLoadedCache.js.map +1 -1
  21. package/build/QueryParser.collectionJoins.js +8 -2
  22. package/build/QueryParser.collectionJoins.js.map +1 -1
  23. package/build/QueryParser.d.ts +4 -2
  24. package/build/QueryParser.d.ts.map +1 -1
  25. package/build/QueryParser.js.map +1 -1
  26. package/build/QueryParser.pruning.d.ts.map +1 -1
  27. package/build/QueryParser.pruning.js +8 -1
  28. package/build/QueryParser.pruning.js.map +1 -1
  29. package/build/ReactionsManager.d.ts +2 -1
  30. package/build/ReactionsManager.d.ts.map +1 -1
  31. package/build/ReactionsManager.js +53 -49
  32. package/build/ReactionsManager.js.map +1 -1
  33. package/build/batchloaders/BatchLoader.d.ts.map +1 -1
  34. package/build/batchloaders/BatchLoader.js +6 -1
  35. package/build/batchloaders/BatchLoader.js.map +1 -1
  36. package/build/batchloaders/populateBatchLoader.d.ts.map +1 -1
  37. package/build/batchloaders/populateBatchLoader.js +26 -8
  38. package/build/batchloaders/populateBatchLoader.js.map +1 -1
  39. package/build/configure.d.ts +3 -3
  40. package/build/configure.d.ts.map +1 -1
  41. package/build/configure.js +64 -0
  42. package/build/configure.js.map +1 -1
  43. package/build/dataloaders/fastWhereFilterHash.d.ts +15 -0
  44. package/build/dataloaders/fastWhereFilterHash.d.ts.map +1 -0
  45. package/build/dataloaders/fastWhereFilterHash.js +164 -0
  46. package/build/dataloaders/fastWhereFilterHash.js.map +1 -0
  47. package/build/dataloaders/fastWhereFilterHash.test.d.ts +2 -0
  48. package/build/dataloaders/fastWhereFilterHash.test.d.ts.map +1 -0
  49. package/build/dataloaders/fastWhereFilterHash.test.js +59 -0
  50. package/build/dataloaders/fastWhereFilterHash.test.js.map +1 -0
  51. package/build/dataloaders/findCountDataLoader.d.ts +1 -2
  52. package/build/dataloaders/findCountDataLoader.d.ts.map +1 -1
  53. package/build/dataloaders/findCountDataLoader.js +16 -13
  54. package/build/dataloaders/findCountDataLoader.js.map +1 -1
  55. package/build/dataloaders/findDataLoader.d.ts +9 -4
  56. package/build/dataloaders/findDataLoader.d.ts.map +1 -1
  57. package/build/dataloaders/findDataLoader.js +120 -88
  58. package/build/dataloaders/findDataLoader.js.map +1 -1
  59. package/build/dataloaders/findIdsDataLoader.d.ts +1 -2
  60. package/build/dataloaders/findIdsDataLoader.d.ts.map +1 -1
  61. package/build/dataloaders/findIdsDataLoader.js +16 -15
  62. package/build/dataloaders/findIdsDataLoader.js.map +1 -1
  63. package/build/dataloaders/findOrCreateDataLoader.d.ts.map +1 -1
  64. package/build/dataloaders/findOrCreateDataLoader.js +3 -3
  65. package/build/dataloaders/findOrCreateDataLoader.js.map +1 -1
  66. package/build/dataloaders/findPaginatedDataLoader.d.ts +1 -2
  67. package/build/dataloaders/findPaginatedDataLoader.d.ts.map +1 -1
  68. package/build/dataloaders/findPaginatedDataLoader.js +33 -25
  69. package/build/dataloaders/findPaginatedDataLoader.js.map +1 -1
  70. package/build/drivers/EntityWriter.js +13 -7
  71. package/build/drivers/EntityWriter.js.map +1 -1
  72. package/build/drivers/buildRawQuery.d.ts.map +1 -1
  73. package/build/drivers/buildRawQuery.js +5 -1
  74. package/build/drivers/buildRawQuery.js.map +1 -1
  75. package/build/fields.d.ts.map +1 -1
  76. package/build/fields.js +16 -6
  77. package/build/fields.js.map +1 -1
  78. package/build/keys.d.ts.map +1 -1
  79. package/build/keys.js +8 -6
  80. package/build/keys.js.map +1 -1
  81. package/build/loadHints.d.ts +9 -0
  82. package/build/loadHints.d.ts.map +1 -1
  83. package/build/loadHints.js +63 -9
  84. package/build/loadHints.js.map +1 -1
  85. package/build/loadLens.d.ts +11 -0
  86. package/build/loadLens.d.ts.map +1 -1
  87. package/build/loadLens.js +43 -10
  88. package/build/loadLens.js.map +1 -1
  89. package/build/normalizeHints.d.ts +7 -0
  90. package/build/normalizeHints.d.ts.map +1 -1
  91. package/build/normalizeHints.js +40 -2
  92. package/build/normalizeHints.js.map +1 -1
  93. package/build/relations/RecursiveCollection.d.ts +2 -6
  94. package/build/relations/RecursiveCollection.d.ts.map +1 -1
  95. package/build/relations/RecursiveCollection.js +9 -17
  96. package/build/relations/RecursiveCollection.js.map +1 -1
  97. package/build/relations/RecursiveCycleError.d.ts +13 -0
  98. package/build/relations/RecursiveCycleError.d.ts.map +1 -0
  99. package/build/relations/RecursiveCycleError.js +15 -0
  100. package/build/relations/RecursiveCycleError.js.map +1 -0
  101. package/build/relations/hasOneThrough.d.ts.map +1 -1
  102. package/build/relations/hasOneThrough.js +6 -4
  103. package/build/relations/hasOneThrough.js.map +1 -1
  104. package/build/relations/hasProperty.d.ts +2 -1
  105. package/build/relations/hasProperty.d.ts.map +1 -1
  106. package/build/relations/hasProperty.js +15 -5
  107. package/build/relations/hasProperty.js.map +1 -1
  108. package/build/utils.d.ts +2 -0
  109. package/build/utils.d.ts.map +1 -1
  110. package/build/utils.js +12 -0
  111. package/build/utils.js.map +1 -1
  112. package/package.json +3 -5
  113. package/build/caches.d.ts +0 -6
  114. package/build/caches.d.ts.map +0 -1
  115. package/build/caches.js +0 -42
  116. package/build/caches.js.map +0 -1
@@ -29,7 +29,6 @@ const IndexManager_1 = require("./IndexManager");
29
29
  // We alias `Entity => EntityW` to denote "Entity wide" i.e. the non-narrowed Entity
30
30
  const loadBatchLoader_1 = require("./batchloaders/loadBatchLoader");
31
31
  const populateBatchLoader_1 = require("./batchloaders/populateBatchLoader");
32
- const caches_1 = require("./caches");
33
32
  const config_1 = require("./config");
34
33
  const configure_1 = require("./configure");
35
34
  const findByUniqueDataLoader_1 = require("./dataloaders/findByUniqueDataLoader");
@@ -43,6 +42,7 @@ const FlushLock_1 = require("./FlushLock");
43
42
  const index_1 = require("./index");
44
43
  const IsLoadedCache_1 = require("./IsLoadedCache");
45
44
  const JoinRows_1 = require("./JoinRows");
45
+ const loadHints_1 = require("./loadHints");
46
46
  const newEntity_1 = require("./newEntity");
47
47
  const newTestInstance_1 = require("./newTestInstance");
48
48
  const PluginManager_1 = require("./PluginManager");
@@ -88,6 +88,8 @@ class EntityManager {
88
88
  txn;
89
89
  entityLimit = defaultEntityLimit;
90
90
  #entitiesArray = [];
91
+ // Incrementally track dirty entities so we don't have to scan `#entityArray` during flush
92
+ #maybePendingFlushEntities = new Set();
91
93
  // Indexes the currently loaded entities by their tagged ids and `toTaggedString` ids (i.e. `a#`). This fixes
92
94
  // real-world performance issues where `findExistingInstance` scanning `#entities` was an `O(n^2)`.
93
95
  #entitiesById = new Map();
@@ -106,6 +108,7 @@ class EntityManager {
106
108
  * so that both see the most accurate state.
107
109
  */
108
110
  #pendingDeletes = [];
111
+ #hasAnyDeletes = false;
109
112
  #dataloaders = {};
110
113
  #batchLoaders = {};
111
114
  #joinRows = {};
@@ -155,6 +158,15 @@ class EntityManager {
155
158
  indexManager: this.#indexManager,
156
159
  isLoadedCache: this.#isLoadedCache,
157
160
  pluginManager,
161
+ markMaybePending(entity) {
162
+ em.#maybePendingFlushEntities.add(entity);
163
+ },
164
+ unmarkMaybePending(entity) {
165
+ em.#maybePendingFlushEntities.delete(entity);
166
+ },
167
+ hasAnyDeletes() {
168
+ return em.#hasAnyDeletes;
169
+ },
158
170
  isMerging(entity) {
159
171
  return em.#merging?.has(entity) ?? false;
160
172
  },
@@ -248,12 +260,9 @@ class EntityManager {
248
260
  async find(type, where, options) {
249
261
  const { populate, ...rest } = options || {};
250
262
  const settings = { where, ...rest };
251
- const loader = hasPaginationSettings(rest)
263
+ const result = await (hasPaginationSettings(rest)
252
264
  ? (0, findPaginatedDataLoader_1.findPaginatedDataLoader)(this, type, settings, populate)
253
- : (0, findDataLoader_1.findDataLoader)(this, type, settings, populate);
254
- const result = await loader
255
- .load(settings)
256
- .catch(function find(err) {
265
+ : (0, findDataLoader_1.findDataLoader)(this, type, settings, populate)).catch(function find(err) {
257
266
  throw appendStack(err, new Error());
258
267
  });
259
268
  if (populate) {
@@ -348,6 +357,9 @@ class EntityManager {
348
357
  }
349
358
  else {
350
359
  const [entity] = this.hydrate(type, [row]);
360
+ if (this.#hasAnyDeletes && entity.isDeletedEntity) {
361
+ return undefined;
362
+ }
351
363
  if (populate) {
352
364
  await this.populate(entity, populate);
353
365
  }
@@ -364,9 +376,7 @@ class EntityManager {
364
376
  */
365
377
  async findCount(type, where, options = {}) {
366
378
  const settings = { where, ...options };
367
- let count = await (0, findCountDataLoader_1.findCountDataLoader)(this, type, settings)
368
- .load(settings)
369
- .catch(function findCount(err) {
379
+ let count = await (0, findCountDataLoader_1.findCountDataLoader)(this, type, settings).catch(function findCount(err) {
370
380
  throw appendStack(err, new Error());
371
381
  });
372
382
  // If the user is do "count all", we can adjust the number up/down based on
@@ -398,9 +408,7 @@ class EntityManager {
398
408
  */
399
409
  async findIds(type, where, options = {}) {
400
410
  const settings = { where, ...options };
401
- return (0, findIdsDataLoader_1.findIdsDataLoader)(this, type, settings)
402
- .load(settings)
403
- .catch(function findIds(err) {
411
+ return (0, findIdsDataLoader_1.findIdsDataLoader)(this, type, settings).catch(function findIds(err) {
404
412
  throw appendStack(err, new Error());
405
413
  });
406
414
  }
@@ -689,48 +697,86 @@ class EntityManager {
689
697
  }
690
698
  async loadAll(type, _ids, hint) {
691
699
  const meta = (0, index_1.getMetadata)(type);
692
- const ids = _ids.map((id) => (0, index_1.tagId)(meta, id));
693
- const idsToLoad = ids.filter((id) => !this.findExistingInstance(id));
694
- if (idsToLoad.length > 0) {
700
+ // Use pre-allocated arrays/for loops instead of `.filter`s since this can be a hot spot
701
+ const ids = new Array(_ids.length);
702
+ const entities = new Array(_ids.length);
703
+ let idsToLoad;
704
+ let positionsToLoad;
705
+ for (let i = 0; i < _ids.length; i++) {
706
+ const id = (0, index_1.tagId)(meta, _ids[i]);
707
+ ids[i] = id;
708
+ const entity = this.findExistingInstance(id);
709
+ if (entity) {
710
+ entities[i] = entity;
711
+ }
712
+ else {
713
+ (idsToLoad ??= []).push(id);
714
+ (positionsToLoad ??= []).push(i);
715
+ }
716
+ }
717
+ if (idsToLoad && idsToLoad.length > 0) {
695
718
  await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
696
719
  .loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
697
720
  .catch(function loadAll(err) {
698
721
  throw appendStack(err, new Error());
699
722
  });
723
+ for (const i of positionsToLoad) {
724
+ entities[i] = this.findExistingInstance(ids[i]);
725
+ }
700
726
  }
701
- const entities = [];
702
- for (const id of ids) {
703
- const entity = this.findExistingInstance(id);
704
- if (entity)
705
- entities.push(entity);
727
+ let idsNotFound;
728
+ for (let i = 0; i < entities.length; i++) {
729
+ if (entities[i] === undefined) {
730
+ (idsNotFound ??= []).push(ids[i]);
731
+ }
706
732
  }
707
- if (entities.length !== ids.length) {
708
- const idsNotFound = ids.filter((_, i) => entities[i] === undefined);
733
+ if (idsNotFound) {
709
734
  throw new NotFoundError(`${idsNotFound.join(",")} were not found`);
710
735
  }
736
+ const loadedEntities = entities;
711
737
  if (hint) {
712
- await this.populate(entities, hint);
738
+ await this.populate(loadedEntities, hint);
713
739
  }
714
740
  if (meta.inheritanceType === "sti" && meta.baseType) {
715
- const wrongType = entities.filter((e) => !(e instanceof meta.cstr));
741
+ const wrongType = loadedEntities.filter((e) => !(e instanceof meta.cstr));
716
742
  if (wrongType.length > 0) {
717
743
  throw new Error(`${wrongType.join(", ")} were not of type ${meta.cstr.name}`);
718
744
  }
719
745
  }
720
- return entities;
746
+ return loadedEntities;
721
747
  }
722
748
  async loadAllIfExists(type, _ids, hint) {
723
749
  const meta = (0, index_1.getMetadata)(type);
724
- const ids = _ids.map((id) => (0, index_1.tagId)(meta, id));
725
- const idsToLoad = ids.filter((id) => !this.findExistingInstance(id));
726
- if (idsToLoad.length > 0) {
750
+ // Use pre-allocated arrays/for loops instead of `.filter`s since this can be a hot spot
751
+ const ids = new Array(_ids.length);
752
+ const entities = [];
753
+ let idsToLoad;
754
+ // Ensure the ids are tagged, and find any not-yet-loaded
755
+ for (let i = 0; i < _ids.length; i++) {
756
+ const id = (0, index_1.tagId)(meta, _ids[i]);
757
+ ids[i] = id;
758
+ const entity = this.findExistingInstance(id);
759
+ if (entity) {
760
+ entities.push(entity);
761
+ }
762
+ else {
763
+ (idsToLoad ??= []).push(id);
764
+ }
765
+ }
766
+ if (idsToLoad && idsToLoad.length > 0) {
727
767
  await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
728
768
  .loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
729
769
  .catch(function loadAllIfExists(err) {
730
770
  throw appendStack(err, new Error());
731
771
  });
772
+ // Now that everything is loaded, recalc `entities`
773
+ entities.length = 0;
774
+ for (const id of ids) {
775
+ const entity = this.findExistingInstance(id);
776
+ if (entity)
777
+ entities.push(entity);
778
+ }
732
779
  }
733
- const entities = ids.map((id) => this.findExistingInstance(id)).filter(Boolean);
734
780
  if (hint) {
735
781
  await this.populate(entities, hint);
736
782
  }
@@ -781,6 +827,12 @@ class EntityManager {
781
827
  if (list.length === 0) {
782
828
  return !fn ? entityOrList : fn(entityOrList);
783
829
  }
830
+ // Avoid building a HintTree/batchloader for persisted entities when everything is already loaded; in the phase 9
831
+ // benchmark this moved already-loaded populates from ~1.10ms to ~0.89ms, and nested already-loaded populates from
832
+ // ~2.39ms to ~1.94ms.
833
+ if (!opts.forceReload && list.every((entity) => !entity.isNewEntity && (0, loadHints_1.isLoadedForPopulate)(entity, hintOpt))) {
834
+ return fn ? fn(entityOrList) : entityOrList;
835
+ }
784
836
  const meta = (0, index_1.getMetadata)(list[0]);
785
837
  if (this.#preloader) {
786
838
  // If we can preload, prevent promise deadlocking by one large-batch preload populate (which can't have
@@ -829,9 +881,11 @@ class EntityManager {
829
881
  delete(entityOrArray) {
830
882
  for (const entity of (0, utils_1.toArray)(entityOrArray)) {
831
883
  // Early return if already deleted.
832
- const alreadyMarked = (0, BaseEntity_1.getInstanceData)(entity).markDeleted(entity);
884
+ const alreadyMarked = (0, BaseEntity_1.getInstanceData)(entity).markDeleted();
833
885
  if (!alreadyMarked)
834
886
  continue;
887
+ // This monotonic flag lets find hot paths skip scanning results until a delete has ever happened in this EM.
888
+ this.#hasAnyDeletes = true;
835
889
  // Any derived fields that read this entity will need recalc-d
836
890
  this.#rm.queueAllDownstreamFields(entity, "deleted");
837
891
  // Synchronously unhook the entity if the relations are loaded
@@ -925,7 +979,7 @@ class EntityManager {
925
979
  const pendingHooks = new Set();
926
980
  // Subset of pendingFlush entities that had hooks invoked in a prior `runHooksOnPendingEntities`
927
981
  const alreadyRanHooks = new Set();
928
- findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
982
+ findPendingFlushEntities(this.#maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
929
983
  // If we're re-looping for AsyncReactiveField, make sure to bump updatedAt
930
984
  // each time, so that for an INSERT-then-UPDATE the triggers don't think the
931
985
  // UPDATE forgot to self-bump updatedAt, and then "helpfully" bump it for us.
@@ -964,7 +1018,7 @@ class EntityManager {
964
1018
  hooksInvoked.add(e);
965
1019
  pendingHooks.clear();
966
1020
  // See if the hooks mutated any new, not-yet-hooksInvoked entities
967
- findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
1021
+ findPendingFlushEntities(this.#maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
968
1022
  // The final run of recalcPendingReactables could have left us with pending type errors and no entities in
969
1023
  // pendingHooks. If so, we need to re-run recalcPendingTypeErrors to get those errors to transition into
970
1024
  // suppressed errors so that we will fail after simpleValidation.
@@ -1099,7 +1153,7 @@ class EntityManager {
1099
1153
  for (const e of createdThenDeleted)
1100
1154
  (0, BaseEntity_1.getInstanceData)(e).fixupCreatedThenDeleted();
1101
1155
  this.#merging?.clear();
1102
- return [...allFlushedEntities];
1156
+ return [...allFlushedEntities].sort((a, b) => (0, BaseEntity_1.getInstanceData)(a).entityIndex - (0, BaseEntity_1.getInstanceData)(b).entityIndex);
1103
1157
  }
1104
1158
  catch (e) {
1105
1159
  if (e instanceof RecursiveCollection_1.RecursiveCycleError) {
@@ -1122,8 +1176,9 @@ class EntityManager {
1122
1176
  throw new index_1.ValidationErrors(message);
1123
1177
  }
1124
1178
  }
1125
- if (e instanceof InMemoryRollbackError)
1126
- return [...allFlushedEntities];
1179
+ if (e instanceof InMemoryRollbackError) {
1180
+ return [...allFlushedEntities].sort((a, b) => (0, BaseEntity_1.getInstanceData)(a).entityIndex - (0, BaseEntity_1.getInstanceData)(b).entityIndex);
1181
+ }
1127
1182
  throw e;
1128
1183
  }
1129
1184
  finally {
@@ -1231,10 +1286,13 @@ class EntityManager {
1231
1286
  */
1232
1287
  hydrate(type, rows, options) {
1233
1288
  const maybeBaseMeta = (0, index_1.getMetadata)(type);
1289
+ const taggedIdPrefix = `${maybeBaseMeta.tagName}:`;
1290
+ const overwriteExisting = options?.overwriteExisting === true;
1234
1291
  let i = 0;
1235
1292
  const entities = new Array(rows.length);
1236
1293
  for (const row of rows) {
1237
- const taggedId = (0, index_1.keyToTaggedId)(maybeBaseMeta, row["id"]) || (0, utils_1.fail)("No id column was available");
1294
+ const id = row["id"];
1295
+ const taggedId = id === undefined || id === null ? (0, utils_1.fail)("No id column was available") : `${taggedIdPrefix}${id}`;
1238
1296
  // See if this is already in our UoW
1239
1297
  let entity = this.findExistingInstance(taggedId);
1240
1298
  if (!entity) {
@@ -1243,23 +1301,36 @@ class EntityManager {
1243
1301
  // Pass id as a hint that we're in hydrate mode
1244
1302
  entity = (0, newEntity_1.newEntity)(this, (0, index_1.asConcreteCstr)(meta.cstr), false);
1245
1303
  (0, BaseEntity_1.getInstanceData)(entity).row = row;
1246
- this.#doRegister(entity, taggedId);
1304
+ this.#doRegister(entity, taggedId, meta, true);
1247
1305
  }
1248
- else if (options?.overwriteExisting === true) {
1306
+ else if (overwriteExisting) {
1249
1307
  // Usually if the entity already exists, we don't write over it, but in this case we assume that
1250
1308
  // `EntityManager.refresh` is telling us to explicitly load the latest data.
1251
1309
  // First swap out the old row with the new row
1252
- (0, BaseEntity_1.getInstanceData)(entity).row = row;
1310
+ const instanceData = (0, BaseEntity_1.getInstanceData)(entity);
1311
+ instanceData.row = row;
1253
1312
  // And then only refresh the data keys that have already been serde-d from rows
1254
1313
  // (this keeps us from deserializing data out of rows that we don't need).
1255
- const { data, originalData } = (0, BaseEntity_1.getInstanceData)(entity);
1256
- const changedFields = entity.changes.fieldsWithoutRelations;
1257
- for (const fieldName of Object.keys(data)) {
1258
- const serde = (0, index_1.getMetadata)(entity).allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1259
- serde.setOnEntity(data, row);
1260
- // Make the field look not-dirty
1261
- if (changedFields.includes(fieldName)) {
1262
- delete originalData[fieldName];
1314
+ const { data } = instanceData;
1315
+ const dataKeys = Object.keys(data);
1316
+ if (dataKeys.length > 0) {
1317
+ const allFields = (0, index_1.getMetadata)(entity).allFields;
1318
+ const changedFields = entity.changes.fieldsWithoutRelations;
1319
+ if (changedFields.length === 0) {
1320
+ for (const fieldName of dataKeys) {
1321
+ const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1322
+ serde.setOnEntity(data, row);
1323
+ }
1324
+ }
1325
+ else {
1326
+ for (const fieldName of dataKeys) {
1327
+ const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1328
+ serde.setOnEntity(data, row);
1329
+ // Make the field look not-dirty
1330
+ if (changedFields.includes(fieldName)) {
1331
+ instanceData.markFieldClean(fieldName);
1332
+ }
1333
+ }
1263
1334
  }
1264
1335
  }
1265
1336
  }
@@ -1268,8 +1339,9 @@ class EntityManager {
1268
1339
  return entities;
1269
1340
  }
1270
1341
  touch(entityOrEntities) {
1271
- for (const entity of (0, utils_1.toArray)(entityOrEntities))
1272
- (0, BaseEntity_1.getInstanceData)(entity).isTouched = true;
1342
+ for (const entity of (0, utils_1.toArray)(entityOrEntities)) {
1343
+ (0, BaseEntity_1.getInstanceData)(entity).markTouched();
1344
+ }
1273
1345
  }
1274
1346
  async recalc(entityOrEntities) {
1275
1347
  // Look for async reactive fields
@@ -1346,9 +1418,16 @@ class EntityManager {
1346
1418
  // Run the beforeDelete hook before we unhook the entity
1347
1419
  const todos = (0, Todo_1.createTodos)(entities);
1348
1420
  await beforeDelete(this.ctx, todos);
1349
- // For all relations, unhook the entity from the other side
1350
- // (...we're using `concat` because `.push(...reallyBigArray)` with ~100k relations can blow the stack size
1351
- relationsToCleanup = relationsToCleanup.concat(entities.flatMap(index_1.getRelations));
1421
+ // For all relations, unhook the entity from the other side; this append path is optimized for
1422
+ // large deletes with ~100k relations by avoiding `flatMap` intermediates and `concat` copies.
1423
+ for (const entity of entities) {
1424
+ const relations = (0, index_1.getRelations)(entity);
1425
+ const start = relationsToCleanup.length;
1426
+ relationsToCleanup.length += relations.length;
1427
+ for (let i = 0; i < relations.length; i++) {
1428
+ relationsToCleanup[start + i] = relations[i];
1429
+ }
1430
+ }
1352
1431
  entities = this.#pendingDeletes;
1353
1432
  this.#pendingDeletes = [];
1354
1433
  }
@@ -1404,9 +1483,9 @@ class EntityManager {
1404
1483
  return entities.filter((e) => e instanceof cstr && !e.isDeletedEntity);
1405
1484
  }
1406
1485
  if (this.#indexManager.shouldIndexType(entities.length)) {
1407
- this.#indexManager.enableIndexingForType(meta, entities);
1486
+ this.#indexManager.enableIndexingForType(meta, entities, where);
1408
1487
  return (this.#indexManager
1409
- .findMatching(meta, where)
1488
+ .findMatching(meta, entities, where)
1410
1489
  // Still filter by `instanceof cstr` to handle subtyping
1411
1490
  .filter((e) => e instanceof cstr && !e.isDeletedEntity));
1412
1491
  }
@@ -1506,7 +1585,8 @@ class EntityManager {
1506
1585
  continue;
1507
1586
  const { originalData: oldOriginalData, data: oldData } = oldInstanceData;
1508
1587
  const newEntity = mapEntity(oldEntity);
1509
- const { originalData: newOriginalData, data: newData } = newEntity.__data;
1588
+ const newInstanceData = newEntity.__data;
1589
+ const { data: newData } = newInstanceData;
1510
1590
  // for new entities, anything in `data` is changed and should be copied across. for existing entities, we
1511
1591
  // only care about changed fields, which are enumerated by originalData
1512
1592
  const maybeEntity = (value) => ((0, Entity_1.isEntity)(value) ? mapEntity(value) : value);
@@ -1514,7 +1594,7 @@ class EntityManager {
1514
1594
  for (const field of fields) {
1515
1595
  // copy over originalData so .changes is consistent across ems
1516
1596
  if (field in oldOriginalData)
1517
- newOriginalData[field] = maybeEntity(oldOriginalData[field]);
1597
+ newInstanceData.markFieldDirty(field, maybeEntity(oldOriginalData[field]));
1518
1598
  newData[field] = maybeEntity(oldData[field]);
1519
1599
  }
1520
1600
  }
@@ -1524,7 +1604,7 @@ class EntityManager {
1524
1604
  const newEntity = mapEntity(oldEntity);
1525
1605
  if (oldEntity.isDeletedEntity) {
1526
1606
  // If the old entity was deleted, that should be persisted in the new em
1527
- newEntity.__data.markDeleted(newEntity);
1607
+ newEntity.__data.markDeleted();
1528
1608
  // deleted entities will fail if you try to `get` their relations, so skip them since they should be cleared
1529
1609
  // out regardless
1530
1610
  continue;
@@ -1669,11 +1749,11 @@ class EntityManager {
1669
1749
  }
1670
1750
  }
1671
1751
  /** Registers a newly-instantiated entity with our EntityManager; only called by #doCreate and hydrate. */
1672
- #doRegister(entity, id) {
1752
+ #doRegister(entity, id, meta, skipDuplicateCheck = false) {
1673
1753
  // Keep our indexes up to date...
1674
1754
  const maybeId = id ?? entity.idTaggedMaybe;
1675
1755
  if (maybeId) {
1676
- if (this.findExistingInstance(maybeId) !== undefined) {
1756
+ if (!skipDuplicateCheck && this.findExistingInstance(maybeId) !== undefined) {
1677
1757
  throw new Error(`Entity ${entity} has a duplicate instance already loaded`);
1678
1758
  }
1679
1759
  this.#entitiesById.set(maybeId, entity);
@@ -1682,8 +1762,9 @@ class EntityManager {
1682
1762
  // Also register by the `a#1` style tagged string for new entities
1683
1763
  this.#entitiesById.set(entity.toTaggedString(), entity);
1684
1764
  }
1765
+ (0, BaseEntity_1.getInstanceData)(entity).entityIndex = this.#entitiesArray.length;
1685
1766
  this.#entitiesArray.push(entity);
1686
- const meta = (0, index_1.getMetadata)(entity);
1767
+ meta ??= (0, index_1.getMetadata)(entity);
1687
1768
  const set = this.#entitiesByTag.get(meta.tagName) ?? [];
1688
1769
  if (set.length === 0)
1689
1770
  this.#entitiesByTag.set(meta.tagName, set);
@@ -1697,8 +1778,6 @@ class EntityManager {
1697
1778
  .join(", ");
1698
1779
  throw new Error(`More than ${this.entityLimit} entities have been instantiated (top entities: ${topTypes})`);
1699
1780
  }
1700
- // If indexing is enabled for this type, add it...
1701
- this.#indexManager.maybeIndexEntity(entity);
1702
1781
  }
1703
1782
  }
1704
1783
  exports.EntityManager = EntityManager;
@@ -1782,14 +1861,14 @@ async function validateReactiveRules(em, logger, todos, joinRowTodos) {
1782
1861
  const p1 = Object.values(todos).flatMap((todo) => {
1783
1862
  const entities = [...todo.inserts, ...todo.updates, ...todo.deletes];
1784
1863
  // Find each statically-declared reactive rule for the given entity type
1785
- const rules = (0, caches_1.getReactiveRules)(todo.metadata);
1864
+ const rules = todo.metadata.reactiveRules;
1786
1865
  return rules.map((rule) => {
1787
1866
  // Of all changed entities of this type, how many specifically trigger this rule?
1788
1867
  const triggered = entities.filter((e) => {
1789
1868
  // If the rule is for a different subtype, skip it
1790
1869
  if (!(e instanceof rule.source))
1791
1870
  return false;
1792
- // Any new-or-deleted entity fires every rule (getReactiveRules has already filtered out read-only)
1871
+ // Any new-or-deleted entity fires every rule (reactiveRules has already filtered out read-only)
1793
1872
  if (e.isNewEntity || e.isDeletedEntity)
1794
1873
  return true;
1795
1874
  // Otherwise see if the changed fields overlaps with the rule's fields
@@ -1807,15 +1886,15 @@ async function validateReactiveRules(em, logger, todos, joinRowTodos) {
1807
1886
  const p2 = Object.values(joinRowTodos).flatMap((todo) => {
1808
1887
  const entities = [...todo.newRows, ...todo.deletedRows].flatMap((jr) => Object.values(jr.columns));
1809
1888
  // Do the first side
1810
- const p1 = (0, caches_1.getReactiveRules)(todo.m2m.meta)
1811
- .filter((rule) => rule.fields.includes(todo.m2m.fieldName))
1889
+ const p1 = todo.m2m.meta
1890
+ .reactiveRules.filter((rule) => rule.fields.includes(todo.m2m.fieldName))
1812
1891
  .map((rule) => {
1813
1892
  const triggered = entities.filter((e) => e instanceof todo.m2m.meta.cstr);
1814
1893
  return followAndQueue(triggered, rule);
1815
1894
  });
1816
1895
  // And the second side
1817
- const p2 = (0, caches_1.getReactiveRules)(todo.m2m.otherMeta)
1818
- .filter((rule) => rule.fields.includes(todo.m2m.otherFieldName))
1896
+ const p2 = todo.m2m.otherMeta
1897
+ .reactiveRules.filter((rule) => rule.fields.includes(todo.m2m.otherFieldName))
1819
1898
  .map((rule) => {
1820
1899
  const triggered = entities.filter((e) => e instanceof todo.m2m.otherMeta.cstr);
1821
1900
  return followAndQueue(triggered, rule);
@@ -2044,7 +2123,7 @@ function maybeBumpUpdatedAt(rm, todos, now) {
2044
2123
  // it has changed. This is technically true, but this will break the oplock SQL generation,
2045
2124
  // so force the field to be dirty.
2046
2125
  const orm = (0, BaseEntity_1.getInstanceData)(e);
2047
- orm.originalData[updatedAt] = (0, fields_1.getField)(e, updatedAt);
2126
+ orm.markFieldDirty(updatedAt, (0, fields_1.getField)(e, updatedAt));
2048
2127
  const serde = todo.metadata.fields[updatedAt].serde;
2049
2128
  orm.data[updatedAt] = serde.mapFromNow(now);
2050
2129
  rm.queueDownstreamReactables(e, updatedAt);
@@ -2081,17 +2160,13 @@ function findConcreteMeta(maybeBaseMeta, row) {
2081
2160
  throw new Error(`${maybeBaseMeta.type} ${(0, index_1.tagId)(maybeBaseMeta, row.id)} must be instantiated via a subtype`);
2082
2161
  }
2083
2162
  // Look for the CTI __class from the driver telling us which subtype to instantiate
2084
- return maybeBaseMeta.subTypes.find((st) => st.type === row.__class) ?? maybeBaseMeta;
2163
+ return maybeBaseMeta.subTypesByType.get(row.__class) ?? maybeBaseMeta;
2085
2164
  }
2086
2165
  else if (maybeBaseMeta.inheritanceType === "sti") {
2087
2166
  // Look for the STI discriminator value
2088
2167
  const baseMeta = (0, index_1.getBaseMeta)(maybeBaseMeta);
2089
- const field = baseMeta.fields[baseMeta.stiDiscriminatorField];
2090
- if (field.kind !== "enum")
2091
- throw new Error("Discriminator field must be an enum");
2092
- const columnName = field.serde.columns[0].columnName;
2093
- const value = row[columnName];
2094
- return baseMeta.subTypes.find((st) => st.stiDiscriminatorValue === value) ?? baseMeta;
2168
+ const value = row[baseMeta.stiDiscriminatorColumnName];
2169
+ return baseMeta.subTypesByStiValue.get(value) ?? baseMeta;
2095
2170
  }
2096
2171
  else {
2097
2172
  throw new Error("Unknown inheritance type");
@@ -2100,7 +2175,7 @@ function findConcreteMeta(maybeBaseMeta, row) {
2100
2175
  /** Sets the `Animal.type` enum to the right subtype value. */
2101
2176
  function setStiDiscriminatorValue(baseMeta, entity) {
2102
2177
  const typeName = entity.constructor.name;
2103
- const st = baseMeta.subTypes.find((st) => st.type === typeName);
2178
+ const st = baseMeta.subTypesByType.get(typeName);
2104
2179
  if (st) {
2105
2180
  const field = baseMeta.fields[baseMeta.stiDiscriminatorField];
2106
2181
  const code = field.enumDetailType.findById(st.stiDiscriminatorValue).code;
@@ -2110,17 +2185,19 @@ function setStiDiscriminatorValue(baseMeta, entity) {
2110
2185
  entity[baseMeta.stiDiscriminatorField] = undefined;
2111
2186
  }
2112
2187
  }
2113
- function findPendingFlushEntities(entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks) {
2114
- for (const e of entities) {
2115
- if ((0, BaseEntity_1.getInstanceData)(e).pendingOperation !== "none") {
2116
- if (!hooksInvoked.has(e)) {
2117
- pendingHooks.add(e);
2118
- }
2119
- else {
2120
- alreadyRanHooks.add(e);
2121
- }
2122
- pendingFlush.add(e);
2188
+ function findPendingFlushEntities(maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks) {
2189
+ for (const e of maybePendingFlushEntities) {
2190
+ if ((0, BaseEntity_1.getInstanceData)(e).pendingOperation === "none") {
2191
+ maybePendingFlushEntities.delete(e);
2192
+ continue;
2193
+ }
2194
+ if (!hooksInvoked.has(e)) {
2195
+ pendingHooks.add(e);
2196
+ }
2197
+ else {
2198
+ alreadyRanHooks.add(e);
2123
2199
  }
2200
+ pendingFlush.add(e);
2124
2201
  }
2125
2202
  }
2126
2203
  /** Returns true if the caller explicitly asked `find` to use SQL pagination. */