joist-core 2.2.0-next.3 → 2.2.0-next.31

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 (161) hide show
  1. package/build/BaseEntity.d.ts +2 -0
  2. package/build/BaseEntity.d.ts.map +1 -1
  3. package/build/BaseEntity.js +1 -1
  4. package/build/BaseEntity.js.map +1 -1
  5. package/build/Entity.d.ts +1 -0
  6. package/build/Entity.d.ts.map +1 -1
  7. package/build/EntityManager.d.ts +6 -2
  8. package/build/EntityManager.d.ts.map +1 -1
  9. package/build/EntityManager.js +170 -88
  10. package/build/EntityManager.js.map +1 -1
  11. package/build/EntityMetadata.d.ts +20 -1
  12. package/build/EntityMetadata.d.ts.map +1 -1
  13. package/build/EntityMetadata.js +13 -0
  14. package/build/EntityMetadata.js.map +1 -1
  15. package/build/IndexManager.d.ts +5 -5
  16. package/build/IndexManager.d.ts.map +1 -1
  17. package/build/IndexManager.js +48 -32
  18. package/build/IndexManager.js.map +1 -1
  19. package/build/InstanceData.d.ts +16 -2
  20. package/build/InstanceData.d.ts.map +1 -1
  21. package/build/InstanceData.js +36 -3
  22. package/build/InstanceData.js.map +1 -1
  23. package/build/IsLoadedCache.d.ts.map +1 -1
  24. package/build/IsLoadedCache.js +31 -25
  25. package/build/IsLoadedCache.js.map +1 -1
  26. package/build/JoinRows.js +1 -1
  27. package/build/JoinRows.js.map +1 -1
  28. package/build/QueryParser.collectionJoins.js +8 -2
  29. package/build/QueryParser.collectionJoins.js.map +1 -1
  30. package/build/QueryParser.d.ts +4 -2
  31. package/build/QueryParser.d.ts.map +1 -1
  32. package/build/QueryParser.js +19 -5
  33. package/build/QueryParser.js.map +1 -1
  34. package/build/QueryParser.pruning.d.ts.map +1 -1
  35. package/build/QueryParser.pruning.js +8 -1
  36. package/build/QueryParser.pruning.js.map +1 -1
  37. package/build/QueryParser.test.d.ts +2 -0
  38. package/build/QueryParser.test.d.ts.map +1 -0
  39. package/build/QueryParser.test.js +16 -0
  40. package/build/QueryParser.test.js.map +1 -0
  41. package/build/ReactionsManager.d.ts +2 -1
  42. package/build/ReactionsManager.d.ts.map +1 -1
  43. package/build/ReactionsManager.js +53 -49
  44. package/build/ReactionsManager.js.map +1 -1
  45. package/build/batchloaders/BatchLoader.d.ts.map +1 -1
  46. package/build/batchloaders/BatchLoader.js +6 -1
  47. package/build/batchloaders/BatchLoader.js.map +1 -1
  48. package/build/batchloaders/manyToManyBatchLoader.d.ts.map +1 -1
  49. package/build/batchloaders/manyToManyBatchLoader.js +1 -1
  50. package/build/batchloaders/manyToManyBatchLoader.js.map +1 -1
  51. package/build/batchloaders/populateBatchLoader.d.ts.map +1 -1
  52. package/build/batchloaders/populateBatchLoader.js +26 -8
  53. package/build/batchloaders/populateBatchLoader.js.map +1 -1
  54. package/build/batchloaders/recursiveChildrenBatchLoader.d.ts.map +1 -1
  55. package/build/batchloaders/recursiveChildrenBatchLoader.js +2 -0
  56. package/build/batchloaders/recursiveChildrenBatchLoader.js.map +1 -1
  57. package/build/batchloaders/recursiveM2mBatchLoader.d.ts.map +1 -1
  58. package/build/batchloaders/recursiveM2mBatchLoader.js +1 -3
  59. package/build/batchloaders/recursiveM2mBatchLoader.js.map +1 -1
  60. package/build/batchloaders/recursiveParentsBatchLoader.d.ts.map +1 -1
  61. package/build/batchloaders/recursiveParentsBatchLoader.js +2 -0
  62. package/build/batchloaders/recursiveParentsBatchLoader.js.map +1 -1
  63. package/build/configure.d.ts +3 -3
  64. package/build/configure.d.ts.map +1 -1
  65. package/build/configure.js +65 -0
  66. package/build/configure.js.map +1 -1
  67. package/build/dataloaders/fastWhereFilterHash.d.ts +15 -0
  68. package/build/dataloaders/fastWhereFilterHash.d.ts.map +1 -0
  69. package/build/dataloaders/fastWhereFilterHash.js +164 -0
  70. package/build/dataloaders/fastWhereFilterHash.js.map +1 -0
  71. package/build/dataloaders/fastWhereFilterHash.test.d.ts +2 -0
  72. package/build/dataloaders/fastWhereFilterHash.test.d.ts.map +1 -0
  73. package/build/dataloaders/fastWhereFilterHash.test.js +59 -0
  74. package/build/dataloaders/fastWhereFilterHash.test.js.map +1 -0
  75. package/build/dataloaders/findCountDataLoader.d.ts +1 -2
  76. package/build/dataloaders/findCountDataLoader.d.ts.map +1 -1
  77. package/build/dataloaders/findCountDataLoader.js +48 -13
  78. package/build/dataloaders/findCountDataLoader.js.map +1 -1
  79. package/build/dataloaders/findDataLoader.d.ts +10 -5
  80. package/build/dataloaders/findDataLoader.d.ts.map +1 -1
  81. package/build/dataloaders/findDataLoader.js +109 -82
  82. package/build/dataloaders/findDataLoader.js.map +1 -1
  83. package/build/dataloaders/findIdsDataLoader.d.ts +1 -2
  84. package/build/dataloaders/findIdsDataLoader.d.ts.map +1 -1
  85. package/build/dataloaders/findIdsDataLoader.js +16 -15
  86. package/build/dataloaders/findIdsDataLoader.js.map +1 -1
  87. package/build/dataloaders/findOrCreateDataLoader.d.ts.map +1 -1
  88. package/build/dataloaders/findOrCreateDataLoader.js +3 -3
  89. package/build/dataloaders/findOrCreateDataLoader.js.map +1 -1
  90. package/build/dataloaders/findPaginatedDataLoader.d.ts +1 -2
  91. package/build/dataloaders/findPaginatedDataLoader.d.ts.map +1 -1
  92. package/build/dataloaders/findPaginatedDataLoader.js +33 -25
  93. package/build/dataloaders/findPaginatedDataLoader.js.map +1 -1
  94. package/build/drivers/EntityWriter.js +13 -7
  95. package/build/drivers/EntityWriter.js.map +1 -1
  96. package/build/drivers/buildRawQuery.d.ts.map +1 -1
  97. package/build/drivers/buildRawQuery.js +5 -1
  98. package/build/drivers/buildRawQuery.js.map +1 -1
  99. package/build/fields.d.ts.map +1 -1
  100. package/build/fields.js +16 -6
  101. package/build/fields.js.map +1 -1
  102. package/build/keys.d.ts.map +1 -1
  103. package/build/keys.js +8 -6
  104. package/build/keys.js.map +1 -1
  105. package/build/loadHints.d.ts +9 -0
  106. package/build/loadHints.d.ts.map +1 -1
  107. package/build/loadHints.js +63 -9
  108. package/build/loadHints.js.map +1 -1
  109. package/build/loadLens.d.ts +11 -0
  110. package/build/loadLens.d.ts.map +1 -1
  111. package/build/loadLens.js +43 -10
  112. package/build/loadLens.js.map +1 -1
  113. package/build/newEntity.d.ts.map +1 -1
  114. package/build/newEntity.js +16 -14
  115. package/build/newEntity.js.map +1 -1
  116. package/build/normalizeHints.d.ts +7 -0
  117. package/build/normalizeHints.d.ts.map +1 -1
  118. package/build/normalizeHints.js +40 -2
  119. package/build/normalizeHints.js.map +1 -1
  120. package/build/relations/ManyToManyCollection.d.ts.map +1 -1
  121. package/build/relations/ManyToManyCollection.js +2 -2
  122. package/build/relations/ManyToManyCollection.js.map +1 -1
  123. package/build/relations/ManyToManyLargeCollection.d.ts.map +1 -1
  124. package/build/relations/ManyToManyLargeCollection.js +2 -1
  125. package/build/relations/ManyToManyLargeCollection.js.map +1 -1
  126. package/build/relations/OneToManyCollection.d.ts.map +1 -1
  127. package/build/relations/OneToManyCollection.js +2 -2
  128. package/build/relations/OneToManyCollection.js.map +1 -1
  129. package/build/relations/OneToManyLargeCollection.d.ts.map +1 -1
  130. package/build/relations/OneToManyLargeCollection.js +2 -1
  131. package/build/relations/OneToManyLargeCollection.js.map +1 -1
  132. package/build/relations/ReactiveManyToMany.d.ts.map +1 -1
  133. package/build/relations/ReactiveManyToMany.js +1 -1
  134. package/build/relations/ReactiveManyToMany.js.map +1 -1
  135. package/build/relations/ReactiveManyToManyOtherSide.d.ts.map +1 -1
  136. package/build/relations/ReactiveManyToManyOtherSide.js +1 -1
  137. package/build/relations/ReactiveManyToManyOtherSide.js.map +1 -1
  138. package/build/relations/RecursiveCollection.d.ts +2 -6
  139. package/build/relations/RecursiveCollection.d.ts.map +1 -1
  140. package/build/relations/RecursiveCollection.js +9 -17
  141. package/build/relations/RecursiveCollection.js.map +1 -1
  142. package/build/relations/RecursiveCycleError.d.ts +13 -0
  143. package/build/relations/RecursiveCycleError.d.ts.map +1 -0
  144. package/build/relations/RecursiveCycleError.js +15 -0
  145. package/build/relations/RecursiveCycleError.js.map +1 -0
  146. package/build/relations/hasOneThrough.d.ts.map +1 -1
  147. package/build/relations/hasOneThrough.js +6 -4
  148. package/build/relations/hasOneThrough.js.map +1 -1
  149. package/build/relations/hasProperty.d.ts +2 -1
  150. package/build/relations/hasProperty.d.ts.map +1 -1
  151. package/build/relations/hasProperty.js +15 -5
  152. package/build/relations/hasProperty.js.map +1 -1
  153. package/build/utils.d.ts +2 -0
  154. package/build/utils.d.ts.map +1 -1
  155. package/build/utils.js +12 -0
  156. package/build/utils.js.map +1 -1
  157. package/package.json +3 -5
  158. package/build/caches.d.ts +0 -6
  159. package/build/caches.d.ts.map +0 -1
  160. package/build/caches.js +0 -42
  161. 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,20 @@ 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
