joist-core 2.2.0 → 2.3.0-next.10

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 (233) hide show
  1. package/build/Aliases.d.ts.map +1 -1
  2. package/build/Aliases.js.map +1 -1
  3. package/build/BaseEntity.d.ts +2 -0
  4. package/build/BaseEntity.d.ts.map +1 -1
  5. package/build/BaseEntity.js +1 -1
  6. package/build/BaseEntity.js.map +1 -1
  7. package/build/Entity.d.ts +1 -0
  8. package/build/Entity.d.ts.map +1 -1
  9. package/build/EntityFilter.d.ts +29 -10
  10. package/build/EntityFilter.d.ts.map +1 -1
  11. package/build/EntityFilter.js +18 -0
  12. package/build/EntityFilter.js.map +1 -1
  13. package/build/EntityGraphQLFilter.d.ts +3 -0
  14. package/build/EntityGraphQLFilter.d.ts.map +1 -1
  15. package/build/EntityGraphQLFilter.js.map +1 -1
  16. package/build/EntityManager.d.ts +36 -50
  17. package/build/EntityManager.d.ts.map +1 -1
  18. package/build/EntityManager.js +261 -125
  19. package/build/EntityManager.js.map +1 -1
  20. package/build/EntityMetadata.d.ts +52 -2
  21. package/build/EntityMetadata.d.ts.map +1 -1
  22. package/build/EntityMetadata.js +17 -0
  23. package/build/EntityMetadata.js.map +1 -1
  24. package/build/IndexManager.d.ts +5 -5
  25. package/build/IndexManager.d.ts.map +1 -1
  26. package/build/IndexManager.js +48 -32
  27. package/build/IndexManager.js.map +1 -1
  28. package/build/InstanceData.d.ts +16 -2
  29. package/build/InstanceData.d.ts.map +1 -1
  30. package/build/InstanceData.js +37 -4
  31. package/build/InstanceData.js.map +1 -1
  32. package/build/IsLoadedCache.d.ts.map +1 -1
  33. package/build/IsLoadedCache.js +31 -25
  34. package/build/IsLoadedCache.js.map +1 -1
  35. package/build/JoinRows.d.ts +54 -12
  36. package/build/JoinRows.d.ts.map +1 -1
  37. package/build/JoinRows.js +114 -44
  38. package/build/JoinRows.js.map +1 -1
  39. package/build/QueryParser.collectionJoins.js +8 -2
  40. package/build/QueryParser.collectionJoins.js.map +1 -1
  41. package/build/QueryParser.d.ts +15 -3
  42. package/build/QueryParser.d.ts.map +1 -1
  43. package/build/QueryParser.js +293 -190
  44. package/build/QueryParser.js.map +1 -1
  45. package/build/QueryParser.pruning.d.ts.map +1 -1
  46. package/build/QueryParser.pruning.js +8 -1
  47. package/build/QueryParser.pruning.js.map +1 -1
  48. package/build/QueryParser.test.d.ts +2 -0
  49. package/build/QueryParser.test.d.ts.map +1 -0
  50. package/build/QueryParser.test.js +27 -0
  51. package/build/QueryParser.test.js.map +1 -0
  52. package/build/ReactionsManager.d.ts +2 -1
  53. package/build/ReactionsManager.d.ts.map +1 -1
  54. package/build/ReactionsManager.js +53 -49
  55. package/build/ReactionsManager.js.map +1 -1
  56. package/build/Todo.d.ts +4 -0
  57. package/build/Todo.d.ts.map +1 -1
  58. package/build/Todo.js.map +1 -1
  59. package/build/batchloaders/BatchLoader.d.ts.map +1 -1
  60. package/build/batchloaders/BatchLoader.js +6 -1
  61. package/build/batchloaders/BatchLoader.js.map +1 -1
  62. package/build/batchloaders/enumCollectionBatchLoader.d.ts +7 -0
  63. package/build/batchloaders/enumCollectionBatchLoader.d.ts.map +1 -0
  64. package/build/batchloaders/enumCollectionBatchLoader.js +54 -0
  65. package/build/batchloaders/enumCollectionBatchLoader.js.map +1 -0
  66. package/build/batchloaders/manyToManyBatchLoader.d.ts.map +1 -1
  67. package/build/batchloaders/manyToManyBatchLoader.js +10 -2
  68. package/build/batchloaders/manyToManyBatchLoader.js.map +1 -1
  69. package/build/batchloaders/populateBatchLoader.d.ts.map +1 -1
  70. package/build/batchloaders/populateBatchLoader.js +26 -8
  71. package/build/batchloaders/populateBatchLoader.js.map +1 -1
  72. package/build/batchloaders/recursiveChildrenBatchLoader.d.ts.map +1 -1
  73. package/build/batchloaders/recursiveChildrenBatchLoader.js +2 -0
  74. package/build/batchloaders/recursiveChildrenBatchLoader.js.map +1 -1
  75. package/build/batchloaders/recursiveM2mBatchLoader.d.ts.map +1 -1
  76. package/build/batchloaders/recursiveM2mBatchLoader.js +1 -3
  77. package/build/batchloaders/recursiveM2mBatchLoader.js.map +1 -1
  78. package/build/batchloaders/recursiveParentsBatchLoader.d.ts.map +1 -1
  79. package/build/batchloaders/recursiveParentsBatchLoader.js +2 -0
  80. package/build/batchloaders/recursiveParentsBatchLoader.js.map +1 -1
  81. package/build/changes.d.ts +13 -0
  82. package/build/changes.d.ts.map +1 -1
  83. package/build/changes.js +49 -0
  84. package/build/changes.js.map +1 -1
  85. package/build/configure.d.ts +5 -3
  86. package/build/configure.d.ts.map +1 -1
  87. package/build/configure.js +70 -0
  88. package/build/configure.js.map +1 -1
  89. package/build/dataloaders/fastWhereFilterHash.d.ts +15 -0
  90. package/build/dataloaders/fastWhereFilterHash.d.ts.map +1 -0
  91. package/build/dataloaders/fastWhereFilterHash.js +164 -0
  92. package/build/dataloaders/fastWhereFilterHash.js.map +1 -0
  93. package/build/dataloaders/fastWhereFilterHash.test.d.ts +2 -0
  94. package/build/dataloaders/fastWhereFilterHash.test.d.ts.map +1 -0
  95. package/build/dataloaders/fastWhereFilterHash.test.js +59 -0
  96. package/build/dataloaders/fastWhereFilterHash.test.js.map +1 -0
  97. package/build/dataloaders/findCountDataLoader.d.ts +13 -4
  98. package/build/dataloaders/findCountDataLoader.d.ts.map +1 -1
  99. package/build/dataloaders/findCountDataLoader.js +58 -15
  100. package/build/dataloaders/findCountDataLoader.js.map +1 -1
  101. package/build/dataloaders/findDataLoader.d.ts +49 -10
  102. package/build/dataloaders/findDataLoader.d.ts.map +1 -1
  103. package/build/dataloaders/findDataLoader.js +217 -108
  104. package/build/dataloaders/findDataLoader.js.map +1 -1
  105. package/build/dataloaders/findIdsDataLoader.d.ts +1 -2
  106. package/build/dataloaders/findIdsDataLoader.d.ts.map +1 -1
  107. package/build/dataloaders/findIdsDataLoader.js +18 -17
  108. package/build/dataloaders/findIdsDataLoader.js.map +1 -1
  109. package/build/dataloaders/findOrCreateDataLoader.d.ts.map +1 -1
  110. package/build/dataloaders/findOrCreateDataLoader.js +3 -3
  111. package/build/dataloaders/findOrCreateDataLoader.js.map +1 -1
  112. package/build/dataloaders/findPaginatedDataLoader.d.ts +7 -0
  113. package/build/dataloaders/findPaginatedDataLoader.d.ts.map +1 -0
  114. package/build/dataloaders/findPaginatedDataLoader.js +84 -0
  115. package/build/dataloaders/findPaginatedDataLoader.js.map +1 -0
  116. package/build/dataloaders/manyToManyFindDataLoader.js +7 -1
  117. package/build/dataloaders/manyToManyFindDataLoader.js.map +1 -1
  118. package/build/drivers/EntityWriter.js +13 -7
  119. package/build/drivers/EntityWriter.js.map +1 -1
  120. package/build/drivers/buildRawQuery.d.ts +6 -4
  121. package/build/drivers/buildRawQuery.d.ts.map +1 -1
  122. package/build/drivers/buildRawQuery.js +8 -4
  123. package/build/drivers/buildRawQuery.js.map +1 -1
  124. package/build/fields.d.ts +2 -0
  125. package/build/fields.d.ts.map +1 -1
  126. package/build/fields.js +18 -6
  127. package/build/fields.js.map +1 -1
  128. package/build/index.d.ts +3 -1
  129. package/build/index.d.ts.map +1 -1
  130. package/build/index.js +4 -1
  131. package/build/index.js.map +1 -1
  132. package/build/keys.d.ts.map +1 -1
  133. package/build/keys.js +8 -6
  134. package/build/keys.js.map +1 -1
  135. package/build/loadHints.d.ts +13 -4
  136. package/build/loadHints.d.ts.map +1 -1
  137. package/build/loadHints.js +63 -9
  138. package/build/loadHints.js.map +1 -1
  139. package/build/loadLens.d.ts +11 -0
  140. package/build/loadLens.d.ts.map +1 -1
  141. package/build/loadLens.js +43 -10
  142. package/build/loadLens.js.map +1 -1
  143. package/build/newEntity.d.ts.map +1 -1
  144. package/build/newEntity.js +16 -14
  145. package/build/newEntity.js.map +1 -1
  146. package/build/newTestInstance.d.ts.map +1 -1
  147. package/build/newTestInstance.js +3 -0
  148. package/build/newTestInstance.js.map +1 -1
  149. package/build/normalizeHints.d.ts +7 -0
  150. package/build/normalizeHints.d.ts.map +1 -1
  151. package/build/normalizeHints.js +40 -2
  152. package/build/normalizeHints.js.map +1 -1
  153. package/build/preloading/JsonAggregatePreloader.d.ts.map +1 -1
  154. package/build/preloading/JsonAggregatePreloader.js +91 -9
  155. package/build/preloading/JsonAggregatePreloader.js.map +1 -1
  156. package/build/preloading/canPreload.d.ts +2 -2
  157. package/build/preloading/canPreload.d.ts.map +1 -1
  158. package/build/preloading/canPreload.js +3 -0
  159. package/build/preloading/canPreload.js.map +1 -1
  160. package/build/preloading/partitionHint.d.ts.map +1 -1
  161. package/build/preloading/partitionHint.js +10 -4
  162. package/build/preloading/partitionHint.js.map +1 -1
  163. package/build/reactiveHints.d.ts +2 -2
  164. package/build/reactiveHints.d.ts.map +1 -1
  165. package/build/reactiveHints.js +9 -0
  166. package/build/reactiveHints.js.map +1 -1
  167. package/build/relations/EnumCollection.d.ts +81 -0
  168. package/build/relations/EnumCollection.d.ts.map +1 -0
  169. package/build/relations/EnumCollection.js +211 -0
  170. package/build/relations/EnumCollection.js.map +1 -0
  171. package/build/relations/ManyToManyCollection.d.ts +1 -0
  172. package/build/relations/ManyToManyCollection.d.ts.map +1 -1
  173. package/build/relations/ManyToManyCollection.js +10 -5
  174. package/build/relations/ManyToManyCollection.js.map +1 -1
  175. package/build/relations/ManyToManyLargeCollection.d.ts +3 -2
  176. package/build/relations/ManyToManyLargeCollection.d.ts.map +1 -1
  177. package/build/relations/ManyToManyLargeCollection.js +7 -4
  178. package/build/relations/ManyToManyLargeCollection.js.map +1 -1
  179. package/build/relations/OneToManyCollection.d.ts.map +1 -1
  180. package/build/relations/OneToManyCollection.js +5 -3
  181. package/build/relations/OneToManyCollection.js.map +1 -1
  182. package/build/relations/OneToManyLargeCollection.d.ts.map +1 -1
  183. package/build/relations/OneToManyLargeCollection.js +2 -1
  184. package/build/relations/OneToManyLargeCollection.js.map +1 -1
  185. package/build/relations/ReactiveManyToMany.d.ts +1 -0
  186. package/build/relations/ReactiveManyToMany.d.ts.map +1 -1
  187. package/build/relations/ReactiveManyToMany.js +4 -1
  188. package/build/relations/ReactiveManyToMany.js.map +1 -1
  189. package/build/relations/ReactiveManyToManyOtherSide.d.ts +1 -0
  190. package/build/relations/ReactiveManyToManyOtherSide.d.ts.map +1 -1
  191. package/build/relations/ReactiveManyToManyOtherSide.js +4 -1
  192. package/build/relations/ReactiveManyToManyOtherSide.js.map +1 -1
  193. package/build/relations/RecursiveCollection.d.ts +2 -6
  194. package/build/relations/RecursiveCollection.d.ts.map +1 -1
  195. package/build/relations/RecursiveCollection.js +9 -17
  196. package/build/relations/RecursiveCollection.js.map +1 -1
  197. package/build/relations/RecursiveCycleError.d.ts +13 -0
  198. package/build/relations/RecursiveCycleError.d.ts.map +1 -0
  199. package/build/relations/RecursiveCycleError.js +15 -0
  200. package/build/relations/RecursiveCycleError.js.map +1 -0
  201. package/build/relations/hasOneThrough.d.ts.map +1 -1
  202. package/build/relations/hasOneThrough.js +6 -4
  203. package/build/relations/hasOneThrough.js.map +1 -1
  204. package/build/relations/hasProperty.d.ts +2 -1
  205. package/build/relations/hasProperty.d.ts.map +1 -1
  206. package/build/relations/hasProperty.js +15 -5
  207. package/build/relations/hasProperty.js.map +1 -1
  208. package/build/relations/index.d.ts +1 -0
  209. package/build/relations/index.d.ts.map +1 -1
  210. package/build/relations/index.js +5 -2
  211. package/build/relations/index.js.map +1 -1
  212. package/build/rules.js +1 -1
  213. package/build/rules.js.map +1 -1
  214. package/build/scopes.d.ts +185 -0
  215. package/build/scopes.d.ts.map +1 -0
  216. package/build/scopes.js +302 -0
  217. package/build/scopes.js.map +1 -0
  218. package/build/temporal.d.ts +32 -2
  219. package/build/temporal.d.ts.map +1 -1
  220. package/build/temporal.js +18 -0
  221. package/build/temporal.js.map +1 -1
  222. package/build/upsert.d.ts.map +1 -1
  223. package/build/upsert.js +9 -2
  224. package/build/upsert.js.map +1 -1
  225. package/build/utils.d.ts +2 -0
  226. package/build/utils.d.ts.map +1 -1
  227. package/build/utils.js +12 -0
  228. package/build/utils.js.map +1 -1
  229. package/package.json +8 -10
  230. package/build/caches.d.ts +0 -6
  231. package/build/caches.d.ts.map +0 -1
  232. package/build/caches.js +0 -42
  233. 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");
