Skip to main content

icydb_core/db/session/
write.rs

1//! Module: db::session::write
2//! Responsibility: session-owned typed write APIs for insert, replace, update,
3//! and structural mutation entrypoints over the shared save pipeline.
4//! Does not own: commit staging, mutation execution, or persistence encoding.
5//! Boundary: keeps public session write semantics above the executor save surface.
6
7#[cfg(test)]
8use crate::db::{DataStore, IndexStore};
9use crate::{
10    db::{DbSession, PersistedRow, WriteBatchResponse, data::UpdatePatch, executor::MutationMode},
11    error::InternalError,
12    traits::{CanisterKind, EntityCreateInput, EntityValue},
13};
14
15impl<C: CanisterKind> DbSession<C> {
16    /// Insert one entity row.
17    pub fn insert<E>(&self, entity: E) -> Result<E, InternalError>
18    where
19        E: PersistedRow<Canister = C> + EntityValue,
20    {
21        self.execute_save_entity(|save| save.insert(entity))
22    }
23
24    /// Insert one authored typed input.
25    pub fn create<I>(&self, input: I) -> Result<I::Entity, InternalError>
26    where
27        I: EntityCreateInput,
28        I::Entity: PersistedRow<Canister = C> + EntityValue,
29    {
30        self.execute_save_entity(|save| save.create(input))
31    }
32
33    /// Insert a single-entity-type batch atomically in one commit window.
34    ///
35    /// If any item fails pre-commit validation, no row in the batch is persisted.
36    ///
37    /// This API is not a multi-entity transaction surface.
38    pub fn insert_many_atomic<E>(
39        &self,
40        entities: impl IntoIterator<Item = E>,
41    ) -> Result<WriteBatchResponse<E>, InternalError>
42    where
43        E: PersistedRow<Canister = C> + EntityValue,
44    {
45        self.execute_save_batch(|save| save.insert_many_atomic(entities))
46    }
47
48    /// Insert a batch with explicitly non-atomic semantics.
49    ///
50    /// WARNING: fail-fast and non-atomic. Earlier inserts may commit before an error.
51    pub fn insert_many_non_atomic<E>(
52        &self,
53        entities: impl IntoIterator<Item = E>,
54    ) -> Result<WriteBatchResponse<E>, InternalError>
55    where
56        E: PersistedRow<Canister = C> + EntityValue,
57    {
58        self.execute_save_batch(|save| save.insert_many_non_atomic(entities))
59    }
60
61    /// Replace one existing entity row.
62    pub fn replace<E>(&self, entity: E) -> Result<E, InternalError>
63    where
64        E: PersistedRow<Canister = C> + EntityValue,
65    {
66        self.execute_save_entity(|save| save.replace(entity))
67    }
68
69    /// Apply one structural mutation under one explicit write-mode contract.
70    ///
71    /// This is the public core session boundary for structural writes:
72    /// callers provide the key, field patch, and intended mutation mode, and
73    /// the session routes that through the shared structural mutation pipeline.
74    pub fn mutate_structural<E>(
75        &self,
76        key: E::Key,
77        patch: UpdatePatch,
78        mode: MutationMode,
79    ) -> Result<E, InternalError>
80    where
81        E: PersistedRow<Canister = C> + EntityValue,
82    {
83        self.execute_save_entity(|save| save.apply_structural_mutation(mode, key, patch))
84    }
85
86    /// Apply one structural replacement, inserting if missing.
87    ///
88    /// Replace semantics still do not inherit omitted fields from the old row.
89    /// Missing fields must materialize through explicit defaults or managed
90    /// field preflight, or the write fails closed.
91    #[cfg(test)]
92    pub(in crate::db) fn replace_structural<E>(
93        &self,
94        key: E::Key,
95        patch: UpdatePatch,
96    ) -> Result<E, InternalError>
97    where
98        E: PersistedRow<Canister = C> + EntityValue,
99    {
100        self.mutate_structural(key, patch, MutationMode::Replace)
101    }
102
103    /// Replace a single-entity-type batch atomically in one commit window.
104    ///
105    /// If any item fails pre-commit validation, no row in the batch is persisted.
106    ///
107    /// This API is not a multi-entity transaction surface.
108    pub fn replace_many_atomic<E>(
109        &self,
110        entities: impl IntoIterator<Item = E>,
111    ) -> Result<WriteBatchResponse<E>, InternalError>
112    where
113        E: PersistedRow<Canister = C> + EntityValue,
114    {
115        self.execute_save_batch(|save| save.replace_many_atomic(entities))
116    }
117
118    /// Replace a batch with explicitly non-atomic semantics.
119    ///
120    /// WARNING: fail-fast and non-atomic. Earlier replaces may commit before an error.
121    pub fn replace_many_non_atomic<E>(
122        &self,
123        entities: impl IntoIterator<Item = E>,
124    ) -> Result<WriteBatchResponse<E>, InternalError>
125    where
126        E: PersistedRow<Canister = C> + EntityValue,
127    {
128        self.execute_save_batch(|save| save.replace_many_non_atomic(entities))
129    }
130
131    /// Update one existing entity row.
132    pub fn update<E>(&self, entity: E) -> Result<E, InternalError>
133    where
134        E: PersistedRow<Canister = C> + EntityValue,
135    {
136        self.execute_save_entity(|save| save.update(entity))
137    }
138
139    /// Apply one structural insert from a patch-defined after-image.
140    ///
141    /// Insert semantics no longer require a pre-built full row image.
142    /// Missing fields still fail closed unless derive-owned materialization can
143    /// supply them through explicit defaults or managed-field preflight.
144    #[cfg(test)]
145    pub(in crate::db) fn insert_structural<E>(
146        &self,
147        key: E::Key,
148        patch: UpdatePatch,
149    ) -> Result<E, InternalError>
150    where
151        E: PersistedRow<Canister = C> + EntityValue,
152    {
153        self.mutate_structural(key, patch, MutationMode::Insert)
154    }
155
156    /// Apply one structural field patch to an existing entity row.
157    ///
158    /// This session-owned boundary keeps structural mutation out of the raw
159    /// executor surface while still routing through the same typed save
160    /// preflight before commit staging.
161    #[cfg(test)]
162    pub(in crate::db) fn update_structural<E>(
163        &self,
164        key: E::Key,
165        patch: UpdatePatch,
166    ) -> Result<E, InternalError>
167    where
168        E: PersistedRow<Canister = C> + EntityValue,
169    {
170        self.mutate_structural(key, patch, MutationMode::Update)
171    }
172
173    /// Update a single-entity-type batch atomically in one commit window.
174    ///
175    /// If any item fails pre-commit validation, no row in the batch is persisted.
176    ///
177    /// This API is not a multi-entity transaction surface.
178    pub fn update_many_atomic<E>(
179        &self,
180        entities: impl IntoIterator<Item = E>,
181    ) -> Result<WriteBatchResponse<E>, InternalError>
182    where
183        E: PersistedRow<Canister = C> + EntityValue,
184    {
185        self.execute_save_batch(|save| save.update_many_atomic(entities))
186    }
187
188    /// Update a batch with explicitly non-atomic semantics.
189    ///
190    /// WARNING: fail-fast and non-atomic. Earlier updates may commit before an error.
191    pub fn update_many_non_atomic<E>(
192        &self,
193        entities: impl IntoIterator<Item = E>,
194    ) -> Result<WriteBatchResponse<E>, InternalError>
195    where
196        E: PersistedRow<Canister = C> + EntityValue,
197    {
198        self.execute_save_batch(|save| save.update_many_non_atomic(entities))
199    }
200
201    /// TEST ONLY: clear all registered data and index stores for this database.
202    #[cfg(test)]
203    #[doc(hidden)]
204    pub fn clear_stores_for_tests(&self) {
205        self.db.with_store_registry(|reg| {
206            // Test cleanup only: clearing all stores is set-like and does not
207            // depend on registry iteration order.
208            for (_, store) in reg.iter() {
209                store.with_data_mut(DataStore::clear);
210                store.with_index_mut(IndexStore::clear);
211            }
212        });
213    }
214}