+ },
170
+ pendingDeleteIds(type) {
171
+ return em.#pendingDeletes
172
+ .filter((entity) => entity instanceof type && entity.idMaybe !== undefined)
173
+ .map((entity) => (0, index_1.keyToNumber)((0, index_1.getMetadata)(entity), entity.id));
174
+ },
158
175
  isMerging(entity) {
159
176
  return em.#merging?.has(entity) ?? false;
160
177
  },
@@ -248,12 +265,9 @@ class EntityManager {
248
265
  async find(type, where, options) {
249
266
  const { populate, ...rest } = options || {};
250
267
  const settings = { where, ...rest };
251
- const loader = hasPaginationSettings(rest)
268
+ const result = await (hasPaginationSettings(rest)
252
269
  ? (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) {
270
+ : (0, findDataLoader_1.findDataLoader)(this, type, settings, populate)).catch(function find(err) {
257
271
  throw appendStack(err, new Error());
258
272
  });
259
273
  if (populate) {
@@ -348,6 +362,9 @@ class EntityManager {
348
362
  }
349
363
  else {
350
364
  const [entity] = this.hydrate(type, [row]);
365
+ if (this.#hasAnyDeletes && entity.isDeletedEntity) {
366
+ return undefined;
367
+ }
351
368
  if (populate) {
352
369
  await this.populate(entity, populate);
353
370
  }
@@ -364,16 +381,14 @@ class EntityManager {
364
381
  */
365
382
  async findCount(type, where, options = {}) {
366
383
  const settings = { where, ...options };
367
- let count = await (0, findCountDataLoader_1.findCountDataLoader)(this, type, settings)
368
- .load(settings)
369
- .catch(function findCount(err) {
384
+ let count = await (0, findCountDataLoader_1.findCountDataLoader)(this, type, settings).catch(function findCount(err) {
370
385
  throw appendStack(err, new Error());
371
386
  });
372
387
  // If the user is do "count all", we can adjust the number up/down based on
373
388
  // WIP creates/deletes. We can't do this if the WHERE clause is populated b/c
374
389
  // then we'd also have to eval each created/deleted entity against the WHERE
375
390
  // clause before knowing if it should adjust teh amount.
376
- const isSelectAll = Object.keys(where).length === 0;
391
+ const isSelectAll = Object.keys(where).length === 0 && options.conditions === undefined;
377
392
  if (isSelectAll) {
378
393
  const tagged = this.#entitiesByTag.get((0, index_1.getMetadata)(type).tagName) ?? [];
379
394
  for (const entity of tagged) {
@@ -398,9 +413,7 @@ class EntityManager {
398
413
  */
399
414
  async findIds(type, where, options = {}) {
400
415
  const settings = { where, ...options };
401
- return (0, findIdsDataLoader_1.findIdsDataLoader)(this, type, settings)
402
- .load(settings)
403
- .catch(function findIds(err) {
416
+ return (0, findIdsDataLoader_1.findIdsDataLoader)(this, type, settings).catch(function findIds(err) {
404
417
  throw appendStack(err, new Error());
405
418
  });
406
419
  }
@@ -689,48 +702,86 @@ class EntityManager {
689
702
  }
690
703
  async loadAll(type, _ids, hint) {
691
704
  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) {
705
+ // Use pre-allocated arrays/for loops instead of `.filter`s since this can be a hot spot
706
+ const ids = new Array(_ids.length);
707
+ const entities = new Array(_ids.length);
708
+ let idsToLoad;
709
+ let positionsToLoad;
710
+ for (let i = 0; i < _ids.length; i++) {
711
+ const id = (0, index_1.tagId)(meta, _ids[i]);
712
+ ids[i] = id;
713
+ const entity = this.findExistingInstance(id);
714
+ if (entity) {
715
+ entities[i] = entity;
716
+ }
717
+ else {
718
+ (idsToLoad ??= []).push(id);
719
+ (positionsToLoad ??= []).push(i);
720
+ }
721
+ }
722
+ if (idsToLoad && idsToLoad.length > 0) {
695
723
  await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
696
724
  .loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
697
725
  .catch(function loadAll(err) {
698
726
  throw appendStack(err, new Error());
699
727
  });
728
+ for (const i of positionsToLoad) {
729
+ entities[i] = this.findExistingInstance(ids[i]);
730
+ }
700
731
  }
701
- const entities = [];
702
- for (const id of ids) {
703
- const entity = this.findExistingInstance(id);
704
- if (entity)
705
- entities.push(entity);
732
+ let idsNotFound;
733
+ for (let i = 0; i < entities.length; i++) {
734
+ if (entities[i] === undefined) {
735
+ (idsNotFound ??= []).push(ids[i]);
736
+ }
706
737
  }
707
- if (entities.length !== ids.length) {
708
- const idsNotFound = ids.filter((_, i) => entities[i] === undefined);
738
+ if (idsNotFound) {
709
739
  throw new NotFoundError(`${idsNotFound.join(",")} were not found`);
710
740
  }
741
+ const loadedEntities = entities;
711
742
  if (hint) {
712
- await this.populate(entities, hint);
743
+ await this.populate(loadedEntities, hint);
713
744
  }
714
745
  if (meta.inheritanceType === "sti" && meta.baseType) {
715
- const wrongType = entities.filter((e) => !(e instanceof meta.cstr));
746
+ const wrongType = loadedEntities.filter((e) => !(e instanceof meta.cstr));
716
747
  if (wrongType.length > 0) {
717
748
  throw new Error(`${wrongType.join(", ")} were not of type ${meta.cstr.name}`);
718
749
  }
719
750
  }
720
- return entities;
751
+ return loadedEntities;
721
752
  }
722
753
  async loadAllIfExists(type, _ids, hint) {
723
754
  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) {
755
+ // Use pre-allocated arrays/for loops instead of `.filter`s since this can be a hot spot
756
+ const ids = new Array(_ids.length);
757
+ const entities = [];
758
+ let idsToLoad;
759
+ // Ensure the ids are tagged, and find any not-yet-loaded
760
+ for (let i = 0; i < _ids.length; i++) {
761
+ const id = (0, index_1.tagId)(meta, _ids[i]);
762
+ ids[i] = id;
763
+ const entity = this.findExistingInstance(id);
764
+ if (entity) {
765
+ entities.push(entity);
766
+ }
767
+ else {
768
+ (idsToLoad ??= []).push(id);
769
+ }
770
+ }
771
+ if (idsToLoad && idsToLoad.length > 0) {
727
772
  await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
728
773
  .loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
729
774
  .catch(function loadAllIfExists(err) {
730
775
  throw appendStack(err, new Error());
731
776
  });
777
+ // Now that everything is loaded, recalc `entities`
778
+ entities.length = 0;
779
+ for (const id of ids) {
780
+ const entity = this.findExistingInstance(id);
781
+ if (entity)
782
+ entities.push(entity);
783
+ }
732
784
  }
733
- const entities = ids.map((id) => this.findExistingInstance(id)).filter(Boolean);
734
785
  if (hint) {
735
786
  await this.populate(entities, hint);
736
787
  }
@@ -781,6 +832,12 @@ class EntityManager {
781
832
  if (list.length === 0) {
782
833
  return !fn ? entityOrList : fn(entityOrList);
783
834
  }
835
+ // Avoid building a HintTree/batchloader for persisted entities when everything is already loaded; in the phase 9
836
+ // benchmark this moved already-loaded populates from ~1.10ms to ~0.89ms, and nested already-loaded populates from
837
+ // ~2.39ms to ~1.94ms.
838
+ if (!opts.forceReload && list.every((entity) => !entity.isNewEntity && (0, loadHints_1.isLoadedForPopulate)(entity, hintOpt))) {
839
+ return fn ? fn(entityOrList) : entityOrList;
840
+ }
784
841
  const meta = (0, index_1.getMetadata)(list[0]);
785
842
  if (this.#preloader) {
786
843
  // If we can preload, prevent promise deadlocking by one large-batch preload populate (which can't have
@@ -829,9 +886,11 @@ class EntityManager {
829
886
  delete(entityOrArray) {
830
887
  for (const entity of (0, utils_1.toArray)(entityOrArray)) {
831
888
  // Early return if already deleted.
832
- const alreadyMarked = (0, BaseEntity_1.getInstanceData)(entity).markDeleted(entity);
889
+ const alreadyMarked = (0, BaseEntity_1.getInstanceData)(entity).markDeleted();
833
890
  if (!alreadyMarked)
834
891
  continue;
892
+ // This monotonic flag lets find hot paths skip scanning results until a delete has ever happened in this EM.
893
+ this.#hasAnyDeletes = true;
835
894
  // Any derived fields that read this entity will need recalc-d
836
895
  this.#rm.queueAllDownstreamFields(entity, "deleted");
837
896
  // Synchronously unhook the entity if the relations are loaded
@@ -925,7 +984,7 @@ class EntityManager {
925
984
  const pendingHooks = new Set();
926
985
  // Subset of pendingFlush entities that had hooks invoked in a prior `runHooksOnPendingEntities`
927
986
  const alreadyRanHooks = new Set();
928
- findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
987
+ findPendingFlushEntities(this.#maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
929
988
  // If we're re-looping for AsyncReactiveField, make sure to bump updatedAt
930
989
  // each time, so that for an INSERT-then-UPDATE the triggers don't think the
931
990
  // UPDATE forgot to self-bump updatedAt, and then "helpfully" bump it for us.
@@ -964,7 +1023,7 @@ class EntityManager {
964
1023
  hooksInvoked.add(e);
965
1024
  pendingHooks.clear();
966
1025
  // See if the hooks mutated any new, not-yet-hooksInvoked entities
967
- findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
1026
+ findPendingFlushEntities(this.#maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
968
1027
  // The final run of recalcPendingReactables could have left us with pending type errors and no entities in
969
1028
  // pendingHooks. If so, we need to re-run recalcPendingTypeErrors to get those errors to transition into
970
1029
  // suppressed errors so that we will fail after simpleValidation.
@@ -1099,7 +1158,7 @@ class EntityManager {
1099
1158
  for (const e of createdThenDeleted)
1100
1159
  (0, BaseEntity_1.getInstanceData)(e).fixupCreatedThenDeleted();
1101
1160
  this.#merging?.clear();
1102
- return [...allFlushedEntities];
1161
+ return [...allFlushedEntities].sort((a, b) => (0, BaseEntity_1.getInstanceData)(a).entityIndex - (0, BaseEntity_1.getInstanceData)(b).entityIndex);
1103
1162
  }
1104
1163
  catch (e) {
1105
1164
  if (e instanceof RecursiveCollection_1.RecursiveCycleError) {
@@ -1122,8 +1181,9 @@ class EntityManager {
1122
1181
  throw new index_1.ValidationErrors(message);
1123
1182
  }
1124
1183
  }
1125
- if (e instanceof InMemoryRollbackError)
1126
- return [...allFlushedEntities];
1184
+ if (e instanceof InMemoryRollbackError) {
1185
+ return [...allFlushedEntities].sort((a, b) => (0, BaseEntity_1.getInstanceData)(a).entityIndex - (0, BaseEntity_1.getInstanceData)(b).entityIndex);
1186
+ }
1127
1187
  throw e;
1128
1188
  }
1129
1189
  finally {
@@ -1231,10 +1291,13 @@ class EntityManager {
1231
1291
  */
1232
1292
  hydrate(type, rows, options) {
1233
1293
  const maybeBaseMeta = (0, index_1.getMetadata)(type);
1294
+ const taggedIdPrefix = `${maybeBaseMeta.tagName}:`;
1295
+ const overwriteExisting = options?.overwriteExisting === true;
1234
1296
  let i = 0;
1235
1297
  const entities = new Array(rows.length);
1236
1298
  for (const row of rows) {
1237
- const taggedId = (0, index_1.keyToTaggedId)(maybeBaseMeta, row["id"]) || (0, utils_1.fail)("No id column was available");
1299
+ const id = row["id"];
1300
+ const taggedId = id === undefined || id === null ? (0, utils_1.fail)("No id column was available") : `${taggedIdPrefix}${id}`;
1238
1301
  // See if this is already in our UoW
1239
1302
  let entity = this.findExistingInstance(taggedId);
1240
1303
  if (!entity) {
@@ -1243,23 +1306,36 @@ class EntityManager {
1243
1306
  // Pass id as a hint that we're in hydrate mode
1244
1307
  entity = (0, newEntity_1.newEntity)(this, (0, index_1.asConcreteCstr)(meta.cstr), false);
1245
1308
  (0, BaseEntity_1.getInstanceData)(entity).row = row;
1246
- this.#doRegister(entity, taggedId);
1309
+ this.#doRegister(entity, taggedId, meta, true);
1247
1310
  }
1248
- else if (options?.overwriteExisting === true) {
1311
+ else if (overwriteExisting) {
1249
1312
  // Usually if the entity already exists, we don't write over it, but in this case we assume that
1250
1313
  // `EntityManager.refresh` is telling us to explicitly load the latest data.
1251
1314
  // First swap out the old row with the new row
1252
- (0, BaseEntity_1.getInstanceData)(entity).row = row;
1315
+ const instanceData = (0, BaseEntity_1.getInstanceData)(entity);
1316
+ instanceData.row = row;
1253
1317
  // And then only refresh the data keys that have already been serde-d from rows
1254
1318
  // (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];
1319
+ const { data } = instanceData;
1320
+ const dataKeys = Object.keys(data);
1321
+ if (dataKeys.length > 0) {
1322
+ const allFields = (0, index_1.getMetadata)(entity).allFields;
1323
+ const changedFields = entity.changes.fieldsWithoutRelations;
1324
+ if (changedFields.length === 0) {
1325
+ for (const fieldName of dataKeys) {
1326
+ const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1327
+ serde.setOnEntity(data, row);
1328
+ }
1329
+ }
1330
+ else {
1331
+ for (const fieldName of dataKeys) {
1332
+ const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1333
+ serde.setOnEntity(data, row);
1334
+ // Make the field look not-dirty
1335
+ if (changedFields.includes(fieldName)) {
1336
+ instanceData.markFieldClean(fieldName);
1337
+ }
1338
+ }
1263
1339
  }
1264
1340
  }
1265
1341
  }
@@ -1268,8 +1344,9 @@ class EntityManager {
1268
1344
  return entities;
1269
1345
  }
1270
1346
  touch(entityOrEntities) {
1271
- for (const entity of (0, utils_1.toArray)(entityOrEntities))
1272
- (0, BaseEntity_1.getInstanceData)(entity).isTouched = true;
1347
+ for (const entity of (0, utils_1.toArray)(entityOrEntities)) {
1348
+ (0, BaseEntity_1.getInstanceData)(entity).markTouched();
1349
+ }
1273
1350
  }
1274
1351
  async recalc(entityOrEntities) {
1275
1352
  // Look for async reactive fields
@@ -1346,9 +1423,16 @@ class EntityManager {
1346
1423
  // Run the beforeDelete hook before we unhook the entity
1347
1424
  const todos = (0, Todo_1.createTodos)(entities);
1348
1425
  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));
1426
+ // For all relations, unhook the entity from the other side; this append path is optimized for
1427
+ // large deletes with ~100k relations by avoiding `flatMap` intermediates and `concat` copies.
1428
+ for (const entity of entities) {
1429
+ const relations = (0, index_1.getRelations)(entity);
1430
+ const start = relationsToCleanup.length;
1431
+ relationsToCleanup.length += relations.length;
1432
+ for (let i = 0; i < relations.length; i++) {
1433
+ relationsToCleanup[start + i] = relations[i];
1434
+ }
1435
+ }
1352
1436
  entities = this.#pendingDeletes;
1353
1437
  this.#pendingDeletes = [];
1354
1438
  }
@@ -1404,9 +1488,9 @@ class EntityManager {
1404
1488
  return entities.filter((e) => e instanceof cstr && !e.isDeletedEntity);
1405
1489
  }
1406
1490
  if (this.#indexManager.shouldIndexType(entities.length)) {
1407
- this.#indexManager.enableIndexingForType(meta, entities);
1491
+ this.#indexManager.enableIndexingForType(meta, entities, where);
1408
1492
  return (this.#indexManager
1409
- .findMatching(meta, where)
1493
+ .findMatching(meta, entities, where)
1410
1494
  // Still filter by `instanceof cstr` to handle subtyping
1411
1495
  .filter((e) => e instanceof cstr && !e.isDeletedEntity));
1412
1496
  }
@@ -1506,7 +1590,8 @@ class EntityManager {
1506
1590
  continue;
1507
1591
  const { originalData: oldOriginalData, data: oldData } = oldInstanceData;
1508
1592
  const newEntity = mapEntity(oldEntity);
1509
- const { originalData: newOriginalData, data: newData } = newEntity.__data;
1593
+ const newInstanceData = newEntity.__data;
1594
+ const { data: newData } = newInstanceData;
1510
1595
  // for new entities, anything in `data` is changed and should be copied across. for existing entities, we
1511
1596
  // only care about changed fields, which are enumerated by originalData
1512
1597
  const maybeEntity = (value) => ((0, Entity_1.isEntity)(value) ? mapEntity(value) : value);
@@ -1514,7 +1599,7 @@ class EntityManager {
1514
1599
  for (const field of fields) {
1515
1600
  // copy over originalData so .changes is consistent across ems
1516
1601
  if (field in oldOriginalData)
1517
- newOriginalData[field] = maybeEntity(oldOriginalData[field]);
1602
+ newInstanceData.markFieldDirty(field, maybeEntity(oldOriginalData[field]));
1518
1603
  newData[field] = maybeEntity(oldData[field]);
1519
1604
  }
1520
1605
  }
@@ -1524,7 +1609,7 @@ class EntityManager {
1524
1609
  const newEntity = mapEntity(oldEntity);
1525
1610
  if (oldEntity.isDeletedEntity) {
1526
1611
  // If the old entity was deleted, that should be persisted in the new em
1527
- newEntity.__data.markDeleted(newEntity);
1612
+ newEntity.__data.markDeleted();
1528
1613
  // deleted entities will fail if you try to `get` their relations, so skip them since they should be cleared
1529
1614
  // out regardless
1530
1615
  continue;
@@ -1669,11 +1754,11 @@ class EntityManager {
1669
1754
  }
1670
1755
  }
1671
1756
  /** Registers a newly-instantiated entity with our EntityManager; only called by #doCreate and hydrate. */
1672
- #doRegister(entity, id) {
1757
+ #doRegister(entity, id, meta, skipDuplicateCheck = false) {
1673
1758
  // Keep our indexes up to date...
1674
1759
  const maybeId = id ?? entity.idTaggedMaybe;
1675
1760
  if (maybeId) {
1676
- if (this.findExistingInstance(maybeId) !== undefined) {
1761
+ if (!skipDuplicateCheck && this.findExistingInstance(maybeId) !== undefined) {
1677
1762
  throw new Error(`Entity ${entity} has a duplicate instance already loaded`);
1678
1763
  }
1679
1764
  this.#entitiesById.set(maybeId, entity);
@@ -1682,8 +1767,9 @@ class EntityManager {
1682
1767
  // Also register by the `a#1` style tagged string for new entities
1683
1768
  this.#entitiesById.set(entity.toTaggedString(), entity);
1684
1769
  }
1770
+ (0, BaseEntity_1.getInstanceData)(entity).entityIndex = this.#entitiesArray.length;
1685
1771
  this.#entitiesArray.push(entity);
1686
- const meta = (0, index_1.getMetadata)(entity);
1772
+ meta ??= (0, index_1.getMetadata)(entity);
1687
1773
  const set = this.#entitiesByTag.get(meta.tagName) ?? [];
1688
1774
  if (set.length === 0)
1689
1775
  this.#entitiesByTag.set(meta.tagName, set);
@@ -1697,8 +1783,6 @@ class EntityManager {
1697
1783
  .join(", ");
1698
1784
  throw new Error(`More than ${this.entityLimit} entities have been instantiated (top entities: ${topTypes})`);
1699
1785
  }
1700
- // If indexing is enabled for this type, add it...
1701
- this.#indexManager.maybeIndexEntity(entity);
1702
1786
  }
1703
1787
  }
1704
1788
  exports.EntityManager = EntityManager;
@@ -1782,14 +1866,14 @@ async function validateReactiveRules(em, logger, todos, joinRowTodos) {
1782
1866
  const p1 = Object.values(todos).flatMap((todo) => {
1783
1867
  const entities = [...todo.inserts, ...todo.updates, ...todo.deletes];
1784
1868
  // Find each statically-declared reactive rule for the given entity type
1785
- const rules = (0, caches_1.getReactiveRules)(todo.metadata);
1869
+ const rules = todo.metadata.reactiveRules;
1786
1870
  return rules.map((rule) => {
1787
1871
  // Of all changed entities of this type, how many specifically trigger this rule?
1788
1872
  const triggered = entities.filter((e) => {
1789
1873
  // If the rule is for a different subtype, skip it
1790
1874
  if (!(e instanceof rule.source))
1791
1875
  return false;
1792
- // Any new-or-deleted entity fires every rule (getReactiveRules has already filtered out read-only)
1876
+ // Any new-or-deleted entity fires every rule (reactiveRules has already filtered out read-only)
1793
1877
  if (e.isNewEntity || e.isDeletedEntity)
1794
1878
  return true;
1795
1879
  // Otherwise see if the changed fields overlaps with the rule's fields
@@ -1807,15 +1891,15 @@ async function validateReactiveRules(em, logger, todos, joinRowTodos) {
1807
1891
  const p2 = Object.values(joinRowTodos).flatMap((todo) => {
1808
1892
  const entities = [...todo.newRows, ...todo.deletedRows].flatMap((jr) => Object.values(jr.columns));
1809
1893
  // Do the first side
1810
- const p1 = (0, caches_1.getReactiveRules)(todo.m2m.meta)
1811
- .filter((rule) => rule.fields.includes(todo.m2m.fieldName))
1894
+ const p1 = todo.m2m.meta
1895
+ .reactiveRules.filter((rule) => rule.fields.includes(todo.m2m.fieldName))
1812
1896
  .map((rule) => {
1813
1897
  const triggered = entities.filter((e) => e instanceof todo.m2m.meta.cstr);
1814
1898
  return followAndQueue(triggered, rule);
1815
1899
  });
1816
1900
  // And the second side
1817
- const p2 = (0, caches_1.getReactiveRules)(todo.m2m.otherMeta)
1818
- .filter((rule) => rule.fields.includes(todo.m2m.otherFieldName))
1901
+ const p2 = todo.m2m.otherMeta
1902
+ .reactiveRules.filter((rule) => rule.fields.includes(todo.m2m.otherFieldName))
1819
1903
  .map((rule) => {
1820
1904
  const triggered = entities.filter((e) => e instanceof todo.m2m.otherMeta.cstr);
1821
1905
  return followAndQueue(triggered, rule);
@@ -2044,7 +2128,7 @@ function maybeBumpUpdatedAt(rm, todos, now) {
2044
2128
  // it has changed. This is technically true, but this will break the oplock SQL generation,
2045
2129
  // so force the field to be dirty.
2046
2130
  const orm = (0, BaseEntity_1.getInstanceData)(e);
2047
- orm.originalData[updatedAt] = (0, fields_1.getField)(e, updatedAt);
2131
+ orm.markFieldDirty(updatedAt, (0, fields_1.getField)(e, updatedAt));
2048
2132
  const serde = todo.metadata.fields[updatedAt].serde;
2049
2133
  orm.data[updatedAt] = serde.mapFromNow(now);
2050
2134
  rm.queueDownstreamReactables(e, updatedAt);
@@ -2081,17 +2165,13 @@ function findConcreteMeta(maybeBaseMeta, row) {
2081
2165
  throw new Error(`${maybeBaseMeta.type} ${(0, index_1.tagId)(maybeBaseMeta, row.id)} must be instantiated via a subtype`);
2082
2166
  }
2083
2167
  // 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;
2168
+ return maybeBaseMeta.subTypesByType.get(row.__class) ?? maybeBaseMeta;
2085
2169
  }
2086
2170
  else if (maybeBaseMeta.inheritanceType === "sti") {
2087
2171
  // Look for the STI discriminator value
2088
2172
  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;
2173
+ const value = row[baseMeta.stiDiscriminatorColumnName];
2174
+ return baseMeta.subTypesByStiValue.get(value) ?? baseMeta;
2095
2175
  }
2096
2176
  else {
2097
2177
  throw new Error("Unknown inheritance type");
@@ -2100,7 +2180,7 @@ function findConcreteMeta(maybeBaseMeta, row) {
2100
2180
  /** Sets the `Animal.type` enum to the right subtype value. */
2101
2181
  function setStiDiscriminatorValue(baseMeta, entity) {
2102
2182
  const typeName = entity.constructor.name;
2103
- const st = baseMeta.subTypes.find((st) => st.type === typeName);
2183
+ const st = baseMeta.subTypesByType.get(typeName);
2104
2184
  if (st) {
2105
2185
  const field = baseMeta.fields[baseMeta.stiDiscriminatorField];
2106
2186
  const code = field.enumDetailType.findById(st.stiDiscriminatorValue).code;
@@ -2110,17 +2190,19 @@ function setStiDiscriminatorValue(baseMeta, entity) {
2110
2190
  entity[baseMeta.stiDiscriminatorField] = undefined;
2111
2191
  }
2112
2192
  }
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);
2193
+ function findPendingFlushEntities(maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks) {
2194
+ for (const e of maybePendingFlushEntities) {
2195
+ if ((0, BaseEntity_1.getInstanceData)(e).pendingOperation === "none") {
2196
+ maybePendingFlushEntities.delete(e);
2197
+ continue;
2198
+ }
2199
+ if (!hooksInvoked.has(e)) {
2200
+ pendingHooks.add(e);
2201
+ }
2202
+ else {
2203
+ alreadyRanHooks.add(e);
2123
2204
  }
2205
+ pendingFlush.add(e);
2124
2206
  }
2125
2207
  }
2126
2208
  /** Returns true if the caller explicitly asked `find` to use SQL pagination. */