@@ -37,20 +36,23 @@ const findCountDataLoader_1 = require("./dataloaders/findCountDataLoader");
37
36
  const findDataLoader_1 = require("./dataloaders/findDataLoader");
38
37
  const findIdsDataLoader_1 = require("./dataloaders/findIdsDataLoader");
39
38
  const findOrCreateDataLoader_1 = require("./dataloaders/findOrCreateDataLoader");
39
+ const findPaginatedDataLoader_1 = require("./dataloaders/findPaginatedDataLoader");
40
40
  const Entity_1 = require("./Entity");
41
41
  const FlushLock_1 = require("./FlushLock");
42
42
  const index_1 = require("./index");
43
43
  const IsLoadedCache_1 = require("./IsLoadedCache");
44
44
  const JoinRows_1 = require("./JoinRows");
45
+ const loadHints_1 = require("./loadHints");
45
46
  const newEntity_1 = require("./newEntity");
46
47
  const newTestInstance_1 = require("./newTestInstance");
47
48
  const PluginManager_1 = require("./PluginManager");
48
49
  const ReactionsManager_1 = require("./ReactionsManager");
49
50
  const reactiveHints_1 = require("./reactiveHints");
50
51
  const relations_1 = require("./relations");
51
- const hasAsyncMethod_1 = require("./relations/hasAsyncMethod");
52
52
  const AsyncProperty_1 = require("./relations/AsyncProperty");
