Skip to main content

common/database/
hashmap_store.rs

1// Generated by Qleany v1.11.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, Default)]
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<HashMap<EntityId, Vec<EntityId>>>,
39    pub jn_workspace_from_root_workspace: RwLock<HashMap<EntityId, Vec<EntityId>>>,
40    pub jn_entity_from_workspace_entities: RwLock<HashMap<EntityId, Vec<EntityId>>>,
41    pub jn_feature_from_workspace_features: RwLock<HashMap<EntityId, Vec<EntityId>>>,
42    pub jn_global_from_workspace_global: RwLock<HashMap<EntityId, Vec<EntityId>>>,
43    pub jn_user_interface_from_workspace_user_interface: RwLock<HashMap<EntityId, Vec<EntityId>>>,
44    pub jn_file_from_system_files: RwLock<HashMap<EntityId, Vec<EntityId>>>,
45    pub jn_field_from_entity_fields: RwLock<HashMap<EntityId, Vec<EntityId>>>,
46    pub jn_entity_from_entity_inherits_from: RwLock<HashMap<EntityId, Vec<EntityId>>>,
47    pub jn_relationship_from_entity_relationships: RwLock<HashMap<EntityId, Vec<EntityId>>>,
48    pub jn_entity_from_field_entity: RwLock<HashMap<EntityId, Vec<EntityId>>>,
49    pub jn_use_case_from_feature_use_cases: RwLock<HashMap<EntityId, Vec<EntityId>>>,
50    pub jn_entity_from_file_entity: RwLock<HashMap<EntityId, Vec<EntityId>>>,
51    pub jn_feature_from_file_feature: RwLock<HashMap<EntityId, Vec<EntityId>>>,
52    pub jn_field_from_file_field: RwLock<HashMap<EntityId, Vec<EntityId>>>,
53    pub jn_use_case_from_file_use_case: RwLock<HashMap<EntityId, Vec<EntityId>>>,
54    pub jn_dto_from_use_case_dto_in: RwLock<HashMap<EntityId, Vec<EntityId>>>,
55    pub jn_dto_from_use_case_dto_out: RwLock<HashMap<EntityId, Vec<EntityId>>>,
56    pub jn_entity_from_use_case_entities: RwLock<HashMap<EntityId, Vec<EntityId>>>,
57    pub jn_dto_field_from_dto_fields: RwLock<HashMap<EntityId, Vec<EntityId>>>,
58    pub jn_entity_from_relationship_left_entity: RwLock<HashMap<EntityId, Vec<EntityId>>>,
59    pub jn_entity_from_relationship_right_entity: RwLock<HashMap<EntityId, Vec<EntityId>>>,
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}
68
69impl HashMapStore {
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    /// Clone the entire store for savepoint support. O(1) thanks to im::HashMap.
75    /// (Poison-tolerant, but NOT atomic across tables — used for savepoints, which
76    /// are taken on the single writer thread with no concurrent writer; use
77    /// [`Self::freeze`] when an atomic cross-table snapshot is required.)
78    pub fn snapshot(&self) -> HashMapStoreSnapshot {
79        HashMapStoreSnapshot {
80            roots: read_or_recover(&self.roots).clone(),
81            workspaces: read_or_recover(&self.workspaces).clone(),
82            systems: read_or_recover(&self.systems).clone(),
83            entities: read_or_recover(&self.entities).clone(),
84            fields: read_or_recover(&self.fields).clone(),
85            features: read_or_recover(&self.features).clone(),
86            files: read_or_recover(&self.files).clone(),
87            use_cases: read_or_recover(&self.use_cases).clone(),
88            dtos: read_or_recover(&self.dtos).clone(),
89            dto_fields: read_or_recover(&self.dto_fields).clone(),
90            globals: read_or_recover(&self.globals).clone(),
91            relationships: read_or_recover(&self.relationships).clone(),
92            user_interfaces: read_or_recover(&self.user_interfaces).clone(),
93            jn_system_from_root_system: read_or_recover(&self.jn_system_from_root_system).clone(),
94            jn_workspace_from_root_workspace: read_or_recover(
95                &self.jn_workspace_from_root_workspace,
96            )
97            .clone(),
98            jn_entity_from_workspace_entities: read_or_recover(
99                &self.jn_entity_from_workspace_entities,
100            )
101            .clone(),
102            jn_feature_from_workspace_features: read_or_recover(
103                &self.jn_feature_from_workspace_features,
104            )
105            .clone(),
106            jn_global_from_workspace_global: read_or_recover(&self.jn_global_from_workspace_global)
107                .clone(),
108            jn_user_interface_from_workspace_user_interface: read_or_recover(
109                &self.jn_user_interface_from_workspace_user_interface,
110            )
111            .clone(),
112            jn_file_from_system_files: read_or_recover(&self.jn_file_from_system_files).clone(),
113            jn_field_from_entity_fields: read_or_recover(&self.jn_field_from_entity_fields).clone(),
114            jn_entity_from_entity_inherits_from: read_or_recover(
115                &self.jn_entity_from_entity_inherits_from,
116            )
117            .clone(),
118            jn_relationship_from_entity_relationships: read_or_recover(
119                &self.jn_relationship_from_entity_relationships,
120            )
121            .clone(),
122            jn_entity_from_field_entity: read_or_recover(&self.jn_entity_from_field_entity).clone(),
123            jn_use_case_from_feature_use_cases: read_or_recover(
124                &self.jn_use_case_from_feature_use_cases,
125            )
126            .clone(),
127            jn_entity_from_file_entity: read_or_recover(&self.jn_entity_from_file_entity).clone(),
128            jn_feature_from_file_feature: read_or_recover(&self.jn_feature_from_file_feature)
129                .clone(),
130            jn_field_from_file_field: read_or_recover(&self.jn_field_from_file_field).clone(),
131            jn_use_case_from_file_use_case: read_or_recover(&self.jn_use_case_from_file_use_case)
132                .clone(),
133            jn_dto_from_use_case_dto_in: read_or_recover(&self.jn_dto_from_use_case_dto_in).clone(),
134            jn_dto_from_use_case_dto_out: read_or_recover(&self.jn_dto_from_use_case_dto_out)
135                .clone(),
136            jn_entity_from_use_case_entities: read_or_recover(
137                &self.jn_entity_from_use_case_entities,
138            )
139            .clone(),
140            jn_dto_field_from_dto_fields: read_or_recover(&self.jn_dto_field_from_dto_fields)
141                .clone(),
142            jn_entity_from_relationship_left_entity: read_or_recover(
143                &self.jn_entity_from_relationship_left_entity,
144            )
145            .clone(),
146            jn_entity_from_relationship_right_entity: read_or_recover(
147                &self.jn_entity_from_relationship_right_entity,
148            )
149            .clone(),
150            counters: read_or_recover(&self.counters).clone(),
151        }
152    }
153
154    /// Atomically snapshot the whole store into a fresh, isolated `HashMapStore`.
155    ///
156    /// Unlike [`Self::snapshot`], this holds a read guard on **every** table, junction,
157    /// and the counters *simultaneously* while cloning, so the result is a single
158    /// consistent point-in-time view of the store *at the instant freeze runs*.
159    /// O(1) clones (im::HashMap structural sharing); the lock hold is just the
160    /// clones. The returned store carries no savepoints and is meant to back a
161    /// read-only (frozen) transaction.
162    ///
163    /// Deadlock-freedom depends on an **invariant** (unenforced by the type
164    /// system): guards are acquired in the struct's declared field order (all
165    /// entity tables, then all junctions, then counters), and no writer ever
166    /// holds two *entity-table* locks simultaneously — each writer takes at most
167    /// one entity table, then its junctions, in table-before-junction order (see
168    /// `remove_multi`). A future cascade that nested two entity-table locks would
169    /// violate this and could deadlock against freeze; keep cascades single-table.
170    ///
171    /// SCOPE: freeze is atomic w.r.t. *itself*, not w.r.t. a writer's *multi-step*
172    /// cascade. A write transaction is not one critical section — its per-repo
173    /// calls each lock/unlock independently — so a cascading delete/create can
174    /// leave a brief window (e.g. child rows removed but the parent's junction not
175    /// yet cleared) into which freeze can land and capture a cross-table-torn
176    /// snapshot (`gather` then hits "entity vanished mid-read", or silently omits
177    /// a just-added-but-unlinked row). This shrinks the torn-read window from the
178    /// whole read to a single instant, but does not eliminate it; a store-wide
179    /// writer/reader gate would be required to fully close it.
180    pub fn freeze(&self) -> HashMapStore {
181        // Acquire ALL read guards first (declared order), then clone under that
182        // one consistent lock set.
183        let g_roots = read_or_recover(&self.roots);
184        let g_workspaces = read_or_recover(&self.workspaces);
185        let g_systems = read_or_recover(&self.systems);
186        let g_entities = read_or_recover(&self.entities);
187        let g_fields = read_or_recover(&self.fields);
188        let g_features = read_or_recover(&self.features);
189        let g_files = read_or_recover(&self.files);
190        let g_use_cases = read_or_recover(&self.use_cases);
191        let g_dtos = read_or_recover(&self.dtos);
192        let g_dto_fields = read_or_recover(&self.dto_fields);
193        let g_globals = read_or_recover(&self.globals);
194        let g_relationships = read_or_recover(&self.relationships);
195        let g_user_interfaces = read_or_recover(&self.user_interfaces);
196        let g_jn_system_from_root_system = read_or_recover(&self.jn_system_from_root_system);
197        let g_jn_workspace_from_root_workspace =
198            read_or_recover(&self.jn_workspace_from_root_workspace);
199        let g_jn_entity_from_workspace_entities =
200            read_or_recover(&self.jn_entity_from_workspace_entities);
201        let g_jn_feature_from_workspace_features =
202            read_or_recover(&self.jn_feature_from_workspace_features);
203        let g_jn_global_from_workspace_global =
204            read_or_recover(&self.jn_global_from_workspace_global);
205        let g_jn_user_interface_from_workspace_user_interface =
206            read_or_recover(&self.jn_user_interface_from_workspace_user_interface);
207        let g_jn_file_from_system_files = read_or_recover(&self.jn_file_from_system_files);
208        let g_jn_field_from_entity_fields = read_or_recover(&self.jn_field_from_entity_fields);
209        let g_jn_entity_from_entity_inherits_from =
210            read_or_recover(&self.jn_entity_from_entity_inherits_from);
211        let g_jn_relationship_from_entity_relationships =
212            read_or_recover(&self.jn_relationship_from_entity_relationships);
213        let g_jn_entity_from_field_entity = read_or_recover(&self.jn_entity_from_field_entity);
214        let g_jn_use_case_from_feature_use_cases =
215            read_or_recover(&self.jn_use_case_from_feature_use_cases);
216        let g_jn_entity_from_file_entity = read_or_recover(&self.jn_entity_from_file_entity);
217        let g_jn_feature_from_file_feature = read_or_recover(&self.jn_feature_from_file_feature);
218        let g_jn_field_from_file_field = read_or_recover(&self.jn_field_from_file_field);
219        let g_jn_use_case_from_file_use_case =
220            read_or_recover(&self.jn_use_case_from_file_use_case);
221        let g_jn_dto_from_use_case_dto_in = read_or_recover(&self.jn_dto_from_use_case_dto_in);
222        let g_jn_dto_from_use_case_dto_out = read_or_recover(&self.jn_dto_from_use_case_dto_out);
223        let g_jn_entity_from_use_case_entities =
224            read_or_recover(&self.jn_entity_from_use_case_entities);
225        let g_jn_dto_field_from_dto_fields = read_or_recover(&self.jn_dto_field_from_dto_fields);
226        let g_jn_entity_from_relationship_left_entity =
227            read_or_recover(&self.jn_entity_from_relationship_left_entity);
228        let g_jn_entity_from_relationship_right_entity =
229            read_or_recover(&self.jn_entity_from_relationship_right_entity);
230        let g_counters = read_or_recover(&self.counters);
231
232        HashMapStore {
233            roots: RwLock::new(g_roots.clone()),
234            workspaces: RwLock::new(g_workspaces.clone()),
235            systems: RwLock::new(g_systems.clone()),
236            entities: RwLock::new(g_entities.clone()),
237            fields: RwLock::new(g_fields.clone()),
238            features: RwLock::new(g_features.clone()),
239            files: RwLock::new(g_files.clone()),
240            use_cases: RwLock::new(g_use_cases.clone()),
241            dtos: RwLock::new(g_dtos.clone()),
242            dto_fields: RwLock::new(g_dto_fields.clone()),
243            globals: RwLock::new(g_globals.clone()),
244            relationships: RwLock::new(g_relationships.clone()),
245            user_interfaces: RwLock::new(g_user_interfaces.clone()),
246            jn_system_from_root_system: RwLock::new(g_jn_system_from_root_system.clone()),
247            jn_workspace_from_root_workspace: RwLock::new(
248                g_jn_workspace_from_root_workspace.clone(),
249            ),
250            jn_entity_from_workspace_entities: RwLock::new(
251                g_jn_entity_from_workspace_entities.clone(),
252            ),
253            jn_feature_from_workspace_features: RwLock::new(
254                g_jn_feature_from_workspace_features.clone(),
255            ),
256            jn_global_from_workspace_global: RwLock::new(g_jn_global_from_workspace_global.clone()),
257            jn_user_interface_from_workspace_user_interface: RwLock::new(
258                g_jn_user_interface_from_workspace_user_interface.clone(),
259            ),
260            jn_file_from_system_files: RwLock::new(g_jn_file_from_system_files.clone()),
261            jn_field_from_entity_fields: RwLock::new(g_jn_field_from_entity_fields.clone()),
262            jn_entity_from_entity_inherits_from: RwLock::new(
263                g_jn_entity_from_entity_inherits_from.clone(),
264            ),
265            jn_relationship_from_entity_relationships: RwLock::new(
266                g_jn_relationship_from_entity_relationships.clone(),
267            ),
268            jn_entity_from_field_entity: RwLock::new(g_jn_entity_from_field_entity.clone()),
269            jn_use_case_from_feature_use_cases: RwLock::new(
270                g_jn_use_case_from_feature_use_cases.clone(),
271            ),
272            jn_entity_from_file_entity: RwLock::new(g_jn_entity_from_file_entity.clone()),
273            jn_feature_from_file_feature: RwLock::new(g_jn_feature_from_file_feature.clone()),
274            jn_field_from_file_field: RwLock::new(g_jn_field_from_file_field.clone()),
275            jn_use_case_from_file_use_case: RwLock::new(g_jn_use_case_from_file_use_case.clone()),
276            jn_dto_from_use_case_dto_in: RwLock::new(g_jn_dto_from_use_case_dto_in.clone()),
277            jn_dto_from_use_case_dto_out: RwLock::new(g_jn_dto_from_use_case_dto_out.clone()),
278            jn_entity_from_use_case_entities: RwLock::new(
279                g_jn_entity_from_use_case_entities.clone(),
280            ),
281            jn_dto_field_from_dto_fields: RwLock::new(g_jn_dto_field_from_dto_fields.clone()),
282            jn_entity_from_relationship_left_entity: RwLock::new(
283                g_jn_entity_from_relationship_left_entity.clone(),
284            ),
285            jn_entity_from_relationship_right_entity: RwLock::new(
286                g_jn_entity_from_relationship_right_entity.clone(),
287            ),
288            counters: RwLock::new(g_counters.clone()),
289            savepoints: RwLock::new(std::collections::HashMap::new()),
290            next_savepoint_id: RwLock::new(0),
291        }
292    }
293
294    /// Restore from a savepoint snapshot.
295    pub fn restore(&self, snap: &HashMapStoreSnapshot) {
296        *write_or_recover(&self.roots) = snap.roots.clone();
297        *write_or_recover(&self.workspaces) = snap.workspaces.clone();
298        *write_or_recover(&self.systems) = snap.systems.clone();
299        *write_or_recover(&self.entities) = snap.entities.clone();
300        *write_or_recover(&self.fields) = snap.fields.clone();
301        *write_or_recover(&self.features) = snap.features.clone();
302        *write_or_recover(&self.files) = snap.files.clone();
303        *write_or_recover(&self.use_cases) = snap.use_cases.clone();
304        *write_or_recover(&self.dtos) = snap.dtos.clone();
305        *write_or_recover(&self.dto_fields) = snap.dto_fields.clone();
306        *write_or_recover(&self.globals) = snap.globals.clone();
307        *write_or_recover(&self.relationships) = snap.relationships.clone();
308        *write_or_recover(&self.user_interfaces) = snap.user_interfaces.clone();
309        *write_or_recover(&self.jn_system_from_root_system) =
310            snap.jn_system_from_root_system.clone();
311        *write_or_recover(&self.jn_workspace_from_root_workspace) =
312            snap.jn_workspace_from_root_workspace.clone();
313        *write_or_recover(&self.jn_entity_from_workspace_entities) =
314            snap.jn_entity_from_workspace_entities.clone();
315        *write_or_recover(&self.jn_feature_from_workspace_features) =
316            snap.jn_feature_from_workspace_features.clone();
317        *write_or_recover(&self.jn_global_from_workspace_global) =
318            snap.jn_global_from_workspace_global.clone();
319        *write_or_recover(&self.jn_user_interface_from_workspace_user_interface) =
320            snap.jn_user_interface_from_workspace_user_interface.clone();
321        *write_or_recover(&self.jn_file_from_system_files) = snap.jn_file_from_system_files.clone();
322        *write_or_recover(&self.jn_field_from_entity_fields) =
323            snap.jn_field_from_entity_fields.clone();
324        *write_or_recover(&self.jn_entity_from_entity_inherits_from) =
325            snap.jn_entity_from_entity_inherits_from.clone();
326        *write_or_recover(&self.jn_relationship_from_entity_relationships) =
327            snap.jn_relationship_from_entity_relationships.clone();
328        *write_or_recover(&self.jn_entity_from_field_entity) =
329            snap.jn_entity_from_field_entity.clone();
330        *write_or_recover(&self.jn_use_case_from_feature_use_cases) =
331            snap.jn_use_case_from_feature_use_cases.clone();
332        *write_or_recover(&self.jn_entity_from_file_entity) =
333            snap.jn_entity_from_file_entity.clone();
334        *write_or_recover(&self.jn_feature_from_file_feature) =
335            snap.jn_feature_from_file_feature.clone();
336        *write_or_recover(&self.jn_field_from_file_field) = snap.jn_field_from_file_field.clone();
337        *write_or_recover(&self.jn_use_case_from_file_use_case) =
338            snap.jn_use_case_from_file_use_case.clone();
339        *write_or_recover(&self.jn_dto_from_use_case_dto_in) =
340            snap.jn_dto_from_use_case_dto_in.clone();
341        *write_or_recover(&self.jn_dto_from_use_case_dto_out) =
342            snap.jn_dto_from_use_case_dto_out.clone();
343        *write_or_recover(&self.jn_entity_from_use_case_entities) =
344            snap.jn_entity_from_use_case_entities.clone();
345        *write_or_recover(&self.jn_dto_field_from_dto_fields) =
346            snap.jn_dto_field_from_dto_fields.clone();
347        *write_or_recover(&self.jn_entity_from_relationship_left_entity) =
348            snap.jn_entity_from_relationship_left_entity.clone();
349        *write_or_recover(&self.jn_entity_from_relationship_right_entity) =
350            snap.jn_entity_from_relationship_right_entity.clone();
351        *write_or_recover(&self.counters) = snap.counters.clone();
352    }
353
354    /// Create a savepoint by snapshotting the entire store. Returns a savepoint id.
355    /// O(1) thanks to im::HashMap structural sharing.
356    pub fn create_savepoint(&self) -> u64 {
357        let snap = self.snapshot();
358        let mut id_counter = write_or_recover(&self.next_savepoint_id);
359        let id = *id_counter;
360        *id_counter += 1;
361        write_or_recover(&self.savepoints).insert(id, snap);
362        id
363    }
364
365    /// Restore the store to a previously created savepoint.
366    pub fn restore_savepoint(&self, savepoint_id: u64) {
367        // A missing savepoint is a caller bug, but this is reached from
368        // `Transaction`'s **`Drop`-time rollback** — and panicking in a `Drop`
369        // that is itself running because of an earlier panic aborts the process
370        // outright, no matter what the panic strategy is. That double-panic is
371        // exactly what this module's poison recovery exists to avoid, so the
372        // rollback must not be the thing that reintroduces it.
373        //
374        // Loud in a debug build, a no-op in a shipped one: nothing to restore
375        // to means the store simply stays as it is.
376        let Some(snap) = read_or_recover(&self.savepoints)
377            .get(&savepoint_id)
378            .cloned()
379        else {
380            debug_assert!(
381                false,
382                "restore_savepoint: savepoint {savepoint_id} does not exist — it was already \
383                 restored, discarded, or never created"
384            );
385            return;
386        };
387        self.restore(&snap);
388    }
389
390    /// Discard a savepoint without restoring (used on successful commit, and
391    /// after a rollback so a rolled-back transaction doesn't leak its snapshot).
392    pub fn discard_savepoint(&self, savepoint_id: u64) {
393        write_or_recover(&self.savepoints).remove(&savepoint_id);
394    }
395
396    /// Get-and-increment counter for an entity type.
397    pub(crate) fn next_id(&self, entity_name: &str) -> EntityId {
398        let mut counters = write_or_recover(&self.counters);
399        let counter = counters.entry(entity_name.to_string()).or_insert(1);
400        let id = *counter;
401        *counter += 1;
402        id
403    }
404
405    /// Restore entity and junction data but preserve current counters.
406    /// Used for undo snapshots where IDs must remain monotonically increasing.
407    pub fn restore_without_counters(&self, snap: &HashMapStoreSnapshot) {
408        *write_or_recover(&self.roots) = snap.roots.clone();
409        *write_or_recover(&self.workspaces) = snap.workspaces.clone();
410        *write_or_recover(&self.systems) = snap.systems.clone();
411        *write_or_recover(&self.entities) = snap.entities.clone();
412        *write_or_recover(&self.fields) = snap.fields.clone();
413        *write_or_recover(&self.features) = snap.features.clone();
414        *write_or_recover(&self.files) = snap.files.clone();
415        *write_or_recover(&self.use_cases) = snap.use_cases.clone();
416        *write_or_recover(&self.dtos) = snap.dtos.clone();
417        *write_or_recover(&self.dto_fields) = snap.dto_fields.clone();
418        *write_or_recover(&self.globals) = snap.globals.clone();
419        *write_or_recover(&self.relationships) = snap.relationships.clone();
420        *write_or_recover(&self.user_interfaces) = snap.user_interfaces.clone();
421        *write_or_recover(&self.jn_system_from_root_system) =
422            snap.jn_system_from_root_system.clone();
423        *write_or_recover(&self.jn_workspace_from_root_workspace) =
424            snap.jn_workspace_from_root_workspace.clone();
425        *write_or_recover(&self.jn_entity_from_workspace_entities) =
426            snap.jn_entity_from_workspace_entities.clone();
427        *write_or_recover(&self.jn_feature_from_workspace_features) =
428            snap.jn_feature_from_workspace_features.clone();
429        *write_or_recover(&self.jn_global_from_workspace_global) =
430            snap.jn_global_from_workspace_global.clone();
431        *write_or_recover(&self.jn_user_interface_from_workspace_user_interface) =
432            snap.jn_user_interface_from_workspace_user_interface.clone();
433        *write_or_recover(&self.jn_file_from_system_files) = snap.jn_file_from_system_files.clone();
434        *write_or_recover(&self.jn_field_from_entity_fields) =
435            snap.jn_field_from_entity_fields.clone();
436        *write_or_recover(&self.jn_entity_from_entity_inherits_from) =
437            snap.jn_entity_from_entity_inherits_from.clone();
438        *write_or_recover(&self.jn_relationship_from_entity_relationships) =
439            snap.jn_relationship_from_entity_relationships.clone();
440        *write_or_recover(&self.jn_entity_from_field_entity) =
441            snap.jn_entity_from_field_entity.clone();
442        *write_or_recover(&self.jn_use_case_from_feature_use_cases) =
443            snap.jn_use_case_from_feature_use_cases.clone();
444        *write_or_recover(&self.jn_entity_from_file_entity) =
445            snap.jn_entity_from_file_entity.clone();
446        *write_or_recover(&self.jn_feature_from_file_feature) =
447            snap.jn_feature_from_file_feature.clone();
448        *write_or_recover(&self.jn_field_from_file_field) = snap.jn_field_from_file_field.clone();
449        *write_or_recover(&self.jn_use_case_from_file_use_case) =
450            snap.jn_use_case_from_file_use_case.clone();
451        *write_or_recover(&self.jn_dto_from_use_case_dto_in) =
452            snap.jn_dto_from_use_case_dto_in.clone();
453        *write_or_recover(&self.jn_dto_from_use_case_dto_out) =
454            snap.jn_dto_from_use_case_dto_out.clone();
455        *write_or_recover(&self.jn_entity_from_use_case_entities) =
456            snap.jn_entity_from_use_case_entities.clone();
457        *write_or_recover(&self.jn_dto_field_from_dto_fields) =
458            snap.jn_dto_field_from_dto_fields.clone();
459        *write_or_recover(&self.jn_entity_from_relationship_left_entity) =
460            snap.jn_entity_from_relationship_left_entity.clone();
461        *write_or_recover(&self.jn_entity_from_relationship_right_entity) =
462            snap.jn_entity_from_relationship_right_entity.clone();
463        // counters intentionally NOT restored — IDs must remain monotonically increasing
464    }
465
466    /// Create a type-erased store snapshot for undo.
467    pub fn store_snapshot(&self) -> StoreSnapshot {
468        StoreSnapshot::new(self.snapshot())
469    }
470
471    /// Restore from a type-erased store snapshot (preserves counters).
472    pub fn restore_store_snapshot(&self, snap: &StoreSnapshot) {
473        let s = snap
474            .downcast_ref::<HashMapStoreSnapshot>()
475            .expect("StoreSnapshot must contain HashMapStoreSnapshot");
476        self.restore_without_counters(s);
477    }
478}
479
480/// Snapshot of the entire store. O(1) to create thanks to im::HashMap structural sharing.
481///
482/// Entity-table and junction fields are `pub(crate)` so the generated per-entity
483/// repositories (same crate) can read the captured rows/junctions during a *scoped*
484/// restore. Counters stay private (restore never touches them — ids stay monotonic).
485#[derive(Debug, Clone)]
486pub struct HashMapStoreSnapshot {
487    pub(crate) roots: HashMap<EntityId, Root>,
488    pub(crate) workspaces: HashMap<EntityId, Workspace>,
489    pub(crate) systems: HashMap<EntityId, System>,
490    pub(crate) entities: HashMap<EntityId, Entity>,
491    pub(crate) fields: HashMap<EntityId, Field>,
492    pub(crate) features: HashMap<EntityId, Feature>,
493    pub(crate) files: HashMap<EntityId, File>,
494    pub(crate) use_cases: HashMap<EntityId, UseCase>,
495    pub(crate) dtos: HashMap<EntityId, Dto>,
496    pub(crate) dto_fields: HashMap<EntityId, DtoField>,
497    pub(crate) globals: HashMap<EntityId, Global>,
498    pub(crate) relationships: HashMap<EntityId, Relationship>,
499    pub(crate) user_interfaces: HashMap<EntityId, UserInterface>,
500    pub(crate) jn_system_from_root_system: HashMap<EntityId, Vec<EntityId>>,
501    pub(crate) jn_workspace_from_root_workspace: HashMap<EntityId, Vec<EntityId>>,
502    pub(crate) jn_entity_from_workspace_entities: HashMap<EntityId, Vec<EntityId>>,
503    pub(crate) jn_feature_from_workspace_features: HashMap<EntityId, Vec<EntityId>>,
504    pub(crate) jn_global_from_workspace_global: HashMap<EntityId, Vec<EntityId>>,
505    pub(crate) jn_user_interface_from_workspace_user_interface: HashMap<EntityId, Vec<EntityId>>,
506    pub(crate) jn_file_from_system_files: HashMap<EntityId, Vec<EntityId>>,
507    pub(crate) jn_field_from_entity_fields: HashMap<EntityId, Vec<EntityId>>,
508    pub(crate) jn_entity_from_entity_inherits_from: HashMap<EntityId, Vec<EntityId>>,
509    pub(crate) jn_relationship_from_entity_relationships: HashMap<EntityId, Vec<EntityId>>,
510    pub(crate) jn_entity_from_field_entity: HashMap<EntityId, Vec<EntityId>>,
511    pub(crate) jn_use_case_from_feature_use_cases: HashMap<EntityId, Vec<EntityId>>,
512    pub(crate) jn_entity_from_file_entity: HashMap<EntityId, Vec<EntityId>>,
513    pub(crate) jn_feature_from_file_feature: HashMap<EntityId, Vec<EntityId>>,
514    pub(crate) jn_field_from_file_field: HashMap<EntityId, Vec<EntityId>>,
515    pub(crate) jn_use_case_from_file_use_case: HashMap<EntityId, Vec<EntityId>>,
516    pub(crate) jn_dto_from_use_case_dto_in: HashMap<EntityId, Vec<EntityId>>,
517    pub(crate) jn_dto_from_use_case_dto_out: HashMap<EntityId, Vec<EntityId>>,
518    pub(crate) jn_entity_from_use_case_entities: HashMap<EntityId, Vec<EntityId>>,
519    pub(crate) jn_dto_field_from_dto_fields: HashMap<EntityId, Vec<EntityId>>,
520    pub(crate) jn_entity_from_relationship_left_entity: HashMap<EntityId, Vec<EntityId>>,
521    pub(crate) jn_entity_from_relationship_right_entity: HashMap<EntityId, Vec<EntityId>>,
522    counters: std::collections::HashMap<String, EntityId>,
523}
524
525impl StoreSnapshotTrait for HashMapStoreSnapshot {
526    fn clone_box(&self) -> Box<dyn StoreSnapshotTrait> {
527        Box::new(self.clone())
528    }
529
530    fn as_any(&self) -> &dyn std::any::Any {
531        self
532    }
533}
534
535// ─────────────────────────────────────────────────────────────────────────────
536// Helper functions
537// ─────────────────────────────────────────────────────────────────────────────
538
539/// Acquire a read guard, recovering the inner value if the lock was poisoned by
540/// a panic elsewhere — and **clearing** the poison so recovery is permanent, not
541/// one-shot (a recovered-but-still-poisoned lock would re-panic on the next plain
542/// `.read()/.write().unwrap()`). Used throughout the store's own snapshot /
543/// savepoint / restore paths so a single table poisoned by an unrelated panic
544/// can't escalate a rollback (the Drop-time `restore_savepoint`) — or the very
545/// next write transaction's `create_savepoint` — into a process-wide abort.
546pub(crate) fn read_or_recover<T>(lock: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
547    match lock.read() {
548        Ok(guard) => guard,
549        Err(poisoned) => {
550            let guard = poisoned.into_inner();
551            lock.clear_poison();
552            guard
553        }
554    }
555}
556
557/// Write-guard counterpart of [`read_or_recover`] (also clears the poison).
558pub(crate) fn write_or_recover<T>(lock: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
559    match lock.write() {
560        Ok(guard) => guard,
561        Err(poisoned) => {
562            let guard = poisoned.into_inner();
563            lock.clear_poison();
564            guard
565        }
566    }
567}
568
569pub(crate) fn delete_from_backward_junction(
570    junction: &RwLock<HashMap<EntityId, Vec<EntityId>>>,
571    id: &EntityId,
572) {
573    let mut jn = write_or_recover(junction);
574    let keys: Vec<EntityId> = jn.keys().copied().collect();
575    for k in keys {
576        // A let-chain rather than nested ifs: clippy's `collapsible_if` rejects
577        // the nested form under `-D warnings`, and the generated crates are
578        // edition 2024, so the chain is available.
579        if let Some(right_ids) = jn.get(&k)
580            && right_ids.contains(id)
581        {
582            let filtered: Vec<EntityId> =
583                right_ids.iter().copied().filter(|eid| eid != id).collect();
584            jn.insert(k, filtered);
585        }
586    }
587}
588
589pub(crate) fn junction_get(
590    junction: &RwLock<HashMap<EntityId, Vec<EntityId>>>,
591    id: &EntityId,
592) -> Vec<EntityId> {
593    read_or_recover(junction)
594        .get(id)
595        .cloned()
596        .unwrap_or_default()
597}
598
599pub(crate) fn junction_set(
600    junction: &RwLock<HashMap<EntityId, Vec<EntityId>>>,
601    id: EntityId,
602    ids: Vec<EntityId>,
603) {
604    write_or_recover(junction).insert(id, ids);
605}
606
607pub(crate) fn junction_remove(junction: &RwLock<HashMap<EntityId, Vec<EntityId>>>, id: &EntityId) {
608    write_or_recover(junction).remove(id);
609}
610
611pub(crate) fn junction_get_relationships_from_right_ids(
612    junction: &RwLock<HashMap<EntityId, Vec<EntityId>>>,
613    right_ids: &[EntityId],
614) -> Vec<(EntityId, Vec<EntityId>)> {
615    let jn = read_or_recover(junction);
616    jn.iter()
617        .filter(|(_, rids)| right_ids.iter().any(|eid| rids.contains(eid)))
618        .map(|(left_id, rids)| (*left_id, rids.clone()))
619        .collect()
620}
621
622pub(crate) fn junction_move_ids(
623    junction: &RwLock<HashMap<EntityId, Vec<EntityId>>>,
624    id: &EntityId,
625    ids_to_move: &[EntityId],
626    new_index: i32,
627) -> Vec<EntityId> {
628    let current = junction_get(junction, id);
629    if ids_to_move.is_empty() {
630        return current;
631    }
632    let move_set: std::collections::HashSet<EntityId> = ids_to_move.iter().copied().collect();
633    let mut remaining: Vec<EntityId> = current
634        .into_iter()
635        .filter(|eid| !move_set.contains(eid))
636        .collect();
637    let insert_pos = if new_index < 0 || (new_index as usize) > remaining.len() {
638        remaining.len()
639    } else {
640        new_index as usize
641    };
642    for (i, &eid) in ids_to_move.iter().enumerate() {
643        remaining.insert(insert_pos + i, eid);
644    }
645    junction_set(junction, *id, remaining.clone());
646    remaining
647}
648
649/// Surgically reconcile an external (out-of-scope) junction list during a scoped undo restore.
650///
651/// `live_list` is the current ordered value of an external referrer's junction row; `snap_list`
652/// is the same row at snapshot time; `scope` is the set of subtree ids being restored. Only the
653/// membership/position of `scope` ids is reconciled to match the snapshot — non-scope siblings are
654/// preserved in their current relative order. Scope ids absent from the snapshot are removed
655/// (undo-of-create); scope ids present in the snapshot are re-inserted at their snapshot-relative
656/// position (undo-of-remove). This is what keeps undo from clobbering other trunks' edits.
657pub(crate) fn reconcile_backref_list(
658    live_list: &[EntityId],
659    snap_list: &[EntityId],
660    scope: &std::collections::HashSet<EntityId>,
661) -> Vec<EntityId> {
662    // Scope ids that should be present after restore, in snapshot order.
663    let snap_members: Vec<EntityId> = snap_list
664        .iter()
665        .copied()
666        .filter(|x| scope.contains(x))
667        .collect();
668    let snap_member_set: std::collections::HashSet<EntityId> =
669        snap_members.iter().copied().collect();
670
671    // Start from the live list, dropping only scope ids that are NOT in the snapshot.
672    let mut work: Vec<EntityId> = live_list
673        .iter()
674        .copied()
675        // De Morgan'd from `!(in_scope && !in_snapshot)`: same rule, stated the
676        // way clippy's `nonminimal_bool` wants it, and arguably plainer — keep
677        // anything outside the scope, plus anything the snapshot still holds.
678        .filter(|x| !scope.contains(x) || snap_member_set.contains(x))
679        .collect();
680
681    // Re-insert missing scope ids at their snapshot-relative position (count of snapshot
682    // predecessors that survive in `work`), so they return next to their original neighbours.
683    for id in &snap_members {
684        if work.contains(id) {
685            continue;
686        }
687        let snap_idx = snap_list.iter().position(|x| x == id).unwrap_or(0);
688        let pos = snap_list[..snap_idx]
689            .iter()
690            .filter(|x| work.contains(x))
691            .count()
692            .min(work.len());
693        work.insert(pos, *id);
694    }
695    work
696}
697
698// ═════════════════════════════════════════════════════════════════════════════
699// Macros for entities WITH relationships (reduces boilerplate)
700// ═════════════════════════════════════════════════════════════════════════════
701
702/// Generate standard relationship methods for a HashMap table implementation.
703#[macro_export]
704macro_rules! impl_relationship_methods {
705    ($table_type:ty, $field_enum:ty) => {
706        fn get_relationship(
707            &self,
708            id: &$crate::types::EntityId,
709            field: &$field_enum,
710        ) -> Result<Vec<$crate::types::EntityId>, $crate::error::RepositoryError> {
711            Ok($crate::database::hashmap_store::junction_get(
712                self.resolve_junction(field),
713                id,
714            ))
715        }
716
717        fn get_relationship_many(
718            &self,
719            ids: &[$crate::types::EntityId],
720            field: &$field_enum,
721        ) -> Result<
722            std::collections::HashMap<$crate::types::EntityId, Vec<$crate::types::EntityId>>,
723            $crate::error::RepositoryError,
724        > {
725            let jn = self.resolve_junction(field);
726            let mut map = std::collections::HashMap::new();
727            for id in ids {
728                map.insert(*id, $crate::database::hashmap_store::junction_get(jn, id));
729            }
730            Ok(map)
731        }
732
733        fn get_relationship_count(
734            &self,
735            id: &$crate::types::EntityId,
736            field: &$field_enum,
737        ) -> Result<usize, $crate::error::RepositoryError> {
738            Ok(
739                $crate::database::hashmap_store::junction_get(self.resolve_junction(field), id)
740                    .len(),
741            )
742        }
743
744        fn get_relationship_in_range(
745            &self,
746            id: &$crate::types::EntityId,
747            field: &$field_enum,
748            offset: usize,
749            limit: usize,
750        ) -> Result<Vec<$crate::types::EntityId>, $crate::error::RepositoryError> {
751            let all =
752                $crate::database::hashmap_store::junction_get(self.resolve_junction(field), id);
753            Ok(all.into_iter().skip(offset).take(limit).collect())
754        }
755
756        fn get_relationships_from_right_ids(
757            &self,
758            field: &$field_enum,
759            right_ids: &[$crate::types::EntityId],
760        ) -> Result<
761            Vec<($crate::types::EntityId, Vec<$crate::types::EntityId>)>,
762            $crate::error::RepositoryError,
763        > {
764            Ok(
765                $crate::database::hashmap_store::junction_get_relationships_from_right_ids(
766                    self.resolve_junction(field),
767                    right_ids,
768                ),
769            )
770        }
771    };
772}
773
774#[macro_export]
775macro_rules! impl_write_relationship_methods {
776    ($table_type:ty, $field_enum:ty) => {
777        $crate::impl_relationship_methods!($table_type, $field_enum);
778
779        fn set_relationship_multi(
780            &mut self,
781            field: &$field_enum,
782            relationships: Vec<($crate::types::EntityId, Vec<$crate::types::EntityId>)>,
783        ) -> Result<(), $crate::error::RepositoryError> {
784            let jn = self.resolve_junction(field);
785            for (left_id, entities) in relationships {
786                $crate::database::hashmap_store::junction_set(jn, left_id, entities);
787            }
788            Ok(())
789        }
790
791        fn set_relationship(
792            &mut self,
793            id: &$crate::types::EntityId,
794            field: &$field_enum,
795            right_ids: &[$crate::types::EntityId],
796        ) -> Result<(), $crate::error::RepositoryError> {
797            $crate::database::hashmap_store::junction_set(
798                self.resolve_junction(field),
799                *id,
800                right_ids.to_vec(),
801            );
802            Ok(())
803        }
804
805        fn move_relationship_ids(
806            &mut self,
807            id: &$crate::types::EntityId,
808            field: &$field_enum,
809            ids_to_move: &[$crate::types::EntityId],
810            new_index: i32,
811        ) -> Result<Vec<$crate::types::EntityId>, $crate::error::RepositoryError> {
812            Ok($crate::database::hashmap_store::junction_move_ids(
813                self.resolve_junction(field),
814                id,
815                ids_to_move,
816                new_index,
817            ))
818        }
819    };
820}
821
822// ═════════════════════════════════════════════════════════════════════════════
823// Leaf entity macro (entities without forward relationships)
824// ═════════════════════════════════════════════════════════════════════════════
825
826#[macro_export]
827macro_rules! impl_leaf_entity_table {
828    (
829        entity: $Entity:ident,
830        entity_name: $entity_name:expr,
831        store_field: $store_field:ident,
832        table_trait: $TableTrait:ident,
833        table_ro_trait: $TableROTrait:ident,
834        table_struct: $TableStruct:ident,
835        table_ro_struct: $TableROStruct:ident,
836        backward_junctions: [ $( ($bj_field:ident) ),* $(,)? ],
837    ) => {
838        pub struct $TableStruct<'a> {
839            store: &'a $crate::database::hashmap_store::HashMapStore,
840        }
841
842        impl<'a> $TableStruct<'a> {
843            pub fn new(store: &'a $crate::database::hashmap_store::HashMapStore) -> Self {
844                Self { store }
845            }
846        }
847
848        impl<'a> $TableTrait for $TableStruct<'a> {
849            fn create(&mut self, entity: &$Entity) -> Result<$Entity, $crate::error::RepositoryError> {
850                self.create_multi(std::slice::from_ref(entity))
851                    .and_then(|v| {
852                        v.into_iter().next().ok_or_else(|| {
853                            $crate::error::RepositoryError::Other(::anyhow::anyhow!(
854                                "create: the store returned no row for the entity it was given"
855                            ))
856                        })
857                    })
858            }
859
860            fn create_multi(&mut self, entities: &[$Entity]) -> Result<Vec<$Entity>, $crate::error::RepositoryError> {
861                let mut created = Vec::with_capacity(entities.len());
862                let mut map = $crate::database::hashmap_store::write_or_recover(&self.store.$store_field);
863
864                for entity in entities {
865                    let new_entity = if entity.id == $crate::types::EntityId::default() {
866                        let id = self.store.next_id($entity_name);
867                        $Entity {
868                            id,
869                            ..entity.clone()
870                        }
871                    } else {
872                        if map.contains_key(&entity.id) {
873                            return Err($crate::error::RepositoryError::DuplicateId {
874                                entity: stringify!($Entity),
875                                id: entity.id,
876                            });
877                        }
878                        entity.clone()
879                    };
880
881                    map.insert(new_entity.id, new_entity.clone());
882                    created.push(new_entity);
883                }
884                Ok(created)
885            }
886
887            fn get(&self, id: &$crate::types::EntityId) -> Result<Option<$Entity>, $crate::error::RepositoryError> {
888                Ok($crate::database::hashmap_store::read_or_recover(&self.store.$store_field).get(id).cloned())
889            }
890
891            fn get_multi(&self, ids: &[$crate::types::EntityId]) -> Result<Vec<Option<$Entity>>, $crate::error::RepositoryError> {
892                let map = $crate::database::hashmap_store::read_or_recover(&self.store.$store_field);
893                Ok(ids.iter().map(|id| map.get(id).cloned()).collect())
894            }
895
896            fn get_all(&self) -> Result<Vec<$Entity>, $crate::error::RepositoryError> {
897                Ok($crate::database::hashmap_store::read_or_recover(&self.store.$store_field).values().cloned().collect())
898            }
899
900            fn update(&mut self, entity: &$Entity) -> Result<$Entity, $crate::error::RepositoryError> {
901                self.update_multi(std::slice::from_ref(entity))
902                    .and_then(|v| {
903                        v.into_iter().next().ok_or_else(|| {
904                            $crate::error::RepositoryError::Other(::anyhow::anyhow!(
905                                "update: the store returned no row for the entity it was given"
906                            ))
907                        })
908                    })
909            }
910
911            fn update_multi(&mut self, entities: &[$Entity]) -> Result<Vec<$Entity>, $crate::error::RepositoryError> {
912                let mut map = $crate::database::hashmap_store::write_or_recover(&self.store.$store_field);
913                let mut result = Vec::with_capacity(entities.len());
914                for entity in entities {
915                    map.insert(entity.id, entity.clone());
916                    result.push(entity.clone());
917                }
918                Ok(result)
919            }
920
921            fn update_with_relationships(&mut self, entity: &$Entity) -> Result<$Entity, $crate::error::RepositoryError> {
922                self.update(entity)
923            }
924
925            fn update_with_relationships_multi(&mut self, entities: &[$Entity]) -> Result<Vec<$Entity>, $crate::error::RepositoryError> {
926                self.update_multi(entities)
927            }
928
929            fn remove(&mut self, id: &$crate::types::EntityId) -> Result<(), $crate::error::RepositoryError> {
930                self.remove_multi(std::slice::from_ref(id))
931            }
932
933            fn remove_multi(&mut self, ids: &[$crate::types::EntityId]) -> Result<(), $crate::error::RepositoryError> {
934                let mut map = $crate::database::hashmap_store::write_or_recover(&self.store.$store_field);
935                for id in ids {
936                    map.remove(id);
937                    $(
938                        $crate::database::hashmap_store::delete_from_backward_junction(&self.store.$bj_field, id);
939                    )*
940                }
941                Ok(())
942            }
943        }
944
945        pub struct $TableROStruct<'a> {
946            store: &'a $crate::database::hashmap_store::HashMapStore,
947        }
948
949        impl<'a> $TableROStruct<'a> {
950            pub fn new(store: &'a $crate::database::hashmap_store::HashMapStore) -> Self {
951                Self { store }
952            }
953        }
954
955        impl<'a> $TableROTrait for $TableROStruct<'a> {
956            fn get(&self, id: &$crate::types::EntityId) -> Result<Option<$Entity>, $crate::error::RepositoryError> {
957                Ok($crate::database::hashmap_store::read_or_recover(&self.store.$store_field).get(id).cloned())
958            }
959
960            fn get_multi(&self, ids: &[$crate::types::EntityId]) -> Result<Vec<Option<$Entity>>, $crate::error::RepositoryError> {
961                let map = $crate::database::hashmap_store::read_or_recover(&self.store.$store_field);
962                Ok(ids.iter().map(|id| map.get(id).cloned()).collect())
963            }
964
965            fn get_all(&self) -> Result<Vec<$Entity>, $crate::error::RepositoryError> {
966                Ok($crate::database::hashmap_store::read_or_recover(&self.store.$store_field).values().cloned().collect())
967            }
968        }
969    };
970}