Skip to main content

common/database/
hashmap_store.rs

1// Generated by Qleany v2.0.0 from hashmap_store.tera
2
3//! HashMap-based storage backend with O(1) snapshots.
4//!
5//! Uses `im::HashMap` (persistent/immutable data structure with structural sharing)
6//! so that cloning the store for savepoints and undo is O(1).
7//! The store uses `RwLock` for interior mutability and thread safety,
8//! allowing `&HashMapStore` to be shared across threads via `Arc`.
9
10use crate::entities::*;
11use crate::snapshot::{StoreSnapshot, StoreSnapshotTrait};
12use crate::types::EntityId;
13use im::HashMap;
14use std::sync::RwLock;
15
16// ─────────────────────────────────────────────────────────────────────────────
17// The Store
18// ─────────────────────────────────────────────────────────────────────────────
19
20#[derive(Debug)]
21pub struct HashMapStore {
22    // ── Entity tables ──────────────────────────────────────────────────
23    pub roots: RwLock<HashMap<EntityId, Root>>,
24    pub workspaces: RwLock<HashMap<EntityId, Workspace>>,
25    pub systems: RwLock<HashMap<EntityId, System>>,
26    pub entities: RwLock<HashMap<EntityId, Entity>>,
27    pub fields: RwLock<HashMap<EntityId, Field>>,
28    pub features: RwLock<HashMap<EntityId, Feature>>,
29    pub files: RwLock<HashMap<EntityId, File>>,
30    pub use_cases: RwLock<HashMap<EntityId, UseCase>>,
31    pub dtos: RwLock<HashMap<EntityId, Dto>>,
32    pub dto_fields: RwLock<HashMap<EntityId, DtoField>>,
33    pub globals: RwLock<HashMap<EntityId, Global>>,
34    pub relationships: RwLock<HashMap<EntityId, Relationship>>,
35    pub user_interfaces: RwLock<HashMap<EntityId, UserInterface>>,
36
37    // ── Junction tables (one per forward relationship, shared for backward cleanup) ─
38    pub jn_system_from_root_system: RwLock<HashMapJunction>,
39    pub jn_workspace_from_root_workspace: RwLock<HashMapJunction>,
40    pub jn_entity_from_workspace_entities: RwLock<HashMapJunction>,
41    pub jn_feature_from_workspace_features: RwLock<HashMapJunction>,
42    pub jn_global_from_workspace_global: RwLock<HashMapJunction>,
43    pub jn_user_interface_from_workspace_user_interface: RwLock<HashMapJunction>,
44    pub jn_file_from_system_files: RwLock<HashMapJunction>,
45    pub jn_field_from_entity_fields: RwLock<HashMapJunction>,
46    pub jn_entity_from_entity_inherits_from: RwLock<HashMapJunction>,
47    pub jn_relationship_from_entity_relationships: RwLock<HashMapJunction>,
48    pub jn_entity_from_field_entity: RwLock<HashMapJunction>,
49    pub jn_use_case_from_feature_use_cases: RwLock<HashMapJunction>,
50    pub jn_entity_from_file_entity: RwLock<HashMapJunction>,
51    pub jn_feature_from_file_feature: RwLock<HashMapJunction>,
52    pub jn_field_from_file_field: RwLock<HashMapJunction>,
53    pub jn_use_case_from_file_use_case: RwLock<HashMapJunction>,
54    pub jn_dto_from_use_case_dto_in: RwLock<HashMapJunction>,
55    pub jn_dto_from_use_case_dto_out: RwLock<HashMapJunction>,
56    pub jn_entity_from_use_case_entities: RwLock<HashMapJunction>,
57    pub jn_dto_field_from_dto_fields: RwLock<HashMapJunction>,
58    pub jn_entity_from_relationship_left_entity: RwLock<HashMapJunction>,
59    pub jn_entity_from_relationship_right_entity: RwLock<HashMapJunction>,
60
61    // ── ID counters (one per entity type) ──────────────────────────
62    pub counters: RwLock<std::collections::HashMap<String, EntityId>>,
63
64    // ── Savepoints ──────────────────────────────────────────────────
65    savepoints: RwLock<std::collections::HashMap<u64, HashMapStoreSnapshot>>,
66    next_savepoint_id: RwLock<u64>,
67}
68impl Default for HashMapStore {
69    fn default() -> Self {
70        Self {
71            roots: RwLock::default(),
72            workspaces: RwLock::default(),
73            systems: RwLock::default(),
74            entities: RwLock::default(),
75            fields: RwLock::default(),
76            features: RwLock::default(),
77            files: RwLock::default(),
78            use_cases: RwLock::default(),
79            dtos: RwLock::default(),
80            dto_fields: RwLock::default(),
81            globals: RwLock::default(),
82            relationships: RwLock::default(),
83            user_interfaces: RwLock::default(),
84            jn_system_from_root_system: RwLock::new(HashMapJunction::exclusive()),
85            jn_workspace_from_root_workspace: RwLock::new(HashMapJunction::exclusive()),
86            jn_entity_from_workspace_entities: RwLock::new(HashMapJunction::exclusive()),
87            jn_feature_from_workspace_features: RwLock::new(HashMapJunction::exclusive()),
88            jn_global_from_workspace_global: RwLock::new(HashMapJunction::exclusive()),
89            jn_user_interface_from_workspace_user_interface: RwLock::new(
90                HashMapJunction::exclusive(),
91            ),
92            jn_file_from_system_files: RwLock::new(HashMapJunction::exclusive()),
93            jn_field_from_entity_fields: RwLock::new(HashMapJunction::exclusive()),
94            jn_entity_from_entity_inherits_from: RwLock::new(HashMapJunction::shared()),
95            jn_relationship_from_entity_relationships: RwLock::new(HashMapJunction::exclusive()),
96            jn_entity_from_field_entity: RwLock::new(HashMapJunction::shared()),
97            jn_use_case_from_feature_use_cases: RwLock::new(HashMapJunction::exclusive()),
98            jn_entity_from_file_entity: RwLock::new(HashMapJunction::shared()),
99            jn_feature_from_file_feature: RwLock::new(HashMapJunction::shared()),
100            jn_field_from_file_field: RwLock::new(HashMapJunction::shared()),
101            jn_use_case_from_file_use_case: RwLock::new(HashMapJunction::shared()),
102            jn_dto_from_use_case_dto_in: RwLock::new(HashMapJunction::exclusive()),
103            jn_dto_from_use_case_dto_out: RwLock::new(HashMapJunction::exclusive()),
104            jn_entity_from_use_case_entities: RwLock::new(HashMapJunction::shared()),
105            jn_dto_field_from_dto_fields: RwLock::new(HashMapJunction::exclusive()),
106            jn_entity_from_relationship_left_entity: RwLock::new(HashMapJunction::shared()),
107            jn_entity_from_relationship_right_entity: RwLock::new(HashMapJunction::shared()),
108            counters: RwLock::default(),
109            savepoints: RwLock::default(),
110            next_savepoint_id: RwLock::default(),
111        }
112    }
113}
114
115impl HashMapStore {
116    pub fn new() -> Self {
117        Self::default()
118    }
119
120    /// Clone the entire store for savepoint support. O(1) thanks to im::HashMap.
121    /// (Poison-tolerant, but NOT atomic across tables — used for savepoints, which
122    /// are taken on the single writer thread with no concurrent writer; use
123    /// [`Self::freeze`] when an atomic cross-table snapshot is required.)
124    pub fn snapshot(&self) -> HashMapStoreSnapshot {
125        HashMapStoreSnapshot {
126            roots: read_or_recover(&self.roots).clone(),
127            workspaces: read_or_recover(&self.workspaces).clone(),
128            systems: read_or_recover(&self.systems).clone(),
129            entities: read_or_recover(&self.entities).clone(),
130            fields: read_or_recover(&self.fields).clone(),
131            features: read_or_recover(&self.features).clone(),
132            files: read_or_recover(&self.files).clone(),
133            use_cases: read_or_recover(&self.use_cases).clone(),
134            dtos: read_or_recover(&self.dtos).clone(),
135            dto_fields: read_or_recover(&self.dto_fields).clone(),
136            globals: read_or_recover(&self.globals).clone(),
137            relationships: read_or_recover(&self.relationships).clone(),
138            user_interfaces: read_or_recover(&self.user_interfaces).clone(),
139            jn_system_from_root_system: read_or_recover(&self.jn_system_from_root_system).clone(),
140            jn_workspace_from_root_workspace: read_or_recover(
141                &self.jn_workspace_from_root_workspace,
142            )
143            .clone(),
144            jn_entity_from_workspace_entities: read_or_recover(
145                &self.jn_entity_from_workspace_entities,
146            )
147            .clone(),
148            jn_feature_from_workspace_features: read_or_recover(
149                &self.jn_feature_from_workspace_features,
150            )
151            .clone(),
152            jn_global_from_workspace_global: read_or_recover(&self.jn_global_from_workspace_global)
153                .clone(),
154            jn_user_interface_from_workspace_user_interface: read_or_recover(
155                &self.jn_user_interface_from_workspace_user_interface,
156            )
157            .clone(),
158            jn_file_from_system_files: read_or_recover(&self.jn_file_from_system_files).clone(),
159            jn_field_from_entity_fields: read_or_recover(&self.jn_field_from_entity_fields).clone(),
160            jn_entity_from_entity_inherits_from: read_or_recover(
161                &self.jn_entity_from_entity_inherits_from,
162            )
163            .clone(),
164            jn_relationship_from_entity_relationships: read_or_recover(
165                &self.jn_relationship_from_entity_relationships,
166            )
167            .clone(),
168            jn_entity_from_field_entity: read_or_recover(&self.jn_entity_from_field_entity).clone(),
169            jn_use_case_from_feature_use_cases: read_or_recover(
170                &self.jn_use_case_from_feature_use_cases,
171            )
172            .clone(),
173            jn_entity_from_file_entity: read_or_recover(&self.jn_entity_from_file_entity).clone(),
174            jn_feature_from_file_feature: read_or_recover(&self.jn_feature_from_file_feature)
175                .clone(),
176            jn_field_from_file_field: read_or_recover(&self.jn_field_from_file_field).clone(),
177            jn_use_case_from_file_use_case: read_or_recover(&self.jn_use_case_from_file_use_case)
178                .clone(),
179            jn_dto_from_use_case_dto_in: read_or_recover(&self.jn_dto_from_use_case_dto_in).clone(),
180            jn_dto_from_use_case_dto_out: read_or_recover(&self.jn_dto_from_use_case_dto_out)
181                .clone(),
182            jn_entity_from_use_case_entities: read_or_recover(
183                &self.jn_entity_from_use_case_entities,
184            )
185            .clone(),
186            jn_dto_field_from_dto_fields: read_or_recover(&self.jn_dto_field_from_dto_fields)
187                .clone(),
188            jn_entity_from_relationship_left_entity: read_or_recover(
189                &self.jn_entity_from_relationship_left_entity,
190            )
191            .clone(),
192            jn_entity_from_relationship_right_entity: read_or_recover(
193                &self.jn_entity_from_relationship_right_entity,
194            )
195            .clone(),
196            counters: read_or_recover(&self.counters).clone(),
197        }
198    }
199
200    /// Atomically snapshot the whole store into a fresh, isolated `HashMapStore`.
201    ///
202    /// Unlike [`Self::snapshot`], this holds a read guard on **every** table, junction,
203    /// and the counters *simultaneously* while cloning, so the result is a single
204    /// consistent point-in-time view of the store *at the instant freeze runs*.
205    /// O(1) clones (im::HashMap structural sharing); the lock hold is just the
206    /// clones. The returned store carries no savepoints and is meant to back a
207    /// read-only (frozen) transaction.
208    ///
209    /// Deadlock-freedom depends on an **invariant** (unenforced by the type
210    /// system): guards are acquired in the struct's declared field order (all
211    /// entity tables, then all junctions, then counters), and no writer ever
212    /// holds two *entity-table* locks simultaneously — each writer takes at most
213    /// one entity table, then its junctions, in table-before-junction order (see
214    /// `remove_multi`). A future cascade that nested two entity-table locks would
215    /// violate this and could deadlock against freeze; keep cascades single-table.
216    ///
217    /// SCOPE: freeze is atomic w.r.t. *itself*, not w.r.t. a writer's *multi-step*
218    /// cascade. A write transaction is not one critical section — its per-repo
219    /// calls each lock/unlock independently — so a cascading delete/create can
220    /// leave a brief window (e.g. child rows removed but the parent's junction not
221    /// yet cleared) into which freeze can land and capture a cross-table-torn
222    /// snapshot (`gather` then hits "entity vanished mid-read", or silently omits
223    /// a just-added-but-unlinked row). This shrinks the torn-read window from the
224    /// whole read to a single instant, but does not eliminate it; a store-wide
225    /// writer/reader gate would be required to fully close it.
226    pub fn freeze(&self) -> HashMapStore {
227        // Acquire ALL read guards first (declared order), then clone under that
228        // one consistent lock set.
229        let g_roots = read_or_recover(&self.roots);
230        let g_workspaces = read_or_recover(&self.workspaces);
231        let g_systems = read_or_recover(&self.systems);
232        let g_entities = read_or_recover(&self.entities);
233        let g_fields = read_or_recover(&self.fields);
234        let g_features = read_or_recover(&self.features);
235        let g_files = read_or_recover(&self.files);
236        let g_use_cases = read_or_recover(&self.use_cases);
237        let g_dtos = read_or_recover(&self.dtos);
238        let g_dto_fields = read_or_recover(&self.dto_fields);
239        let g_globals = read_or_recover(&self.globals);
240        let g_relationships = read_or_recover(&self.relationships);
241        let g_user_interfaces = read_or_recover(&self.user_interfaces);
242        let g_jn_system_from_root_system = read_or_recover(&self.jn_system_from_root_system);
243        let g_jn_workspace_from_root_workspace =
244            read_or_recover(&self.jn_workspace_from_root_workspace);
245        let g_jn_entity_from_workspace_entities =
246            read_or_recover(&self.jn_entity_from_workspace_entities);
247        let g_jn_feature_from_workspace_features =
248            read_or_recover(&self.jn_feature_from_workspace_features);
249        let g_jn_global_from_workspace_global =
250            read_or_recover(&self.jn_global_from_workspace_global);
251        let g_jn_user_interface_from_workspace_user_interface =
252            read_or_recover(&self.jn_user_interface_from_workspace_user_interface);
253        let g_jn_file_from_system_files = read_or_recover(&self.jn_file_from_system_files);
254        let g_jn_field_from_entity_fields = read_or_recover(&self.jn_field_from_entity_fields);
255        let g_jn_entity_from_entity_inherits_from =
256            read_or_recover(&self.jn_entity_from_entity_inherits_from);
257        let g_jn_relationship_from_entity_relationships =
258            read_or_recover(&self.jn_relationship_from_entity_relationships);
259        let g_jn_entity_from_field_entity = read_or_recover(&self.jn_entity_from_field_entity);
260        let g_jn_use_case_from_feature_use_cases =
261            read_or_recover(&self.jn_use_case_from_feature_use_cases);
262        let g_jn_entity_from_file_entity = read_or_recover(&self.jn_entity_from_file_entity);
263        let g_jn_feature_from_file_feature = read_or_recover(&self.jn_feature_from_file_feature);
264        let g_jn_field_from_file_field = read_or_recover(&self.jn_field_from_file_field);
265        let g_jn_use_case_from_file_use_case =
266            read_or_recover(&self.jn_use_case_from_file_use_case);
267        let g_jn_dto_from_use_case_dto_in = read_or_recover(&self.jn_dto_from_use_case_dto_in);
268        let g_jn_dto_from_use_case_dto_out = read_or_recover(&self.jn_dto_from_use_case_dto_out);
269        let g_jn_entity_from_use_case_entities =
270            read_or_recover(&self.jn_entity_from_use_case_entities);
271        let g_jn_dto_field_from_dto_fields = read_or_recover(&self.jn_dto_field_from_dto_fields);
272        let g_jn_entity_from_relationship_left_entity =
273            read_or_recover(&self.jn_entity_from_relationship_left_entity);
274        let g_jn_entity_from_relationship_right_entity =
275            read_or_recover(&self.jn_entity_from_relationship_right_entity);
276        let g_counters = read_or_recover(&self.counters);
277
278        HashMapStore {
279            roots: RwLock::new(g_roots.clone()),
280            workspaces: RwLock::new(g_workspaces.clone()),
281            systems: RwLock::new(g_systems.clone()),
282            entities: RwLock::new(g_entities.clone()),
283            fields: RwLock::new(g_fields.clone()),
284            features: RwLock::new(g_features.clone()),
285            files: RwLock::new(g_files.clone()),
286            use_cases: RwLock::new(g_use_cases.clone()),
287            dtos: RwLock::new(g_dtos.clone()),
288            dto_fields: RwLock::new(g_dto_fields.clone()),
289            globals: RwLock::new(g_globals.clone()),
290            relationships: RwLock::new(g_relationships.clone()),
291            user_interfaces: RwLock::new(g_user_interfaces.clone()),
292            jn_system_from_root_system: RwLock::new(g_jn_system_from_root_system.clone()),
293            jn_workspace_from_root_workspace: RwLock::new(
294                g_jn_workspace_from_root_workspace.clone(),
295            ),
296            jn_entity_from_workspace_entities: RwLock::new(
297                g_jn_entity_from_workspace_entities.clone(),
298            ),
299            jn_feature_from_workspace_features: RwLock::new(
300                g_jn_feature_from_workspace_features.clone(),
301            ),
302            jn_global_from_workspace_global: RwLock::new(g_jn_global_from_workspace_global.clone()),
303            jn_user_interface_from_workspace_user_interface: RwLock::new(
304                g_jn_user_interface_from_workspace_user_interface.clone(),
305            ),
306            jn_file_from_system_files: RwLock::new(g_jn_file_from_system_files.clone()),
307            jn_field_from_entity_fields: RwLock::new(g_jn_field_from_entity_fields.clone()),
308            jn_entity_from_entity_inherits_from: RwLock::new(
309                g_jn_entity_from_entity_inherits_from.clone(),
310            ),
311            jn_relationship_from_entity_relationships: RwLock::new(
312                g_jn_relationship_from_entity_relationships.clone(),
313            ),
314            jn_entity_from_field_entity: RwLock::new(g_jn_entity_from_field_entity.clone()),
315            jn_use_case_from_feature_use_cases: RwLock::new(
316                g_jn_use_case_from_feature_use_cases.clone(),
317            ),
318            jn_entity_from_file_entity: RwLock::new(g_jn_entity_from_file_entity.clone()),
319            jn_feature_from_file_feature: RwLock::new(g_jn_feature_from_file_feature.clone()),
320            jn_field_from_file_field: RwLock::new(g_jn_field_from_file_field.clone()),
321            jn_use_case_from_file_use_case: RwLock::new(g_jn_use_case_from_file_use_case.clone()),
322            jn_dto_from_use_case_dto_in: RwLock::new(g_jn_dto_from_use_case_dto_in.clone()),
323            jn_dto_from_use_case_dto_out: RwLock::new(g_jn_dto_from_use_case_dto_out.clone()),
324            jn_entity_from_use_case_entities: RwLock::new(
325                g_jn_entity_from_use_case_entities.clone(),
326            ),
327            jn_dto_field_from_dto_fields: RwLock::new(g_jn_dto_field_from_dto_fields.clone()),
328            jn_entity_from_relationship_left_entity: RwLock::new(
329                g_jn_entity_from_relationship_left_entity.clone(),
330            ),
331            jn_entity_from_relationship_right_entity: RwLock::new(
332                g_jn_entity_from_relationship_right_entity.clone(),
333            ),
334            counters: RwLock::new(g_counters.clone()),
335            savepoints: RwLock::new(std::collections::HashMap::new()),
336            next_savepoint_id: RwLock::new(0),
337        }
338    }
339
340    /// Restore from a savepoint snapshot.
341    pub fn restore(&self, snap: &HashMapStoreSnapshot) {
342        *write_or_recover(&self.roots) = snap.roots.clone();
343        *write_or_recover(&self.workspaces) = snap.workspaces.clone();
344        *write_or_recover(&self.systems) = snap.systems.clone();
345        *write_or_recover(&self.entities) = snap.entities.clone();
346        *write_or_recover(&self.fields) = snap.fields.clone();
347        *write_or_recover(&self.features) = snap.features.clone();
348        *write_or_recover(&self.files) = snap.files.clone();
349        *write_or_recover(&self.use_cases) = snap.use_cases.clone();
350        *write_or_recover(&self.dtos) = snap.dtos.clone();
351        *write_or_recover(&self.dto_fields) = snap.dto_fields.clone();
352        *write_or_recover(&self.globals) = snap.globals.clone();
353        *write_or_recover(&self.relationships) = snap.relationships.clone();
354        *write_or_recover(&self.user_interfaces) = snap.user_interfaces.clone();
355        *write_or_recover(&self.jn_system_from_root_system) =
356            snap.jn_system_from_root_system.clone();
357        *write_or_recover(&self.jn_workspace_from_root_workspace) =
358            snap.jn_workspace_from_root_workspace.clone();
359        *write_or_recover(&self.jn_entity_from_workspace_entities) =
360            snap.jn_entity_from_workspace_entities.clone();
361        *write_or_recover(&self.jn_feature_from_workspace_features) =
362            snap.jn_feature_from_workspace_features.clone();
363        *write_or_recover(&self.jn_global_from_workspace_global) =
364            snap.jn_global_from_workspace_global.clone();
365        *write_or_recover(&self.jn_user_interface_from_workspace_user_interface) =
366            snap.jn_user_interface_from_workspace_user_interface.clone();
367        *write_or_recover(&self.jn_file_from_system_files) = snap.jn_file_from_system_files.clone();
368        *write_or_recover(&self.jn_field_from_entity_fields) =
369            snap.jn_field_from_entity_fields.clone();
370        *write_or_recover(&self.jn_entity_from_entity_inherits_from) =
371            snap.jn_entity_from_entity_inherits_from.clone();
372        *write_or_recover(&self.jn_relationship_from_entity_relationships) =
373            snap.jn_relationship_from_entity_relationships.clone();
374        *write_or_recover(&self.jn_entity_from_field_entity) =
375            snap.jn_entity_from_field_entity.clone();
376        *write_or_recover(&self.jn_use_case_from_feature_use_cases) =
377            snap.jn_use_case_from_feature_use_cases.clone();
378        *write_or_recover(&self.jn_entity_from_file_entity) =
379            snap.jn_entity_from_file_entity.clone();
380        *write_or_recover(&self.jn_feature_from_file_feature) =
381            snap.jn_feature_from_file_feature.clone();
382        *write_or_recover(&self.jn_field_from_file_field) = snap.jn_field_from_file_field.clone();
383        *write_or_recover(&self.jn_use_case_from_file_use_case) =
384            snap.jn_use_case_from_file_use_case.clone();
385        *write_or_recover(&self.jn_dto_from_use_case_dto_in) =
386            snap.jn_dto_from_use_case_dto_in.clone();
387        *write_or_recover(&self.jn_dto_from_use_case_dto_out) =
388            snap.jn_dto_from_use_case_dto_out.clone();
389        *write_or_recover(&self.jn_entity_from_use_case_entities) =
390            snap.jn_entity_from_use_case_entities.clone();
391        *write_or_recover(&self.jn_dto_field_from_dto_fields) =
392            snap.jn_dto_field_from_dto_fields.clone();
393        *write_or_recover(&self.jn_entity_from_relationship_left_entity) =
394            snap.jn_entity_from_relationship_left_entity.clone();
395        *write_or_recover(&self.jn_entity_from_relationship_right_entity) =
396            snap.jn_entity_from_relationship_right_entity.clone();
397        *write_or_recover(&self.counters) = snap.counters.clone();
398    }
399
400    /// Create a savepoint by snapshotting the entire store. Returns a savepoint id.
401    /// O(1) thanks to im::HashMap structural sharing.
402    pub fn create_savepoint(&self) -> u64 {
403        let snap = self.snapshot();
404        let mut id_counter = write_or_recover(&self.next_savepoint_id);
405        let id = *id_counter;
406        *id_counter += 1;
407        write_or_recover(&self.savepoints).insert(id, snap);
408        id
409    }
410
411    /// Restore the store to a previously created savepoint.
412    pub fn restore_savepoint(&self, savepoint_id: u64) {
413        // A missing savepoint is a caller bug, but this is reached from
414        // `Transaction`'s **`Drop`-time rollback** — and panicking in a `Drop`
415        // that is itself running because of an earlier panic aborts the process
416        // outright, no matter what the panic strategy is. That double-panic is
417        // exactly what this module's poison recovery exists to avoid, so the
418        // rollback must not be the thing that reintroduces it.
419        //
420        // Loud in a debug build, a no-op in a shipped one: nothing to restore
421        // to means the store simply stays as it is.
422        let Some(snap) = read_or_recover(&self.savepoints)
423            .get(&savepoint_id)
424            .cloned()
425        else {
426            debug_assert!(
427                false,
428                "restore_savepoint: savepoint {savepoint_id} does not exist — it was already \
429                 restored, discarded, or never created"
430            );
431            return;
432        };
433        self.restore(&snap);
434    }
435
436    /// Discard a savepoint without restoring (used on successful commit, and
437    /// after a rollback so a rolled-back transaction doesn't leak its snapshot).
438    pub fn discard_savepoint(&self, savepoint_id: u64) {
439        write_or_recover(&self.savepoints).remove(&savepoint_id);
440    }
441
442    /// Get-and-increment counter for an entity type.
443    pub(crate) fn next_id(&self, entity_name: &str) -> EntityId {
444        let mut counters = write_or_recover(&self.counters);
445        let counter = counters.entry(entity_name.to_string()).or_insert(1);
446        let id = *counter;
447        *counter += 1;
448        id
449    }
450
451    /// Restore entity and junction data but preserve current counters.
452    /// Used for undo snapshots where IDs must remain monotonically increasing.
453    pub fn restore_without_counters(&self, snap: &HashMapStoreSnapshot) {
454        *write_or_recover(&self.roots) = snap.roots.clone();
455        *write_or_recover(&self.workspaces) = snap.workspaces.clone();
456        *write_or_recover(&self.systems) = snap.systems.clone();
457        *write_or_recover(&self.entities) = snap.entities.clone();
458        *write_or_recover(&self.fields) = snap.fields.clone();
459        *write_or_recover(&self.features) = snap.features.clone();
460        *write_or_recover(&self.files) = snap.files.clone();
461        *write_or_recover(&self.use_cases) = snap.use_cases.clone();
462        *write_or_recover(&self.dtos) = snap.dtos.clone();
463        *write_or_recover(&self.dto_fields) = snap.dto_fields.clone();
464        *write_or_recover(&self.globals) = snap.globals.clone();
465        *write_or_recover(&self.relationships) = snap.relationships.clone();
466        *write_or_recover(&self.user_interfaces) = snap.user_interfaces.clone();
467        *write_or_recover(&self.jn_system_from_root_system) =
468            snap.jn_system_from_root_system.clone();
469        *write_or_recover(&self.jn_workspace_from_root_workspace) =
470            snap.jn_workspace_from_root_workspace.clone();
471        *write_or_recover(&self.jn_entity_from_workspace_entities) =
472            snap.jn_entity_from_workspace_entities.clone();
473        *write_or_recover(&self.jn_feature_from_workspace_features) =
474            snap.jn_feature_from_workspace_features.clone();
475        *write_or_recover(&self.jn_global_from_workspace_global) =
476            snap.jn_global_from_workspace_global.clone();
477        *write_or_recover(&self.jn_user_interface_from_workspace_user_interface) =
478            snap.jn_user_interface_from_workspace_user_interface.clone();
479        *write_or_recover(&self.jn_file_from_system_files) = snap.jn_file_from_system_files.clone();
480        *write_or_recover(&self.jn_field_from_entity_fields) =
481            snap.jn_field_from_entity_fields.clone();
482        *write_or_recover(&self.jn_entity_from_entity_inherits_from) =
483            snap.jn_entity_from_entity_inherits_from.clone();
484        *write_or_recover(&self.jn_relationship_from_entity_relationships) =
485            snap.jn_relationship_from_entity_relationships.clone();
486        *write_or_recover(&self.jn_entity_from_field_entity) =
487            snap.jn_entity_from_field_entity.clone();
488        *write_or_recover(&self.jn_use_case_from_feature_use_cases) =
489            snap.jn_use_case_from_feature_use_cases.clone();
490        *write_or_recover(&self.jn_entity_from_file_entity) =
491            snap.jn_entity_from_file_entity.clone();
492        *write_or_recover(&self.jn_feature_from_file_feature) =
493            snap.jn_feature_from_file_feature.clone();
494        *write_or_recover(&self.jn_field_from_file_field) = snap.jn_field_from_file_field.clone();
495        *write_or_recover(&self.jn_use_case_from_file_use_case) =
496            snap.jn_use_case_from_file_use_case.clone();
497        *write_or_recover(&self.jn_dto_from_use_case_dto_in) =
498            snap.jn_dto_from_use_case_dto_in.clone();
499        *write_or_recover(&self.jn_dto_from_use_case_dto_out) =
500            snap.jn_dto_from_use_case_dto_out.clone();
501        *write_or_recover(&self.jn_entity_from_use_case_entities) =
502            snap.jn_entity_from_use_case_entities.clone();
503        *write_or_recover(&self.jn_dto_field_from_dto_fields) =
504            snap.jn_dto_field_from_dto_fields.clone();
505        *write_or_recover(&self.jn_entity_from_relationship_left_entity) =
506            snap.jn_entity_from_relationship_left_entity.clone();
507        *write_or_recover(&self.jn_entity_from_relationship_right_entity) =
508            snap.jn_entity_from_relationship_right_entity.clone();
509        // counters intentionally NOT restored — IDs must remain monotonically increasing
510    }
511
512    /// Create a type-erased store snapshot for undo.
513    pub fn store_snapshot(&self) -> StoreSnapshot {
514        StoreSnapshot::new(self.snapshot())
515    }
516
517    /// Restore from a type-erased store snapshot (preserves counters).
518    pub fn restore_store_snapshot(&self, snap: &StoreSnapshot) {
519        let s = snap
520            .downcast_ref::<HashMapStoreSnapshot>()
521            .expect("StoreSnapshot must contain HashMapStoreSnapshot");
522        self.restore_without_counters(s);
523    }
524
525    /// Check every junction's reverse index against a rebuild from its rows,
526    /// and that each junction is exclusive exactly when its relationship is.
527    ///
528    /// A diagnostic for tests: it walks every junction, so it costs O(store).
529    /// Nothing calls it on a write path. Returns the first discrepancy found.
530    pub fn check_junction_indexes(&self) -> Result<(), String> {
531        {
532            let jn = read_or_recover(&self.jn_system_from_root_system);
533            if !jn.is_exclusive() {
534                return Err(
535                    "jn_system_from_root_system: exclusivity does not match the relationship"
536                        .into(),
537                );
538            }
539            jn.check_index()
540                .map_err(|e| format!("jn_system_from_root_system: {e}"))?;
541        }
542        {
543            let jn = read_or_recover(&self.jn_workspace_from_root_workspace);
544            if !jn.is_exclusive() {
545                return Err(
546                    "jn_workspace_from_root_workspace: exclusivity does not match the relationship"
547                        .into(),
548                );
549            }
550            jn.check_index()
551                .map_err(|e| format!("jn_workspace_from_root_workspace: {e}"))?;
552        }
553        {
554            let jn = read_or_recover(&self.jn_entity_from_workspace_entities);
555            if !jn.is_exclusive() {
556                return Err("jn_entity_from_workspace_entities: exclusivity does not match the relationship".into());
557            }
558            jn.check_index()
559                .map_err(|e| format!("jn_entity_from_workspace_entities: {e}"))?;
560        }
561        {
562            let jn = read_or_recover(&self.jn_feature_from_workspace_features);
563            if !jn.is_exclusive() {
564                return Err("jn_feature_from_workspace_features: exclusivity does not match the relationship".into());
565            }
566            jn.check_index()
567                .map_err(|e| format!("jn_feature_from_workspace_features: {e}"))?;
568        }
569        {
570            let jn = read_or_recover(&self.jn_global_from_workspace_global);
571            if !jn.is_exclusive() {
572                return Err(
573                    "jn_global_from_workspace_global: exclusivity does not match the relationship"
574                        .into(),
575                );
576            }
577            jn.check_index()
578                .map_err(|e| format!("jn_global_from_workspace_global: {e}"))?;
579        }
580        {
581            let jn = read_or_recover(&self.jn_user_interface_from_workspace_user_interface);
582            if !jn.is_exclusive() {
583                return Err("jn_user_interface_from_workspace_user_interface: exclusivity does not match the relationship".into());
584            }
585            jn.check_index()
586                .map_err(|e| format!("jn_user_interface_from_workspace_user_interface: {e}"))?;
587        }
588        {
589            let jn = read_or_recover(&self.jn_file_from_system_files);
590            if !jn.is_exclusive() {
591                return Err(
592                    "jn_file_from_system_files: exclusivity does not match the relationship".into(),
593                );
594            }
595            jn.check_index()
596                .map_err(|e| format!("jn_file_from_system_files: {e}"))?;
597        }
598        {
599            let jn = read_or_recover(&self.jn_field_from_entity_fields);
600            if !jn.is_exclusive() {
601                return Err(
602                    "jn_field_from_entity_fields: exclusivity does not match the relationship"
603                        .into(),
604                );
605            }
606            jn.check_index()
607                .map_err(|e| format!("jn_field_from_entity_fields: {e}"))?;
608        }
609        {
610            let jn = read_or_recover(&self.jn_entity_from_entity_inherits_from);
611            if jn.is_exclusive() {
612                return Err("jn_entity_from_entity_inherits_from: exclusivity does not match the relationship".into());
613            }
614            jn.check_index()
615                .map_err(|e| format!("jn_entity_from_entity_inherits_from: {e}"))?;
616        }
617        {
618            let jn = read_or_recover(&self.jn_relationship_from_entity_relationships);
619            if !jn.is_exclusive() {
620                return Err("jn_relationship_from_entity_relationships: exclusivity does not match the relationship".into());
621            }
622            jn.check_index()
623                .map_err(|e| format!("jn_relationship_from_entity_relationships: {e}"))?;
624        }
625        {
626            let jn = read_or_recover(&self.jn_entity_from_field_entity);
627            if jn.is_exclusive() {
628                return Err(
629                    "jn_entity_from_field_entity: exclusivity does not match the relationship"
630                        .into(),
631                );
632            }
633            jn.check_index()
634                .map_err(|e| format!("jn_entity_from_field_entity: {e}"))?;
635        }
636        {
637            let jn = read_or_recover(&self.jn_use_case_from_feature_use_cases);
638            if !jn.is_exclusive() {
639                return Err("jn_use_case_from_feature_use_cases: exclusivity does not match the relationship".into());
640            }
641            jn.check_index()
642                .map_err(|e| format!("jn_use_case_from_feature_use_cases: {e}"))?;
643        }
644        {
645            let jn = read_or_recover(&self.jn_entity_from_file_entity);
646            if jn.is_exclusive() {
647                return Err(
648                    "jn_entity_from_file_entity: exclusivity does not match the relationship"
649                        .into(),
650                );
651            }
652            jn.check_index()
653                .map_err(|e| format!("jn_entity_from_file_entity: {e}"))?;
654        }
655        {
656            let jn = read_or_recover(&self.jn_feature_from_file_feature);
657            if jn.is_exclusive() {
658                return Err(
659                    "jn_feature_from_file_feature: exclusivity does not match the relationship"
660                        .into(),
661                );
662            }
663            jn.check_index()
664                .map_err(|e| format!("jn_feature_from_file_feature: {e}"))?;
665        }
666        {
667            let jn = read_or_recover(&self.jn_field_from_file_field);
668            if jn.is_exclusive() {
669                return Err(
670                    "jn_field_from_file_field: exclusivity does not match the relationship".into(),
671                );
672            }
673            jn.check_index()
674                .map_err(|e| format!("jn_field_from_file_field: {e}"))?;
675        }
676        {
677            let jn = read_or_recover(&self.jn_use_case_from_file_use_case);
678            if jn.is_exclusive() {
679                return Err(
680                    "jn_use_case_from_file_use_case: exclusivity does not match the relationship"
681                        .into(),
682                );
683            }
684            jn.check_index()
685                .map_err(|e| format!("jn_use_case_from_file_use_case: {e}"))?;
686        }
687        {
688            let jn = read_or_recover(&self.jn_dto_from_use_case_dto_in);
689            if !jn.is_exclusive() {
690                return Err(
691                    "jn_dto_from_use_case_dto_in: exclusivity does not match the relationship"
692                        .into(),
693                );
694            }
695            jn.check_index()
696                .map_err(|e| format!("jn_dto_from_use_case_dto_in: {e}"))?;
697        }
698        {
699            let jn = read_or_recover(&self.jn_dto_from_use_case_dto_out);
700            if !jn.is_exclusive() {
701                return Err(
702                    "jn_dto_from_use_case_dto_out: exclusivity does not match the relationship"
703                        .into(),
704                );
705            }
706            jn.check_index()
707                .map_err(|e| format!("jn_dto_from_use_case_dto_out: {e}"))?;
708        }
709        {
710            let jn = read_or_recover(&self.jn_entity_from_use_case_entities);
711            if jn.is_exclusive() {
712                return Err(
713                    "jn_entity_from_use_case_entities: exclusivity does not match the relationship"
714                        .into(),
715                );
716            }
717            jn.check_index()
718                .map_err(|e| format!("jn_entity_from_use_case_entities: {e}"))?;
719        }
720        {
721            let jn = read_or_recover(&self.jn_dto_field_from_dto_fields);
722            if !jn.is_exclusive() {
723                return Err(
724                    "jn_dto_field_from_dto_fields: exclusivity does not match the relationship"
725                        .into(),
726                );
727            }
728            jn.check_index()
729                .map_err(|e| format!("jn_dto_field_from_dto_fields: {e}"))?;
730        }
731        {
732            let jn = read_or_recover(&self.jn_entity_from_relationship_left_entity);
733            if jn.is_exclusive() {
734                return Err("jn_entity_from_relationship_left_entity: exclusivity does not match the relationship".into());
735            }
736            jn.check_index()
737                .map_err(|e| format!("jn_entity_from_relationship_left_entity: {e}"))?;
738        }
739        {
740            let jn = read_or_recover(&self.jn_entity_from_relationship_right_entity);
741            if jn.is_exclusive() {
742                return Err("jn_entity_from_relationship_right_entity: exclusivity does not match the relationship".into());
743            }
744            jn.check_index()
745                .map_err(|e| format!("jn_entity_from_relationship_right_entity: {e}"))?;
746        }
747        Ok(())
748    }
749}
750
751/// Snapshot of the entire store. O(1) to create thanks to im::HashMap structural sharing.
752///
753/// Entity-table and junction fields are `pub(crate)` so the generated per-entity
754/// repositories (same crate) can read the captured rows/junctions during a *scoped*
755/// restore. Counters stay private (restore never touches them — ids stay monotonic).
756#[derive(Debug, Clone)]
757pub struct HashMapStoreSnapshot {
758    pub(crate) roots: HashMap<EntityId, Root>,
759    pub(crate) workspaces: HashMap<EntityId, Workspace>,
760    pub(crate) systems: HashMap<EntityId, System>,
761    pub(crate) entities: HashMap<EntityId, Entity>,
762    pub(crate) fields: HashMap<EntityId, Field>,
763    pub(crate) features: HashMap<EntityId, Feature>,
764    pub(crate) files: HashMap<EntityId, File>,
765    pub(crate) use_cases: HashMap<EntityId, UseCase>,
766    pub(crate) dtos: HashMap<EntityId, Dto>,
767    pub(crate) dto_fields: HashMap<EntityId, DtoField>,
768    pub(crate) globals: HashMap<EntityId, Global>,
769    pub(crate) relationships: HashMap<EntityId, Relationship>,
770    pub(crate) user_interfaces: HashMap<EntityId, UserInterface>,
771    pub(crate) jn_system_from_root_system: HashMapJunction,
772    pub(crate) jn_workspace_from_root_workspace: HashMapJunction,
773    pub(crate) jn_entity_from_workspace_entities: HashMapJunction,
774    pub(crate) jn_feature_from_workspace_features: HashMapJunction,
775    pub(crate) jn_global_from_workspace_global: HashMapJunction,
776    pub(crate) jn_user_interface_from_workspace_user_interface: HashMapJunction,
777    pub(crate) jn_file_from_system_files: HashMapJunction,
778    pub(crate) jn_field_from_entity_fields: HashMapJunction,
779    pub(crate) jn_entity_from_entity_inherits_from: HashMapJunction,
780    pub(crate) jn_relationship_from_entity_relationships: HashMapJunction,
781    pub(crate) jn_entity_from_field_entity: HashMapJunction,
782    pub(crate) jn_use_case_from_feature_use_cases: HashMapJunction,
783    pub(crate) jn_entity_from_file_entity: HashMapJunction,
784    pub(crate) jn_feature_from_file_feature: HashMapJunction,
785    pub(crate) jn_field_from_file_field: HashMapJunction,
786    pub(crate) jn_use_case_from_file_use_case: HashMapJunction,
787    pub(crate) jn_dto_from_use_case_dto_in: HashMapJunction,
788    pub(crate) jn_dto_from_use_case_dto_out: HashMapJunction,
789    pub(crate) jn_entity_from_use_case_entities: HashMapJunction,
790    pub(crate) jn_dto_field_from_dto_fields: HashMapJunction,
791    pub(crate) jn_entity_from_relationship_left_entity: HashMapJunction,
792    pub(crate) jn_entity_from_relationship_right_entity: HashMapJunction,
793    counters: std::collections::HashMap<String, EntityId>,
794}
795
796impl StoreSnapshotTrait for HashMapStoreSnapshot {
797    fn clone_box(&self) -> Box<dyn StoreSnapshotTrait> {
798        Box::new(self.clone())
799    }
800
801    fn as_any(&self) -> &dyn std::any::Any {
802        self
803    }
804}
805
806// ─────────────────────────────────────────────────────────────────────────────
807// Junctions
808// ─────────────────────────────────────────────────────────────────────────────
809
810/// One relationship's junction: each source id mapped to its ordered target ids.
811///
812/// The junction of an *exclusive* relationship (one-to-one, one-to-many,
813/// ordered one-to-many) also keeps a reverse index, target to the sources
814/// holding it, so that "this target already belongs to another source" costs
815/// one lookup per target of a write instead of a walk over every row of the
816/// relationship. Without it, creating N rows that each own a child is O(N²).
817///
818/// The rows are private, so nothing can change them behind the index: every
819/// mutation goes through [`Self::insert`], [`Self::remove`] or
820/// [`Self::remove_target`], which update both in the same call, and a
821/// whole-junction clone (snapshot, savepoint, [`HashMapStore::freeze`],
822/// restore) carries both together. Both are `im::HashMap`s, so that clone
823/// stays O(1).
824///
825/// The read methods mirror the map it wraps (`get`, `iter`, `contains_key`, …),
826/// so code that only reads a junction reads it as before.
827#[derive(Debug, Clone, Default)]
828pub struct HashMapJunction {
829    rows: HashMap<EntityId, Vec<EntityId>>,
830    /// `None` for a shared relationship (many-to-one, many-to-many), which has
831    /// no ownership rule to check. For an exclusive one, every source whose row
832    /// holds the target, once per occurrence. A consistent store has exactly
833    /// one; keeping a list makes the index exact even when a write that is not
834    /// validated (a scoped undo restoring rows from its snapshot) leaves one
835    /// target in two rows, so every check still answers exactly as a walk over
836    /// the rows would.
837    owners: Option<HashMap<EntityId, Vec<EntityId>>>,
838}
839
840impl HashMapJunction {
841    /// An empty junction for an exclusive relationship, with its reverse index.
842    pub fn exclusive() -> Self {
843        Self {
844            rows: HashMap::new(),
845            owners: Some(HashMap::new()),
846        }
847    }
848
849    /// An empty junction for a shared relationship, without a reverse index.
850    pub fn shared() -> Self {
851        Self::default()
852    }
853
854    /// Whether this junction belongs to an exclusive relationship.
855    pub fn is_exclusive(&self) -> bool {
856        self.owners.is_some()
857    }
858
859    /// The ordered targets of `source`, if it has a row.
860    pub fn get(&self, source: &EntityId) -> Option<&Vec<EntityId>> {
861        self.rows.get(source)
862    }
863
864    /// Whether `source` has a row (possibly empty).
865    pub fn contains_key(&self, source: &EntityId) -> bool {
866        self.rows.contains_key(source)
867    }
868
869    /// Every row, as `(source, targets)`, in no particular order.
870    pub fn iter(&self) -> impl Iterator<Item = (&EntityId, &Vec<EntityId>)> {
871        self.rows.iter()
872    }
873
874    /// Every source that has a row, in no particular order.
875    pub fn keys(&self) -> impl Iterator<Item = &EntityId> {
876        self.rows.keys()
877    }
878
879    /// Every row's targets, in no particular order.
880    pub fn values(&self) -> impl Iterator<Item = &Vec<EntityId>> {
881        self.rows.values()
882    }
883
884    /// The number of rows.
885    pub fn len(&self) -> usize {
886        self.rows.len()
887    }
888
889    /// Whether the junction has no row at all.
890    pub fn is_empty(&self) -> bool {
891        self.rows.is_empty()
892    }
893
894    /// Every source whose row holds `target`, once per occurrence: none or one
895    /// in a consistent exclusive junction. One lookup on an exclusive junction;
896    /// a walk over every row on a shared one, which has no index.
897    pub fn sources_of(&self, target: &EntityId) -> Vec<EntityId> {
898        match &self.owners {
899            Some(owners) => owners.get(target).cloned().unwrap_or_default(),
900            None => self
901                .rows
902                .iter()
903                .flat_map(|(source, targets)| {
904                    targets
905                        .iter()
906                        .filter(|t| *t == target)
907                        .map(move |_| *source)
908                })
909                .collect(),
910        }
911    }
912
913    /// Replace the row of `source` with `targets`, returning the previous row.
914    pub fn insert(&mut self, source: EntityId, targets: Vec<EntityId>) -> Option<Vec<EntityId>> {
915        if let Some(owners) = self.owners.as_mut() {
916            let previous = self
917                .rows
918                .get(&source)
919                .map(Vec::as_slice)
920                .unwrap_or_default();
921            reindex(owners, source, previous, &targets);
922        }
923        self.rows.insert(source, targets)
924    }
925
926    /// Remove the row of `source`, returning it.
927    pub fn remove(&mut self, source: &EntityId) -> Option<Vec<EntityId>> {
928        let removed = self.rows.remove(source)?;
929        if let Some(owners) = self.owners.as_mut() {
930            reindex(owners, *source, &removed, &[]);
931        }
932        Some(removed)
933    }
934
935    /// Remove `target` from every row holding it, as when the target entity is
936    /// deleted. Visits only those rows on an exclusive junction, every row on a
937    /// shared one.
938    pub fn remove_target(&mut self, target: &EntityId) {
939        let sources: Vec<EntityId> = match self.owners.as_mut() {
940            Some(owners) => {
941                let mut sources = owners.remove(target).unwrap_or_default();
942                sources.sort_unstable();
943                sources.dedup();
944                sources
945            }
946            None => self
947                .rows
948                .iter()
949                .filter(|(_, targets)| targets.contains(target))
950                .map(|(source, _)| *source)
951                .collect(),
952        };
953        for source in sources {
954            if let Some(targets) = self.rows.get_mut(&source) {
955                targets.retain(|t| t != target);
956            }
957        }
958    }
959
960    /// Compare the reverse index with one rebuilt from the rows.
961    ///
962    /// A diagnostic for tests, O(junction): see
963    /// [`HashMapStore::check_junction_indexes`]. A shared junction has no index
964    /// and always passes.
965    pub fn check_index(&self) -> Result<(), String> {
966        let Some(owners) = &self.owners else {
967            return Ok(());
968        };
969        let mut expected: std::collections::HashMap<EntityId, Vec<EntityId>> =
970            std::collections::HashMap::new();
971        for (source, targets) in self.rows.iter() {
972            for target in targets {
973                expected.entry(*target).or_default().push(*source);
974            }
975        }
976        if expected.len() != owners.len() {
977            return Err(format!(
978                "the reverse index lists {} targets but the rows hold {}",
979                owners.len(),
980                expected.len()
981            ));
982        }
983        for (target, mut holders) in expected {
984            let mut indexed = owners.get(&target).cloned().unwrap_or_default();
985            holders.sort_unstable();
986            indexed.sort_unstable();
987            if holders != indexed {
988                return Err(format!(
989                    "target {target} is held by {holders:?} but indexed under {indexed:?}"
990                ));
991            }
992        }
993        Ok(())
994    }
995}
996
997/// Move `source`'s entries in a reverse index from its `previous` targets to
998/// its `next` ones. Only the difference is written, counted per occurrence, so
999/// re-saving a long ordered list with one target appended (how a child is
1000/// added to its owner) updates one index entry, not one per child.
1001fn reindex(
1002    owners: &mut HashMap<EntityId, Vec<EntityId>>,
1003    source: EntityId,
1004    previous: &[EntityId],
1005    next: &[EntityId],
1006) {
1007    // A new row and a removed one need no difference: the common cases of
1008    // creating and deleting a source skip the scratch map below.
1009    if previous.is_empty() {
1010        for target in next {
1011            link(owners, source, *target, 1);
1012        }
1013        return;
1014    }
1015    if next.is_empty() {
1016        for target in previous {
1017            unlink(owners, source, *target, 1);
1018        }
1019        return;
1020    }
1021    if previous == next {
1022        return;
1023    }
1024    let mut delta: std::collections::HashMap<EntityId, isize> =
1025        std::collections::HashMap::with_capacity(previous.len() + next.len());
1026    for target in previous {
1027        *delta.entry(*target).or_default() -= 1;
1028    }
1029    for target in next {
1030        *delta.entry(*target).or_default() += 1;
1031    }
1032    for (target, change) in delta {
1033        if change > 0 {
1034            link(owners, source, target, change.unsigned_abs());
1035        } else if change < 0 {
1036            unlink(owners, source, target, change.unsigned_abs());
1037        }
1038    }
1039}
1040
1041/// Record `count` more occurrences of `target` in the row of `source`.
1042fn link(
1043    owners: &mut HashMap<EntityId, Vec<EntityId>>,
1044    source: EntityId,
1045    target: EntityId,
1046    count: usize,
1047) {
1048    owners
1049        .entry(target)
1050        .or_default()
1051        .extend(std::iter::repeat_n(source, count));
1052}
1053
1054/// Forget `count` occurrences of `target` in the row of `source`, dropping the
1055/// index entry once no row holds the target any more.
1056fn unlink(
1057    owners: &mut HashMap<EntityId, Vec<EntityId>>,
1058    source: EntityId,
1059    target: EntityId,
1060    count: usize,
1061) {
1062    let emptied = match owners.get_mut(&target) {
1063        Some(holders) => {
1064            for _ in 0..count {
1065                if let Some(at) = holders.iter().position(|h| *h == source) {
1066                    holders.swap_remove(at);
1067                }
1068            }
1069            holders.is_empty()
1070        }
1071        None => false,
1072    };
1073    if emptied {
1074        owners.remove(&target);
1075    }
1076}
1077
1078// ─────────────────────────────────────────────────────────────────────────────
1079// Helper functions
1080// ─────────────────────────────────────────────────────────────────────────────
1081
1082/// Acquire a read guard, recovering the inner value if the lock was poisoned by
1083/// a panic elsewhere — and **clearing** the poison so recovery is permanent, not
1084/// one-shot (a recovered-but-still-poisoned lock would re-panic on the next plain
1085/// `.read()/.write().unwrap()`). Used throughout the store's own snapshot /
1086/// savepoint / restore paths so a single table poisoned by an unrelated panic
1087/// can't escalate a rollback (the Drop-time `restore_savepoint`) — or the very
1088/// next write transaction's `create_savepoint` — into a process-wide abort.
1089pub(crate) fn read_or_recover<T>(lock: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
1090    match lock.read() {
1091        Ok(guard) => guard,
1092        Err(poisoned) => {
1093            let guard = poisoned.into_inner();
1094            lock.clear_poison();
1095            guard
1096        }
1097    }
1098}
1099
1100/// Write-guard counterpart of [`read_or_recover`] (also clears the poison).
1101pub(crate) fn write_or_recover<T>(lock: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
1102    match lock.write() {
1103        Ok(guard) => guard,
1104        Err(poisoned) => {
1105            let guard = poisoned.into_inner();
1106            lock.clear_poison();
1107            guard
1108        }
1109    }
1110}
1111
1112/// Remove `id` from every row of a junction that points at it (the entity `id`
1113/// is being deleted). See [`HashMapJunction::remove_target`].
1114pub(crate) fn delete_from_backward_junction(junction: &RwLock<HashMapJunction>, id: &EntityId) {
1115    write_or_recover(junction).remove_target(id);
1116}
1117
1118pub(crate) fn junction_get(junction: &RwLock<HashMapJunction>, id: &EntityId) -> Vec<EntityId> {
1119    read_or_recover(junction)
1120        .get(id)
1121        .cloned()
1122        .unwrap_or_default()
1123}
1124
1125pub(crate) fn junction_set(junction: &RwLock<HashMapJunction>, id: EntityId, ids: Vec<EntityId>) {
1126    write_or_recover(junction).insert(id, ids);
1127}
1128
1129/// Validate a complete replacement batch before changing any junction rows.
1130/// Ownership exclusivity is per relationship, independently of cascade strength,
1131/// and belongs to the junction itself (see [`HashMapJunction::exclusive`]).
1132///
1133/// The cost follows the batch, never the junction: an exclusive target is
1134/// looked up in the reverse index, so a batch of k targets costs O(k) however
1135/// many rows the relationship holds, and a batch carrying no target at all
1136/// does not even take the junction's lock.
1137pub(crate) fn validate_junction_updates(
1138    junction: &RwLock<HashMapJunction>,
1139    replacements: &[(EntityId, Vec<EntityId>)],
1140    scalar: bool,
1141) -> Result<(), crate::error::RepositoryError> {
1142    use crate::error::RepositoryError::ConstraintViolation;
1143    use std::collections::HashSet;
1144    let mut owners = HashSet::new();
1145    for (owner, targets) in replacements {
1146        if !owners.insert(*owner) {
1147            return Err(ConstraintViolation(
1148                "duplicate source in relationship batch".into(),
1149            ));
1150        }
1151        if scalar && targets.len() > 1 {
1152            return Err(ConstraintViolation(
1153                "to-one relationship accepts at most one target".into(),
1154            ));
1155        }
1156        let mut unique = HashSet::new();
1157        if targets.iter().any(|id| !unique.insert(*id)) {
1158            return Err(ConstraintViolation("duplicate relationship target".into()));
1159        }
1160    }
1161    if replacements.iter().all(|(_, targets)| targets.is_empty()) {
1162        return Ok(());
1163    }
1164    let current = read_or_recover(junction);
1165    let Some(index) = current.owners.as_ref() else {
1166        // A shared relationship: any number of sources may hold a target.
1167        return Ok(());
1168    };
1169    // A target is taken when a source outside this batch holds it, or when two
1170    // rows of the batch both claim it. Sources in the batch are being replaced,
1171    // so moving a target between two of them within one batch is valid.
1172    let mut claimed = HashSet::new();
1173    for (_, targets) in replacements {
1174        for id in targets {
1175            let held_outside = index
1176                .get(id)
1177                .is_some_and(|holders| holders.iter().any(|holder| !owners.contains(holder)));
1178            if held_outside || !claimed.insert(*id) {
1179                return Err(ConstraintViolation(
1180                    "relationship target already belongs to another source".into(),
1181                ));
1182            }
1183        }
1184    }
1185    Ok(())
1186}
1187
1188pub(crate) fn junction_remove(junction: &RwLock<HashMapJunction>, id: &EntityId) {
1189    write_or_recover(junction).remove(id);
1190}
1191
1192pub(crate) fn junction_get_relationships_from_right_ids(
1193    junction: &RwLock<HashMapJunction>,
1194    right_ids: &[EntityId],
1195) -> Vec<(EntityId, Vec<EntityId>)> {
1196    let jn = read_or_recover(junction);
1197    jn.iter()
1198        .filter(|(_, rids)| right_ids.iter().any(|eid| rids.contains(eid)))
1199        .map(|(left_id, rids)| (*left_id, rids.clone()))
1200        .collect()
1201}
1202
1203pub(crate) fn junction_move_ids(
1204    junction: &RwLock<HashMapJunction>,
1205    id: &EntityId,
1206    ids_to_move: &[EntityId],
1207    new_index: i32,
1208) -> Vec<EntityId> {
1209    let current = junction_get(junction, id);
1210    if ids_to_move.is_empty() {
1211        return current;
1212    }
1213    let move_set: std::collections::HashSet<EntityId> = ids_to_move.iter().copied().collect();
1214    let mut remaining: Vec<EntityId> = current
1215        .into_iter()
1216        .filter(|eid| !move_set.contains(eid))
1217        .collect();
1218    let insert_pos = if new_index < 0 || (new_index as usize) > remaining.len() {
1219        remaining.len()
1220    } else {
1221        new_index as usize
1222    };
1223    for (i, &eid) in ids_to_move.iter().enumerate() {
1224        remaining.insert(insert_pos + i, eid);
1225    }
1226    junction_set(junction, *id, remaining.clone());
1227    remaining
1228}
1229
1230/// Surgically reconcile an external (out-of-scope) junction list during a scoped undo restore.
1231///
1232/// `live_list` is the current ordered value of an external referrer's junction row; `snap_list`
1233/// is the same row at snapshot time; `scope` is the set of subtree ids being restored. Only the
1234/// membership/position of `scope` ids is reconciled to match the snapshot — non-scope siblings are
1235/// preserved in their current relative order. Scope ids absent from the snapshot are removed
1236/// (undo-of-create); scope ids present in the snapshot are re-inserted at their snapshot-relative
1237/// position (undo-of-remove). This is what keeps undo from clobbering other trunks' edits.
1238pub(crate) fn reconcile_backref_list(
1239    live_list: &[EntityId],
1240    snap_list: &[EntityId],
1241    scope: &std::collections::HashSet<EntityId>,
1242) -> Vec<EntityId> {
1243    // Scope ids that should be present after restore, in snapshot order.
1244    let snap_members: Vec<EntityId> = snap_list
1245        .iter()
1246        .copied()
1247        .filter(|x| scope.contains(x))
1248        .collect();
1249    let snap_member_set: std::collections::HashSet<EntityId> =
1250        snap_members.iter().copied().collect();
1251
1252    // Start from the live list, dropping only scope ids that are NOT in the snapshot.
1253    let mut work: Vec<EntityId> = live_list
1254        .iter()
1255        .copied()
1256        // De Morgan'd from `!(in_scope && !in_snapshot)`: same rule, stated the
1257        // way clippy's `nonminimal_bool` wants it, and arguably plainer — keep
1258        // anything outside the scope, plus anything the snapshot still holds.
1259        .filter(|x| !scope.contains(x) || snap_member_set.contains(x))
1260        .collect();
1261
1262    // Re-insert missing scope ids at their snapshot-relative position (count of snapshot
1263    // predecessors that survive in `work`), so they return next to their original neighbours.
1264    for id in &snap_members {
1265        if work.contains(id) {
1266            continue;
1267        }
1268        let snap_idx = snap_list.iter().position(|x| x == id).unwrap_or(0);
1269        let pos = snap_list[..snap_idx]
1270            .iter()
1271            .filter(|x| work.contains(x))
1272            .count()
1273            .min(work.len());
1274        work.insert(pos, *id);
1275    }
1276    work
1277}
1278
1279// ═════════════════════════════════════════════════════════════════════════════
1280// Macros for entities WITH relationships (reduces boilerplate)
1281// ═════════════════════════════════════════════════════════════════════════════
1282
1283/// Generate standard relationship methods for a HashMap table implementation.
1284#[macro_export]
1285macro_rules! impl_relationship_methods {
1286    ($table_type:ty, $field_enum:ty) => {
1287        fn get_relationship(
1288            &self,
1289            id: &$crate::types::EntityId,
1290            field: &$field_enum,
1291        ) -> Result<Vec<$crate::types::EntityId>, $crate::error::RepositoryError> {
1292            Ok($crate::database::hashmap_store::junction_get(
1293                self.resolve_junction(field),
1294                id,
1295            ))
1296        }
1297
1298        fn get_relationship_many(
1299            &self,
1300            ids: &[$crate::types::EntityId],
1301            field: &$field_enum,
1302        ) -> Result<
1303            std::collections::HashMap<$crate::types::EntityId, Vec<$crate::types::EntityId>>,
1304            $crate::error::RepositoryError,
1305        > {
1306            let jn = self.resolve_junction(field);
1307            let mut map = std::collections::HashMap::new();
1308            for id in ids {
1309                map.insert(*id, $crate::database::hashmap_store::junction_get(jn, id));
1310            }
1311            Ok(map)
1312        }
1313
1314        fn get_relationship_count(
1315            &self,
1316            id: &$crate::types::EntityId,
1317            field: &$field_enum,
1318        ) -> Result<usize, $crate::error::RepositoryError> {
1319            Ok(
1320                $crate::database::hashmap_store::junction_get(self.resolve_junction(field), id)
1321                    .len(),
1322            )
1323        }
1324
1325        fn get_relationship_in_range(
1326            &self,
1327            id: &$crate::types::EntityId,
1328            field: &$field_enum,
1329            offset: usize,
1330            limit: usize,
1331        ) -> Result<Vec<$crate::types::EntityId>, $crate::error::RepositoryError> {
1332            let all =
1333                $crate::database::hashmap_store::junction_get(self.resolve_junction(field), id);
1334            Ok(all.into_iter().skip(offset).take(limit).collect())
1335        }
1336
1337        fn get_relationships_from_right_ids(
1338            &self,
1339            field: &$field_enum,
1340            right_ids: &[$crate::types::EntityId],
1341        ) -> Result<
1342            Vec<($crate::types::EntityId, Vec<$crate::types::EntityId>)>,
1343            $crate::error::RepositoryError,
1344        > {
1345            Ok(
1346                $crate::database::hashmap_store::junction_get_relationships_from_right_ids(
1347                    self.resolve_junction(field),
1348                    right_ids,
1349                ),
1350            )
1351        }
1352    };
1353}
1354
1355#[macro_export]
1356macro_rules! impl_write_relationship_methods {
1357    ($table_type:ty, $field_enum:ty) => {
1358        $crate::impl_relationship_methods!($table_type, $field_enum);
1359
1360        fn set_relationship_multi(
1361            &mut self,
1362            field: &$field_enum,
1363            relationships: Vec<($crate::types::EntityId, Vec<$crate::types::EntityId>)>,
1364        ) -> Result<(), $crate::error::RepositoryError> {
1365            for (id, _) in &relationships {
1366                if !self.relationship_source_exists(id) {
1367                    return Err($crate::error::RepositoryError::ConstraintViolation(
1368                        "relationship source does not exist".into(),
1369                    ));
1370                }
1371            }
1372            self.validate_relationship_updates(field, &relationships, &[])?;
1373            let jn = self.resolve_junction(field);
1374            for (left_id, entities) in relationships {
1375                $crate::database::hashmap_store::junction_set(jn, left_id, entities);
1376            }
1377            Ok(())
1378        }
1379
1380        fn set_relationship(
1381            &mut self,
1382            id: &$crate::types::EntityId,
1383            field: &$field_enum,
1384            right_ids: &[$crate::types::EntityId],
1385        ) -> Result<(), $crate::error::RepositoryError> {
1386            self.set_relationship_multi(field, vec![(*id, right_ids.to_vec())])
1387        }
1388
1389        fn move_relationship_ids(
1390            &mut self,
1391            id: &$crate::types::EntityId,
1392            field: &$field_enum,
1393            ids_to_move: &[$crate::types::EntityId],
1394            new_index: i32,
1395        ) -> Result<Vec<$crate::types::EntityId>, $crate::error::RepositoryError> {
1396            if !self.relationship_is_ordered(field) {
1397                return Err($crate::error::RepositoryError::ConstraintViolation(
1398                    "only ordered relationships can be moved".into(),
1399                ));
1400            }
1401            let current =
1402                $crate::database::hashmap_store::junction_get(self.resolve_junction(field), id);
1403            let mut seen = std::collections::HashSet::new();
1404            if ids_to_move
1405                .iter()
1406                .any(|target| !seen.insert(*target) || !current.contains(target))
1407            {
1408                return Err($crate::error::RepositoryError::ConstraintViolation(
1409                    "move requires distinct existing relationship members".into(),
1410                ));
1411            }
1412            self.validate_relationship_updates(field, &[(*id, current)], &[])?;
1413            if !self.relationship_source_exists(id) {
1414                return Err($crate::error::RepositoryError::ConstraintViolation(
1415                    "relationship source does not exist".into(),
1416                ));
1417            }
1418            Ok($crate::database::hashmap_store::junction_move_ids(
1419                self.resolve_junction(field),
1420                id,
1421                ids_to_move,
1422                new_index,
1423            ))
1424        }
1425    };
1426}
1427
1428// ═════════════════════════════════════════════════════════════════════════════
1429// Leaf entity macro (entities without forward relationships)
1430// ═════════════════════════════════════════════════════════════════════════════
1431
1432#[macro_export]
1433macro_rules! impl_leaf_entity_table {
1434    (
1435        entity: $Entity:ident,
1436        entity_name: $entity_name:expr,
1437        store_field: $store_field:ident,
1438        table_trait: $TableTrait:ident,
1439        table_ro_trait: $TableROTrait:ident,
1440        table_struct: $TableStruct:ident,
1441        table_ro_struct: $TableROStruct:ident,
1442        backward_junctions: [ $( ($bj_field:ident) ),* $(,)? ],
1443    ) => {
1444        pub struct $TableStruct<'a> {
1445            store: &'a $crate::database::hashmap_store::HashMapStore,
1446        }
1447
1448        impl<'a> $TableStruct<'a> {
1449            pub fn new(store: &'a $crate::database::hashmap_store::HashMapStore) -> Self {
1450                Self { store }
1451            }
1452        }
1453
1454        impl<'a> $TableTrait for $TableStruct<'a> {
1455            fn create(&mut self, entity: &$Entity) -> Result<$Entity, $crate::error::RepositoryError> {
1456                self.create_multi(std::slice::from_ref(entity))
1457                    .and_then(|v| {
1458                        v.into_iter().next().ok_or_else(|| {
1459                            $crate::error::RepositoryError::Other(::anyhow::anyhow!(
1460                                "create: the store returned no row for the entity it was given"
1461                            ))
1462                        })
1463                    })
1464            }
1465
1466            fn create_multi(&mut self, entities: &[$Entity]) -> Result<Vec<$Entity>, $crate::error::RepositoryError> {
1467                let mut created = Vec::with_capacity(entities.len());
1468                let mut map = $crate::database::hashmap_store::write_or_recover(&self.store.$store_field);
1469
1470                for entity in entities {
1471                    let new_entity = if entity.id == $crate::types::EntityId::default() {
1472                        let id = self.store.next_id($entity_name);
1473                        $Entity {
1474                            id,
1475                            ..entity.clone()
1476                        }
1477                    } else {
1478                        if map.contains_key(&entity.id) {
1479                            return Err($crate::error::RepositoryError::DuplicateId {
1480                                entity: stringify!($Entity),
1481                                id: entity.id,
1482                            });
1483                        }
1484                        entity.clone()
1485                    };
1486
1487                    map.insert(new_entity.id, new_entity.clone());
1488                    created.push(new_entity);
1489                }
1490                Ok(created)
1491            }
1492
1493            fn get(&self, id: &$crate::types::EntityId) -> Result<Option<$Entity>, $crate::error::RepositoryError> {
1494                Ok($crate::database::hashmap_store::read_or_recover(&self.store.$store_field).get(id).cloned())
1495            }
1496
1497            fn get_multi(&self, ids: &[$crate::types::EntityId]) -> Result<Vec<Option<$Entity>>, $crate::error::RepositoryError> {
1498                let map = $crate::database::hashmap_store::read_or_recover(&self.store.$store_field);
1499                Ok(ids.iter().map(|id| map.get(id).cloned()).collect())
1500            }
1501
1502            fn get_all(&self) -> Result<Vec<$Entity>, $crate::error::RepositoryError> {
1503                Ok($crate::database::hashmap_store::read_or_recover(&self.store.$store_field).values().cloned().collect())
1504            }
1505
1506            fn update(&mut self, entity: &$Entity) -> Result<$Entity, $crate::error::RepositoryError> {
1507                self.update_multi(std::slice::from_ref(entity))
1508                    .and_then(|v| {
1509                        v.into_iter().next().ok_or_else(|| {
1510                            $crate::error::RepositoryError::Other(::anyhow::anyhow!(
1511                                "update: the store returned no row for the entity it was given"
1512                            ))
1513                        })
1514                    })
1515            }
1516
1517            fn update_multi(&mut self, entities: &[$Entity]) -> Result<Vec<$Entity>, $crate::error::RepositoryError> {
1518                let mut map = $crate::database::hashmap_store::write_or_recover(&self.store.$store_field);
1519                let mut result = Vec::with_capacity(entities.len());
1520                for entity in entities {
1521                    map.insert(entity.id, entity.clone());
1522                    result.push(entity.clone());
1523                }
1524                Ok(result)
1525            }
1526
1527            fn update_with_relationships(&mut self, entity: &$Entity) -> Result<$Entity, $crate::error::RepositoryError> {
1528                self.update(entity)
1529            }
1530
1531            fn update_with_relationships_multi(&mut self, entities: &[$Entity]) -> Result<Vec<$Entity>, $crate::error::RepositoryError> {
1532                self.update_multi(entities)
1533            }
1534
1535            fn remove(&mut self, id: &$crate::types::EntityId) -> Result<(), $crate::error::RepositoryError> {
1536                self.remove_multi(std::slice::from_ref(id))
1537            }
1538
1539            fn remove_multi(&mut self, ids: &[$crate::types::EntityId]) -> Result<(), $crate::error::RepositoryError> {
1540                let mut map = $crate::database::hashmap_store::write_or_recover(&self.store.$store_field);
1541                for id in ids {
1542                    map.remove(id);
1543                    $(
1544                        $crate::database::hashmap_store::delete_from_backward_junction(&self.store.$bj_field, id);
1545                    )*
1546                }
1547                Ok(())
1548            }
1549        }
1550
1551        pub struct $TableROStruct<'a> {
1552            store: &'a $crate::database::hashmap_store::HashMapStore,
1553        }
1554
1555        impl<'a> $TableROStruct<'a> {
1556            pub fn new(store: &'a $crate::database::hashmap_store::HashMapStore) -> Self {
1557                Self { store }
1558            }
1559        }
1560
1561        impl<'a> $TableROTrait for $TableROStruct<'a> {
1562            fn get(&self, id: &$crate::types::EntityId) -> Result<Option<$Entity>, $crate::error::RepositoryError> {
1563                Ok($crate::database::hashmap_store::read_or_recover(&self.store.$store_field).get(id).cloned())
1564            }
1565
1566            fn get_multi(&self, ids: &[$crate::types::EntityId]) -> Result<Vec<Option<$Entity>>, $crate::error::RepositoryError> {
1567                let map = $crate::database::hashmap_store::read_or_recover(&self.store.$store_field);
1568                Ok(ids.iter().map(|id| map.get(id).cloned()).collect())
1569            }
1570
1571            fn get_all(&self) -> Result<Vec<$Entity>, $crate::error::RepositoryError> {
1572                Ok($crate::database::hashmap_store::read_or_recover(&self.store.$store_field).values().cloned().collect())
1573            }
1574        }
1575    };
1576}
1577
1578// ═════════════════════════════════════════════════════════════════════════════
1579// Junction index tests (entity-independent, so generated into every project)
1580// ═════════════════════════════════════════════════════════════════════════════
1581
1582#[cfg(test)]
1583mod junction_tests {
1584    use super::*;
1585    use crate::error::RepositoryError;
1586
1587    /// The 2.0.0 check, kept verbatim as the reference: it walks every row of
1588    /// the junction that is not being replaced. The indexed check must give
1589    /// the same answer for every junction and every batch.
1590    fn validate_by_walking(
1591        junction: &HashMapJunction,
1592        replacements: &[(EntityId, Vec<EntityId>)],
1593        scalar: bool,
1594    ) -> Result<(), RepositoryError> {
1595        use RepositoryError::ConstraintViolation;
1596        use std::collections::HashSet;
1597        let mut owners = HashSet::new();
1598        for (owner, targets) in replacements {
1599            if !owners.insert(*owner) {
1600                return Err(ConstraintViolation(
1601                    "duplicate source in relationship batch".into(),
1602                ));
1603            }
1604            if scalar && targets.len() > 1 {
1605                return Err(ConstraintViolation(
1606                    "to-one relationship accepts at most one target".into(),
1607                ));
1608            }
1609            let mut unique = HashSet::new();
1610            if targets.iter().any(|id| !unique.insert(*id)) {
1611                return Err(ConstraintViolation("duplicate relationship target".into()));
1612            }
1613        }
1614        if junction.is_exclusive() {
1615            let mut used = HashSet::new();
1616            for (_, targets) in junction.iter().filter(|(id, _)| !owners.contains(id)) {
1617                used.extend(targets.iter().copied());
1618            }
1619            for (_, targets) in replacements {
1620                if targets.iter().any(|id| !used.insert(*id)) {
1621                    return Err(ConstraintViolation(
1622                        "relationship target already belongs to another source".into(),
1623                    ));
1624                }
1625            }
1626        }
1627        Ok(())
1628    }
1629
1630    /// xorshift64: deterministic, so a failure replays exactly.
1631    struct Rng(u64);
1632
1633    impl Rng {
1634        fn below(&mut self, bound: u64) -> u64 {
1635            self.0 ^= self.0 << 13;
1636            self.0 ^= self.0 >> 7;
1637            self.0 ^= self.0 << 17;
1638            self.0 % bound
1639        }
1640
1641        fn ids(&mut self, max_len: u64, bound: u64) -> Vec<EntityId> {
1642            (0..self.below(max_len + 1))
1643                .map(|_| self.below(bound))
1644                .collect()
1645        }
1646    }
1647
1648    /// Drive every writer at random, including the unvalidated ones a scoped
1649    /// undo uses, so rows may share a target or repeat one. After each step the
1650    /// index must equal a rebuild from the rows, and the indexed check must
1651    /// answer a random batch exactly as the walk does.
1652    #[test]
1653    fn the_index_stays_exact_and_the_check_matches_the_walk() {
1654        for exclusive in [true, false] {
1655            let mut rng = Rng(0x9E37_79B9_7F4A_7C15 + u64::from(exclusive));
1656            let junction = RwLock::new(if exclusive {
1657                HashMapJunction::exclusive()
1658            } else {
1659                HashMapJunction::shared()
1660            });
1661            for step in 0..3000 {
1662                {
1663                    let mut jn = write_or_recover(&junction);
1664                    match rng.below(6) {
1665                        0..=2 => {
1666                            let (source, targets) = (rng.below(12), rng.ids(4, 16));
1667                            jn.insert(source, targets);
1668                        }
1669                        3 => {
1670                            jn.remove(&rng.below(12));
1671                        }
1672                        4 => jn.remove_target(&rng.below(16)),
1673                        // What snapshot, savepoint and restore do: swap in a clone.
1674                        _ => {
1675                            let copy = jn.clone();
1676                            *jn = copy;
1677                        }
1678                    }
1679                    if let Err(problem) = jn.check_index() {
1680                        panic!("exclusive={exclusive}, step {step}: {problem}");
1681                    }
1682                }
1683                let batch: Vec<(EntityId, Vec<EntityId>)> = (0..=rng.below(3))
1684                    .map(|_| (rng.below(12), rng.ids(2, 16)))
1685                    .collect();
1686                for scalar in [false, true] {
1687                    let walked = validate_by_walking(&read_or_recover(&junction), &batch, scalar);
1688                    let indexed = validate_junction_updates(&junction, &batch, scalar);
1689                    assert_eq!(
1690                        format!("{indexed:?}"),
1691                        format!("{walked:?}"),
1692                        "exclusive={exclusive}, step {step}, scalar={scalar}, batch {batch:?}"
1693                    );
1694                }
1695            }
1696        }
1697    }
1698
1699    #[test]
1700    fn a_transfer_within_one_batch_is_valid_but_not_from_outside_it() {
1701        let junction = RwLock::new(HashMapJunction::exclusive());
1702        junction_set(&junction, 1, vec![10, 11]);
1703        junction_set(&junction, 2, vec![20]);
1704        let check = |batch: &[(EntityId, Vec<EntityId>)]| {
1705            validate_junction_updates(&junction, batch, false)
1706        };
1707        assert!(check(&[(1, vec![11]), (2, vec![20, 10])]).is_ok());
1708        assert!(check(&[(2, vec![20, 10])]).is_err());
1709        assert!(check(&[(3, vec![10])]).is_err());
1710        assert!(check(&[(1, vec![]), (2, vec![10]), (3, vec![10])]).is_err());
1711        // Nothing to place: valid however full the junction is.
1712        assert!(check(&[(3, vec![]), (4, vec![])]).is_ok());
1713        // Deleting the owner, or the target, frees the target.
1714        junction_remove(&junction, &1);
1715        assert!(check(&[(3, vec![10])]).is_ok());
1716        delete_from_backward_junction(&junction, &20);
1717        assert!(check(&[(3, vec![20])]).is_ok());
1718        assert_eq!(junction_get(&junction, &2), Vec::<EntityId>::new());
1719        assert!(read_or_recover(&junction).check_index().is_ok());
1720    }
1721}