53
+ const hasAsyncMethod_1 = require("./relations/hasAsyncMethod");
53
54
  const RecursiveCollection_1 = require("./relations/RecursiveCollection");
55
+ const scopes_1 = require("./scopes");
54
56
  const Todo_1 = require("./Todo");
55
57
  const trusted_1 = require("./trusted");
56
58
  const upsert_1 = require("./upsert");
@@ -87,6 +89,8 @@ class EntityManager {
87
89
  txn;
88
90
  entityLimit = defaultEntityLimit;
89
91
  #entitiesArray = [];
92
+ // Incrementally track dirty entities so we don't have to scan `#entityArray` during flush
93
+ #maybePendingFlushEntities = new Set();
90
94
  // Indexes the currently loaded entities by their tagged ids and `toTaggedString` ids (i.e. `a#`). This fixes
91
95
  // real-world performance issues where `findExistingInstance` scanning `#entities` was an `O(n^2)`.
92
96
  #entitiesById = new Map();
@@ -105,6 +109,7 @@ class EntityManager {
105
109
  * so that both see the most accurate state.
106
110
  */
107
111
  #pendingDeletes = [];
112
+ #hasAnyDeletes = false;
108
113
  #dataloaders = {};
109
114
  #batchLoaders = {};
110
115
  #joinRows = {};
@@ -154,6 +159,20 @@ class EntityManager {
154
159
  indexManager: this.#indexManager,
155
160
  isLoadedCache: this.#isLoadedCache,
156
161
  pluginManager,
162
+ markMaybePending(entity) {
163
+ em.#maybePendingFlushEntities.add(entity);
164
+ },
165
+ unmarkMaybePending(entity) {
166
+ em.#maybePendingFlushEntities.delete(entity);
167
+ },
168
+ hasAnyDeletes() {
169
+ return em.#hasAnyDeletes;
170
+ },
171
+ pendingDeleteIds(type) {
172
+ return em.#pendingDeletes
173
+ .filter((entity) => entity instanceof type && entity.idMaybe !== undefined)
174
+ .map((entity) => (0, index_1.keyToNumber)((0, index_1.getMetadata)(entity), entity.id));
175
+ },
157
176
  isMerging(entity) {
158
177
  return em.#merging?.has(entity) ?? false;
159
178
  },
@@ -219,7 +238,8 @@ class EntityManager {
219
238
  }
220
239
  for (const joinRows of Object.values(this.#joinRows)) {
221
240
  const todo = joinRows.toTodo();
222
- if (todo) {
241
+ // Enum m2ms (EnumCollections) aren't entity-to-entity, so skip them here.
242
+ if (todo && !todo.m2m.otherEnum) {
223
243
  for (const row of todo.newRows) {
224
244
  const entities = Object.values(row.columns);
225
245
  changes.push({ kind: "m2m", op: "add", joinTableName: todo.m2m.joinTableName, entities });
@@ -246,10 +266,11 @@ class EntityManager {
246
266
  }
247
267
  async find(type, where, options) {
248
268
  const { populate, ...rest } = options || {};
249
- const settings = { where, ...rest };
250
- const result = await (0, findDataLoader_1.findDataLoader)(this, type, settings, populate)
251
- .load(settings)
252
- .catch(function find(err) {
269
+ const normalized = (0, index_1.mergeFindOptions)(where, rest);
270
+ const settings = { where: normalized.where, ...normalized.options };
271
+ const result = await (hasPaginationSettings(normalized.options)
272
+ ? (0, findPaginatedDataLoader_1.findPaginatedDataLoader)(this, type, settings, populate)
273
+ : (0, findDataLoader_1.findDataLoader)(this, type, settings, populate)).catch(function find(err) {
253
274
  throw appendStack(err, new Error());
254
275
  });
255
276
  if (populate) {
@@ -257,37 +278,52 @@ class EntityManager {
257
278
  }
258
279
  return result;
259
280
  }
260
- async findPaginated(type, where, options) {
261
- const { populate, limit, offset, ...rest } = options || {};
262
- const meta = (0, index_1.getMetadata)(type);
263
- const query = (0, index_1.parseFindQuery)(meta, where, rest);
264
- const rows = await this.executeFind(meta, "findPaginated", query, { ...rest, limit, offset, checkLimit: false });
265
- const result = this.hydrate(type, rows);
266
- if (populate) {
267
- await this.populate(result, populate);
268
- }
269
- return result;
270
- }
271
281
  /** Runs the post-parse find pipeline: plugins mutate the logical AST, then Joist optimizes/prunes before SQL. */
272
282
  async executeFind(meta, operation, parsed, settings) {
273
- const { checkLimit, ...driverSettings } = settings;
283
+ const { checkLimit, findSettings } = this.prepareFind(meta, operation, parsed, settings);
284
+ return this.executePreparedFind(meta, operation, parsed, findSettings, checkLimit);
285
+ }
286
+ /** Executes a query that has already had find hooks and optimizations applied. */
287
+ async executePreparedFind(meta, operation, parsed, findSettings, checkLimit) {
274
288
  const { pluginManager } = getEmInternalApi(this);
275
- pluginManager.beforeFind(meta, operation, parsed, driverSettings);
276
- (0, index_1.optimizeCollectionJoins)(parsed, settings);
277
- const rows = await this.driver.executeFind(this, parsed, driverSettings);
289
+ const rows = await this.driver.executeFind(this, parsed, findSettings);
278
290
  // Check by default unless explicitly disabled or the caller removed the LIMIT via `limit: undefined`
279
- const shouldCheck = checkLimit ?? !("limit" in settings && settings.limit === undefined);
291
+ const shouldCheck = checkLimit ?? !("limit" in findSettings && findSettings.limit === undefined);
280
292
  if (shouldCheck && rows.length >= this.entityLimit) {
281
293
  throw new Error(`Query returned more than ${this.entityLimit} entityLimit rows`);
282
294
  }
283
295
  pluginManager.afterFind(meta, operation, rows);
284
296
  return rows;
285
297
  }
286
- async findGql(type, where, options) {
287
- return this.find(type, where, options);
298
+ /**
299
+ * Runs pre-SQL find hooks and optimizations against a parsed query.
300
+ *
301
+ * This allows plugins to see "pre-batched" / "logical" query ASTs, instead of our
302
+ * more complicated `_find` batched queries. The flow would be:
303
+ *
304
+ * - A loader calls `prepareFind(originalQuery)`
305
+ * - Plugins inspect/modify the query as/if needed
306
+ * - The loader crafts a new, more complicated query that embeds the originalQuery
307
+ * - The loader calls `executePreparedFind` with the 2nd query
308
+ */
309
+ prepareFind(meta, operation, parsed, settings) {
310
+ const { checkLimit, ...findSettings } = settings;
311
+ const { pluginManager } = getEmInternalApi(this);
312
+ // Plugins may mutate the settings object, so return the post-hook version that loaders must reuse.
313
+ pluginManager.beforeFind(meta, operation, parsed, findSettings);
314
+ (0, index_1.optimizeCollectionJoins)(parsed, settings);
315
+ return { checkLimit, findSettings };
288
316
  }
289
- async findGqlPaginated(type, where, options) {
290
- return this.findPaginated(type, where, options);
317
+ async findGql(type, where, options) {
318
+ if (!options) {
319
+ return this.find(type, where);
320
+ }
321
+ const normalized = { ...options };
322
+ if ("limit" in normalized)
323
+ normalized.limit = normalized.limit ?? undefined;
324
+ if ("offset" in normalized)
325
+ normalized.offset = normalized.offset ?? undefined;
326
+ return this.find(type, where, normalized);
291
327
  }
292
328
  async findOne(type, where, options) {
293
329
  const list = await this.find(type, where, options);
@@ -329,6 +365,9 @@ class EntityManager {
329
365
  }
330
366
  else {
331
367
  const [entity] = this.hydrate(type, [row]);
368
+ if (this.#hasAnyDeletes && entity.isDeletedEntity) {
369
+ return undefined;
370
+ }
332
371
  if (populate) {
333
372
  await this.populate(entity, populate);
334
373
  }
@@ -344,17 +383,16 @@ class EntityManager {
344
383
  * Note: this method is not currently auto-batched, so it will cause N+1s if called in a loop.
345
384
  */
346
385
  async findCount(type, where, options = {}) {
347
- const settings = { where, ...options };
348
- let count = await (0, findCountDataLoader_1.findCountDataLoader)(this, type, settings)
349
- .load(settings)
350
- .catch(function findCount(err) {
386
+ const normalized = (0, findCountDataLoader_1.mergeCountOptions)(where, options);
387
+ const settings = { where: normalized.where, ...normalized.options };
388
+ let count = await (0, findCountDataLoader_1.findCountDataLoader)(this, type, settings).catch(function findCount(err) {
351
389
  throw appendStack(err, new Error());
352
390
  });
353
391
  // If the user is do "count all", we can adjust the number up/down based on
354
392
  // WIP creates/deletes. We can't do this if the WHERE clause is populated b/c
355
393
  // then we'd also have to eval each created/deleted entity against the WHERE
356
394
  // clause before knowing if it should adjust teh amount.
357
- const isSelectAll = Object.keys(where).length === 0;
395
+ const isSelectAll = (0, scopes_1.isSelectAllFilter)(where, options.conditions);
358
396
  if (isSelectAll) {
359
397
  const tagged = this.#entitiesByTag.get((0, index_1.getMetadata)(type).tagName) ?? [];
360
398
  for (const entity of tagged) {
@@ -378,10 +416,9 @@ class EntityManager {
378
416
  * as `em.find`.
379
417
  */
380
418
  async findIds(type, where, options = {}) {
381
- const settings = { where, ...options };
382
- return (0, findIdsDataLoader_1.findIdsDataLoader)(this, type, settings)
383
- .load(settings)
384
- .catch(function findIds(err) {
419
+ const normalized = (0, findCountDataLoader_1.mergeCountOptions)(where, options);
420
+ const settings = { where: normalized.where, ...normalized.options };
421
+ return (0, findIdsDataLoader_1.findIdsDataLoader)(this, type, settings).catch(function findIds(err) {
385
422
  throw appendStack(err, new Error());
386
423
  });
387
424
  }
@@ -485,6 +522,7 @@ class EntityManager {
485
522
  case "primaryKey":
486
523
  case "o2m":
487
524
  case "m2m":
525
+ case "m2mEnum":
488
526
  case "o2o":
489
527
  case "lo2m":
490
528
  return undefined;
@@ -613,6 +651,7 @@ class EntityManager {
613
651
  else if (kind === "primitive" ||
614
652
  kind === "primaryKey" ||
615
653
  kind === "enum" ||
654
+ kind === "m2mEnum" ||
616
655
  kind === "poly" ||
617
656
  kind === "m2o" ||
618
657
  kind === "lo2m") {
@@ -670,48 +709,86 @@ class EntityManager {
670
709
  }
671
710
  async loadAll(type, _ids, hint) {
672
711
  const meta = (0, index_1.getMetadata)(type);
673
- const ids = _ids.map((id) => (0, index_1.tagId)(meta, id));
674
- const idsToLoad = ids.filter((id) => !this.findExistingInstance(id));
675
- if (idsToLoad.length > 0) {
712
+ // Use pre-allocated arrays/for loops instead of `.filter`s since this can be a hot spot
713
+ const ids = new Array(_ids.length);
714
+ const entities = new Array(_ids.length);
715
+ let idsToLoad;
716
+ let positionsToLoad;
717
+ for (let i = 0; i < _ids.length; i++) {
718
+ const id = (0, index_1.tagId)(meta, _ids[i]);
719
+ ids[i] = id;
720
+ const entity = this.findExistingInstance(id);
721
+ if (entity) {
722
+ entities[i] = entity;
723
+ }
724
+ else {
725
+ (idsToLoad ??= []).push(id);
726
+ (positionsToLoad ??= []).push(i);
727
+ }
728
+ }
729
+ if (idsToLoad && idsToLoad.length > 0) {
676
730
  await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
677
731
  .loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
678
732
  .catch(function loadAll(err) {
679
733
  throw appendStack(err, new Error());
680
734
  });
735
+ for (const i of positionsToLoad) {
736
+ entities[i] = this.findExistingInstance(ids[i]);
737
+ }
681
738
  }
682
- const entities = [];
683
- for (const id of ids) {
684
- const entity = this.findExistingInstance(id);
685
- if (entity)
686
- entities.push(entity);
739
+ let idsNotFound;
740
+ for (let i = 0; i < entities.length; i++) {
741
+ if (entities[i] === undefined) {
742
+ (idsNotFound ??= []).push(ids[i]);
743
+ }
687
744
  }
688
- if (entities.length !== ids.length) {
689
- const idsNotFound = ids.filter((_, i) => entities[i] === undefined);
745
+ if (idsNotFound) {
690
746
  throw new NotFoundError(`${idsNotFound.join(",")} were not found`);
691
747
  }
748
+ const loadedEntities = entities;
692
749
  if (hint) {
693
- await this.populate(entities, hint);
750
+ await this.populate(loadedEntities, hint);
694
751
  }
695
752
  if (meta.inheritanceType === "sti" && meta.baseType) {
696
- const wrongType = entities.filter((e) => !(e instanceof meta.cstr));
753
+ const wrongType = loadedEntities.filter((e) => !(e instanceof meta.cstr));
697
754
  if (wrongType.length > 0) {
698
755
  throw new Error(`${wrongType.join(", ")} were not of type ${meta.cstr.name}`);
699
756
  }
700
757
  }
701
- return entities;
758
+ return loadedEntities;
702
759
  }
703
760
  async loadAllIfExists(type, _ids, hint) {
704
761
  const meta = (0, index_1.getMetadata)(type);
705
- const ids = _ids.map((id) => (0, index_1.tagId)(meta, id));
706
- const idsToLoad = ids.filter((id) => !this.findExistingInstance(id));
707
- if (idsToLoad.length > 0) {
762
+ // Use pre-allocated arrays/for loops instead of `.filter`s since this can be a hot spot
763
+ const ids = new Array(_ids.length);
764
+ const entities = [];
765
+ let idsToLoad;
766
+ // Ensure the ids are tagged, and find any not-yet-loaded
767
+ for (let i = 0; i < _ids.length; i++) {
768
+ const id = (0, index_1.tagId)(meta, _ids[i]);
769
+ ids[i] = id;
770
+ const entity = this.findExistingInstance(id);
771
+ if (entity) {
772
+ entities.push(entity);
773
+ }
774
+ else {
775
+ (idsToLoad ??= []).push(id);
776
+ }
777
+ }
778
+ if (idsToLoad && idsToLoad.length > 0) {
708
779
  await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
709
780
  .loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
710
781
  .catch(function loadAllIfExists(err) {
711
782
  throw appendStack(err, new Error());
712
783
  });
784
+ // Now that everything is loaded, recalc `entities`
785
+ entities.length = 0;
786
+ for (const id of ids) {
787
+ const entity = this.findExistingInstance(id);
788
+ if (entity)
789
+ entities.push(entity);
790
+ }
713
791
  }
714
- const entities = ids.map((id) => this.findExistingInstance(id)).filter(Boolean);
715
792
  if (hint) {
716
793
  await this.populate(entities, hint);
717
794
  }
@@ -762,6 +839,12 @@ class EntityManager {
762
839
  if (list.length === 0) {
763
840
  return !fn ? entityOrList : fn(entityOrList);
764
841
  }
842
+ // Avoid building a HintTree/batchloader for persisted entities when everything is already loaded; in the phase 9
843
+ // benchmark this moved already-loaded populates from ~1.10ms to ~0.89ms, and nested already-loaded populates from
844
+ // ~2.39ms to ~1.94ms.
845
+ if (!opts.forceReload && list.every((entity) => !entity.isNewEntity && (0, loadHints_1.isLoadedForPopulate)(entity, hintOpt))) {
846
+ return fn ? fn(entityOrList) : entityOrList;
847
+ }
765
848
  const meta = (0, index_1.getMetadata)(list[0]);
766
849
  if (this.#preloader) {
767
850
  // If we can preload, prevent promise deadlocking by one large-batch preload populate (which can't have
@@ -810,9 +893,11 @@ class EntityManager {
810
893
  delete(entityOrArray) {
811
894
  for (const entity of (0, utils_1.toArray)(entityOrArray)) {
812
895
  // Early return if already deleted.
813
- const alreadyMarked = (0, BaseEntity_1.getInstanceData)(entity).markDeleted(entity);
896
+ const alreadyMarked = (0, BaseEntity_1.getInstanceData)(entity).markDeleted();
814
897
  if (!alreadyMarked)
815
898
  continue;
899
+ // This monotonic flag lets find hot paths skip scanning results until a delete has ever happened in this EM.
900
+ this.#hasAnyDeletes = true;
816
901
  // Any derived fields that read this entity will need recalc-d
817
902
  this.#rm.queueAllDownstreamFields(entity, "deleted");
818
903
  // Synchronously unhook the entity if the relations are loaded
@@ -906,7 +991,7 @@ class EntityManager {
906
991
  const pendingHooks = new Set();
907
992
  // Subset of pendingFlush entities that had hooks invoked in a prior `runHooksOnPendingEntities`
908
993
  const alreadyRanHooks = new Set();
909
- findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
994
+ findPendingFlushEntities(this.#maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
910
995
  // If we're re-looping for AsyncReactiveField, make sure to bump updatedAt
911
996
  // each time, so that for an INSERT-then-UPDATE the triggers don't think the
912
997
  // UPDATE forgot to self-bump updatedAt, and then "helpfully" bump it for us.
@@ -930,9 +1015,13 @@ class EntityManager {
930
1015
  // fields, they'd be via the synchronous getter and would not be stale.
931
1016
  recalcSynchronousDerivedFields(todos);
932
1017
  // The hooks could have deleted this-loop or prior-loop entities, so re-cascade again.
933
- await this.flushDeletes();
934
- // The hooks could have changed fields, so recalc again.
935
- await this.#rm.recalcPendingReactables("reactables");
1018
+ // Reactions (the recalc below) can themselves `em.delete` entities, so keep draining
1019
+ // until both deletes and recalcs have settled.
1020
+ do {
1021
+ await this.flushDeletes();
1022
+ // The hooks could have changed fields, so recalc again.
1023
+ await this.#rm.recalcPendingReactables("reactables");
1024
+ } while (this.#pendingDeletes.length > 0);
936
1025
  // We may have reactables that failed earlier, but will succeed now that hooks have been run and cascade
937
1026
  // deletes have been processed
938
1027
  if (this.#rm.hasPendingTypeErrors) {
@@ -945,7 +1034,7 @@ class EntityManager {
945
1034
  hooksInvoked.add(e);
946
1035
  pendingHooks.clear();
947
1036
  // See if the hooks mutated any new, not-yet-hooksInvoked entities
948
- findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
1037
+ findPendingFlushEntities(this.#maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
949
1038
  // The final run of recalcPendingReactables could have left us with pending type errors and no entities in
950
1039
  // pendingHooks. If so, we need to re-run recalcPendingTypeErrors to get those errors to transition into
951
1040
  // suppressed errors so that we will fail after simpleValidation.
@@ -1004,7 +1093,7 @@ class EntityManager {
1004
1093
  // The driver will handle the right thing if we're already in an existing transaction.
1005
1094
  await this.driver.transaction(this, async () => {
1006
1095
  do {
1007
- if (Object.keys(entityTodos).length > 0 || Object.keys(joinRowTodos)) {
1096
+ if (Object.keys(entityTodos).length > 0 || Object.keys(joinRowTodos).length > 0) {
1008
1097
  await this.driver.flush(this, entityTodos, joinRowTodos);
1009
1098
  }
1010
1099
  // Now that we've flushed, we can let plugins know what we've done.
@@ -1080,7 +1169,7 @@ class EntityManager {
1080
1169
  for (const e of createdThenDeleted)
1081
1170
  (0, BaseEntity_1.getInstanceData)(e).fixupCreatedThenDeleted();
1082
1171
  this.#merging?.clear();
1083
- return [...allFlushedEntities];
1172
+ return [...allFlushedEntities].sort((a, b) => (0, BaseEntity_1.getInstanceData)(a).entityIndex - (0, BaseEntity_1.getInstanceData)(b).entityIndex);
1084
1173
  }
1085
1174
  catch (e) {
1086
1175
  if (e instanceof RecursiveCollection_1.RecursiveCycleError) {
@@ -1096,15 +1185,28 @@ class EntityManager {
1096
1185
  }
1097
1186
  }
1098
1187
  }
1188
+ // node-pg keeps the DETAIL line (i.e. "Key (identity_id, final_id)=(102591, null) already exists")
1189
+ // on a separate `detail` field, so keep it to suffix onto messages to help debugging.
1190
+ const detail = e && typeof e === "object" && "detail" in e && typeof e.detail === "string" ? e.detail : undefined;
1099
1191
  if (e && typeof e === "object" && "constraint" in e && typeof e.constraint === "string") {
1100
1192
  // node-pg errors use `constraint` to indicate the constraint name
1101
1193
  const message = config_1.constraintNameToValidationError[e.constraint];
1102
1194
  if (message) {
1103
- throw new index_1.ValidationErrors(message);
1195
+ const errors = new index_1.ValidationErrors(message);
1196
+ // Keep the user-facing `errors[].message` pretty, but suffix the DETAIL onto the top-level
1197
+ // `message` so the debugging context survives into logs/stack traces.
1198
+ if (detail)
1199
+ errors.message = `${message} — ${detail}`;
1200
+ throw errors;
1104
1201
  }
1105
1202
  }
1106
- if (e instanceof InMemoryRollbackError)
1107
- return [...allFlushedEntities];
1203
+ if (e instanceof InMemoryRollbackError) {
1204
+ return [...allFlushedEntities].sort((a, b) => (0, BaseEntity_1.getInstanceData)(a).entityIndex - (0, BaseEntity_1.getInstanceData)(b).entityIndex);
1205
+ }
1206
+ // For raw (non-pretty) pg errors, fold the DETAIL into `message` so it survives into logs.
1207
+ if (detail && e instanceof Error) {
1208
+ e.message = `${e.message} — ${detail}`;
1209
+ }
1108
1210
  throw e;
1109
1211
  }
1110
1212
  finally {
@@ -1212,10 +1314,13 @@ class EntityManager {
1212
1314
  */
1213
1315
  hydrate(type, rows, options) {
1214
1316
  const maybeBaseMeta = (0, index_1.getMetadata)(type);
1317
+ const taggedIdPrefix = `${maybeBaseMeta.tagName}:`;
1318
+ const overwriteExisting = options?.overwriteExisting === true;
1215
1319
  let i = 0;
1216
1320
  const entities = new Array(rows.length);
1217
1321
  for (const row of rows) {
1218
- const taggedId = (0, index_1.keyToTaggedId)(maybeBaseMeta, row["id"]) || (0, utils_1.fail)("No id column was available");
1322
+ const id = row["id"];
1323
+ const taggedId = id === undefined || id === null ? (0, utils_1.fail)("No id column was available") : `${taggedIdPrefix}${id}`;
1219
1324
  // See if this is already in our UoW
1220
1325
  let entity = this.findExistingInstance(taggedId);
1221
1326
  if (!entity) {
@@ -1224,23 +1329,36 @@ class EntityManager {
1224
1329
  // Pass id as a hint that we're in hydrate mode
1225
1330
  entity = (0, newEntity_1.newEntity)(this, (0, index_1.asConcreteCstr)(meta.cstr), false);
1226
1331
  (0, BaseEntity_1.getInstanceData)(entity).row = row;
1227
- this.#doRegister(entity, taggedId);
1332
+ this.#doRegister(entity, taggedId, meta, true);
1228
1333
  }
1229
- else if (options?.overwriteExisting === true) {
1334
+ else if (overwriteExisting) {
1230
1335
  // Usually if the entity already exists, we don't write over it, but in this case we assume that
1231
1336
  // `EntityManager.refresh` is telling us to explicitly load the latest data.
1232
1337
  // First swap out the old row with the new row
1233
- (0, BaseEntity_1.getInstanceData)(entity).row = row;
1338
+ const instanceData = (0, BaseEntity_1.getInstanceData)(entity);
1339
+ instanceData.row = row;
1234
1340
  // And then only refresh the data keys that have already been serde-d from rows
1235
1341
  // (this keeps us from deserializing data out of rows that we don't need).
1236
- const { data, originalData } = (0, BaseEntity_1.getInstanceData)(entity);
1237
- const changedFields = entity.changes.fieldsWithoutRelations;
1238
- for (const fieldName of Object.keys(data)) {
1239
- const serde = (0, index_1.getMetadata)(entity).allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1240
- serde.setOnEntity(data, row);
1241
- // Make the field look not-dirty
1242
- if (changedFields.includes(fieldName)) {
1243
- delete originalData[fieldName];
1342
+ const { data } = instanceData;
1343
+ const dataKeys = Object.keys(data);
1344
+ if (dataKeys.length > 0) {
1345
+ const allFields = (0, index_1.getMetadata)(entity).allFields;
1346
+ const changedFields = entity.changes.fieldsWithoutRelations;
1347
+ if (changedFields.length === 0) {
1348
+ for (const fieldName of dataKeys) {
1349
+ const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1350
+ serde.setOnEntity(data, row);
1351
+ }
1352
+ }
1353
+ else {
1354
+ for (const fieldName of dataKeys) {
1355
+ const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1356
+ serde.setOnEntity(data, row);
1357
+ // Make the field look not-dirty
1358
+ if (changedFields.includes(fieldName)) {
1359
+ instanceData.markFieldClean(fieldName);
1360
+ }
1361
+ }
1244
1362
  }
1245
1363
  }
1246
1364
  }
@@ -1249,8 +1367,9 @@ class EntityManager {
1249
1367
  return entities;
1250
1368
  }
1251
1369
  touch(entityOrEntities) {
1252
- for (const entity of (0, utils_1.toArray)(entityOrEntities))
1253
- (0, BaseEntity_1.getInstanceData)(entity).isTouched = true;
1370
+ for (const entity of (0, utils_1.toArray)(entityOrEntities)) {
1371
+ (0, BaseEntity_1.getInstanceData)(entity).markTouched();
1372
+ }
1254
1373
  }
1255
1374
  async recalc(entityOrEntities) {
1256
1375
  // Look for async reactive fields
@@ -1327,9 +1446,16 @@ class EntityManager {
1327
1446
  // Run the beforeDelete hook before we unhook the entity
1328
1447
  const todos = (0, Todo_1.createTodos)(entities);
1329
1448
  await beforeDelete(this.ctx, todos);
1330
- // For all relations, unhook the entity from the other side
1331
- // (...we're using `concat` because `.push(...reallyBigArray)` with ~100k relations can blow the stack size
1332
- relationsToCleanup = relationsToCleanup.concat(entities.flatMap(index_1.getRelations));
1449
+ // For all relations, unhook the entity from the other side; this append path is optimized for
1450
+ // large deletes with ~100k relations by avoiding `flatMap` intermediates and `concat` copies.
1451
+ for (const entity of entities) {
1452
+ const relations = (0, index_1.getRelations)(entity);
1453
+ const start = relationsToCleanup.length;
1454
+ relationsToCleanup.length += relations.length;
1455
+ for (let i = 0; i < relations.length; i++) {
1456
+ relationsToCleanup[start + i] = relations[i];
1457
+ }
1458
+ }
1333
1459
  entities = this.#pendingDeletes;
1334
1460
  this.#pendingDeletes = [];
1335
1461
  }
@@ -1385,9 +1511,9 @@ class EntityManager {
1385
1511
  return entities.filter((e) => e instanceof cstr && !e.isDeletedEntity);
1386
1512
  }
1387
1513
  if (this.#indexManager.shouldIndexType(entities.length)) {
1388
- this.#indexManager.enableIndexingForType(meta, entities);
1514
+ this.#indexManager.enableIndexingForType(meta, entities, where);
1389
1515
  return (this.#indexManager
1390
- .findMatching(meta, where)
1516
+ .findMatching(meta, entities, where)
1391
1517
  // Still filter by `instanceof cstr` to handle subtyping
1392
1518
  .filter((e) => e instanceof cstr && !e.isDeletedEntity));
1393
1519
  }
@@ -1487,7 +1613,8 @@ class EntityManager {
1487
1613
  continue;
1488
1614
  const { originalData: oldOriginalData, data: oldData } = oldInstanceData;
1489
1615
  const newEntity = mapEntity(oldEntity);
1490
- const { originalData: newOriginalData, data: newData } = newEntity.__data;
1616
+ const newInstanceData = newEntity.__data;
1617
+ const { data: newData } = newInstanceData;
1491
1618
  // for new entities, anything in `data` is changed and should be copied across. for existing entities, we
1492
1619
  // only care about changed fields, which are enumerated by originalData
1493
1620
  const maybeEntity = (value) => ((0, Entity_1.isEntity)(value) ? mapEntity(value) : value);
@@ -1495,7 +1622,7 @@ class EntityManager {
1495
1622
  for (const field of fields) {
1496
1623
  // copy over originalData so .changes is consistent across ems
1497
1624
  if (field in oldOriginalData)
1498
- newOriginalData[field] = maybeEntity(oldOriginalData[field]);
1625
+ newInstanceData.markFieldDirty(field, maybeEntity(oldOriginalData[field]));
1499
1626
  newData[field] = maybeEntity(oldData[field]);
1500
1627
  }
1501
1628
  }
@@ -1505,7 +1632,7 @@ class EntityManager {
1505
1632
  const newEntity = mapEntity(oldEntity);
1506
1633
  if (oldEntity.isDeletedEntity) {
1507
1634
  // If the old entity was deleted, that should be persisted in the new em
1508
- newEntity.__data.markDeleted(newEntity);
1635
+ newEntity.__data.markDeleted();
1509
1636
  // deleted entities will fail if you try to `get` their relations, so skip them since they should be cleared
1510
1637
  // out regardless
1511
1638
  continue;
@@ -1650,11 +1777,11 @@ class EntityManager {
1650
1777
  }
1651
1778
  }
1652
1779
  /** Registers a newly-instantiated entity with our EntityManager; only called by #doCreate and hydrate. */
1653
- #doRegister(entity, id) {
1780
+ #doRegister(entity, id, meta, skipDuplicateCheck = false) {
1654
1781
  // Keep our indexes up to date...
1655
1782
  const maybeId = id ?? entity.idTaggedMaybe;
1656
1783
  if (maybeId) {
1657
- if (this.findExistingInstance(maybeId) !== undefined) {
1784
+ if (!skipDuplicateCheck && this.findExistingInstance(maybeId) !== undefined) {
1658
1785
  throw new Error(`Entity ${entity} has a duplicate instance already loaded`);
1659
1786
  }
1660
1787
  this.#entitiesById.set(maybeId, entity);
@@ -1663,8 +1790,9 @@ class EntityManager {
1663
1790
  // Also register by the `a#1` style tagged string for new entities
1664
1791
  this.#entitiesById.set(entity.toTaggedString(), entity);
1665
1792
  }
1793
+ (0, BaseEntity_1.getInstanceData)(entity).entityIndex = this.#entitiesArray.length;
1666
1794
  this.#entitiesArray.push(entity);
1667
- const meta = (0, index_1.getMetadata)(entity);
1795
+ meta ??= (0, index_1.getMetadata)(entity);
1668
1796
  const set = this.#entitiesByTag.get(meta.tagName) ?? [];
1669
1797
  if (set.length === 0)
1670
1798
  this.#entitiesByTag.set(meta.tagName, set);
@@ -1678,8 +1806,6 @@ class EntityManager {
1678
1806
  .join(", ");
1679
1807
  throw new Error(`More than ${this.entityLimit} entities have been instantiated (top entities: ${topTypes})`);
1680
1808
  }
1681
- // If indexing is enabled for this type, add it...
1682
- this.#indexManager.maybeIndexEntity(entity);
1683
1809
  }
1684
1810
  }
1685
1811
  exports.EntityManager = EntityManager;
@@ -1763,14 +1889,14 @@ async function validateReactiveRules(em, logger, todos, joinRowTodos) {
1763
1889
  const p1 = Object.values(todos).flatMap((todo) => {
1764
1890
  const entities = [...todo.inserts, ...todo.updates, ...todo.deletes];
1765
1891
  // Find each statically-declared reactive rule for the given entity type
1766
- const rules = (0, caches_1.getReactiveRules)(todo.metadata);
1892
+ const rules = todo.metadata.reactiveRules;
1767
1893
  return rules.map((rule) => {
1768
1894
  // Of all changed entities of this type, how many specifically trigger this rule?
1769
1895
  const triggered = entities.filter((e) => {
1770
1896
  // If the rule is for a different subtype, skip it
1771
1897
  if (!(e instanceof rule.source))
1772
1898
  return false;
1773
- // Any new-or-deleted entity fires every rule (getReactiveRules has already filtered out read-only)
1899
+ // Any new-or-deleted entity fires every rule (reactiveRules has already filtered out read-only)
1774
1900
  if (e.isNewEntity || e.isDeletedEntity)
1775
1901
  return true;
1776
1902
  // Otherwise see if the changed fields overlaps with the rule's fields
@@ -1786,22 +1912,26 @@ async function validateReactiveRules(em, logger, todos, joinRowTodos) {
1786
1912
  });
1787
1913
  });
1788
1914
  const p2 = Object.values(joinRowTodos).flatMap((todo) => {
1789
- const entities = [...todo.newRows, ...todo.deletedRows].flatMap((jr) => Object.values(jr.columns));
1790
- // Do the first side
1791
- const p1 = (0, caches_1.getReactiveRules)(todo.m2m.meta)
1792
- .filter((rule) => rule.fields.includes(todo.m2m.fieldName))
1915
+ // For enum m2ms the columns include the enum's numeric id, so keep only the entity values.
1916
+ const entities = [...todo.newRows, ...todo.deletedRows].flatMap((jr) => Object.values(jr.columns)).filter(Entity_1.isEntity);
1917
+ // The owning-entity side (applies to both entity-to-entity and entity-to-enum m2ms).
1918
+ const a = todo.m2m.meta
1919
+ .reactiveRules.filter((rule) => rule.fields.includes(todo.m2m.fieldName))
1793
1920
  .map((rule) => {
1794
1921
  const triggered = entities.filter((e) => e instanceof todo.m2m.meta.cstr);
1795
1922
  return followAndQueue(triggered, rule);
1796
1923
  });
1797
- // And the second side
1798
- const p2 = (0, caches_1.getReactiveRules)(todo.m2m.otherMeta)
1799
- .filter((rule) => rule.fields.includes(todo.m2m.otherFieldName))
1800
- .map((rule) => {
1801
- const triggered = entities.filter((e) => e instanceof todo.m2m.otherMeta.cstr);
1802
- return followAndQueue(triggered, rule);
1803
- });
1804
- return [...p1, ...p2];
1924
+ // The "other" side only exists for entity-to-entity m2ms; enums have no reverse field.
1925
+ const { otherMeta, otherFieldName } = todo.m2m;
1926
+ const b = otherMeta
1927
+ ? otherMeta
1928
+ .reactiveRules.filter((rule) => rule.fields.includes(otherFieldName))
1929
+ .map((rule) => {
1930
+ const triggered = entities.filter((e) => e instanceof otherMeta.cstr);
1931
+ return followAndQueue(triggered, rule);
1932
+ })
1933
+ : [];
1934
+ return [...a, ...b];
1805
1935
  });
1806
1936
  (0, utils_1.failIfAnyRejected)(await Promise.allSettled([...p1, ...p2]));
1807
1937
  // Now that we've found the fn+entities to run, run them and collect any errors
@@ -1883,7 +2013,11 @@ function entitiesFromTodos(entityTodos, joinRowTodos) {
1883
2013
  }
1884
2014
  for (const todo of Object.values(joinRowTodos)) {
1885
2015
  [...todo.newRows, ...todo.deletedRows].forEach((row) => {
1886
- Object.values(row.columns).forEach((entity) => entities.add(entity));
2016
+ // For enum m2ms, a column value is the enum's numeric id rather than an entity.
2017
+ Object.values(row.columns).forEach((value) => {
2018
+ if ((0, Entity_1.isEntity)(value))
2019
+ entities.add(value);
2020
+ });
1887
2021
  });
1888
2022
  }
1889
2023
  return [...entities];
@@ -2025,7 +2159,7 @@ function maybeBumpUpdatedAt(rm, todos, now) {
2025
2159
  // it has changed. This is technically true, but this will break the oplock SQL generation,
2026
2160
  // so force the field to be dirty.
2027
2161
  const orm = (0, BaseEntity_1.getInstanceData)(e);
2028
- orm.originalData[updatedAt] = (0, fields_1.getField)(e, updatedAt);
2162
+ orm.markFieldDirty(updatedAt, (0, fields_1.getField)(e, updatedAt));
2029
2163
  const serde = todo.metadata.fields[updatedAt].serde;
2030
2164
  orm.data[updatedAt] = serde.mapFromNow(now);
2031
2165
  rm.queueDownstreamReactables(e, updatedAt);
@@ -2062,17 +2196,13 @@ function findConcreteMeta(maybeBaseMeta, row) {
2062
2196
  throw new Error(`${maybeBaseMeta.type} ${(0, index_1.tagId)(maybeBaseMeta, row.id)} must be instantiated via a subtype`);
2063
2197
  }
2064
2198
  // Look for the CTI __class from the driver telling us which subtype to instantiate
2065
- return maybeBaseMeta.subTypes.find((st) => st.type === row.__class) ?? maybeBaseMeta;
2199
+ return maybeBaseMeta.subTypesByType.get(row.__class) ?? maybeBaseMeta;
2066
2200
  }
2067
2201
  else if (maybeBaseMeta.inheritanceType === "sti") {
2068
2202
  // Look for the STI discriminator value
2069
2203
  const baseMeta = (0, index_1.getBaseMeta)(maybeBaseMeta);
2070
- const field = baseMeta.fields[baseMeta.stiDiscriminatorField];
2071
- if (field.kind !== "enum")
2072
- throw new Error("Discriminator field must be an enum");
2073
- const columnName = field.serde.columns[0].columnName;
2074
- const value = row[columnName];
2075
- return baseMeta.subTypes.find((st) => st.stiDiscriminatorValue === value) ?? baseMeta;
2204
+ const value = row[baseMeta.stiDiscriminatorColumnName];
2205
+ return baseMeta.subTypesByStiValue.get(value) ?? baseMeta;
2076
2206
  }
2077
2207
  else {
2078
2208
  throw new Error("Unknown inheritance type");
@@ -2081,7 +2211,7 @@ function findConcreteMeta(maybeBaseMeta, row) {
2081
2211
  /** Sets the `Animal.type` enum to the right subtype value. */
2082
2212
  function setStiDiscriminatorValue(baseMeta, entity) {
2083
2213
  const typeName = entity.constructor.name;
2084
- const st = baseMeta.subTypes.find((st) => st.type === typeName);
2214
+ const st = baseMeta.subTypesByType.get(typeName);
2085
2215
  if (st) {
2086
2216
  const field = baseMeta.fields[baseMeta.stiDiscriminatorField];
2087
2217
  const code = field.enumDetailType.findById(st.stiDiscriminatorValue).code;
@@ -2091,19 +2221,25 @@ function setStiDiscriminatorValue(baseMeta, entity) {
2091
2221
  entity[baseMeta.stiDiscriminatorField] = undefined;
2092
2222
  }
2093
2223
  }
2094
- function findPendingFlushEntities(entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks) {
2095
- for (const e of entities) {
2096
- if ((0, BaseEntity_1.getInstanceData)(e).pendingOperation !== "none") {
2097
- if (!hooksInvoked.has(e)) {
2098
- pendingHooks.add(e);
2099
- }
2100
- else {
2101
- alreadyRanHooks.add(e);
2102
- }
2103
- pendingFlush.add(e);
2224
+ function findPendingFlushEntities(maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks) {
2225
+ for (const e of maybePendingFlushEntities) {
2226
+ if ((0, BaseEntity_1.getInstanceData)(e).pendingOperation === "none") {
2227
+ maybePendingFlushEntities.delete(e);
2228
+ continue;
2104
2229
  }
2230
+ if (!hooksInvoked.has(e)) {
2231
+ pendingHooks.add(e);
2232
+ }
2233
+ else {
2234
+ alreadyRanHooks.add(e);
2235
+ }
2236
+ pendingFlush.add(e);
2105
2237
  }
2106
2238
  }
2239
+ /** Returns true if the caller explicitly asked `find` to use SQL pagination. */
2240
+ function hasPaginationSettings(options) {
2241
+ return "limit" in options || "offset" in options;
2242
+ }
2107
2243
  /** An error we throw to get knex to `ROLLBACK`, but then catch. */
2108
2244
  class InMemoryRollbackError extends Error {
2109
2245
  }