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
7use crate::{
8    db::{
9        DbSession, PersistedRow, WriteBatchResponse,
10        data::{FieldSlot, StructuralPatch},
11        executor::{EntityAuthority, MutationMode},
12        schema::AcceptedRowLayoutRuntimeDescriptor,
13    },
14    error::InternalError,
15    traits::{CanisterKind, EntityCreateInput, EntityValue},
16    value::Value,
17};
18
19// Append one session-resolved structural field update. The caller passes the
20// accepted runtime descriptor that already crossed schema reconciliation, so
21// field-name lookup follows persisted row-layout metadata rather than generated
22// declaration order.
23fn append_accepted_structural_patch_field(
24    entity_path: &'static str,
25    descriptor: &AcceptedRowLayoutRuntimeDescriptor<'_>,
26    patch: StructuralPatch,
27    field_name: &str,
28    value: Value,
29) -> Result<StructuralPatch, InternalError> {
30    let slot = descriptor
31        .field_slot_index_by_name(field_name)
32        .ok_or_else(|| InternalError::mutation_structural_field_unknown(entity_path, field_name))?;
33
34    Ok(patch.set(FieldSlot::from_validated_index(slot), value))
35}
36
37impl<C: CanisterKind> DbSession<C> {
38    /// Insert one entity row.
39    pub fn insert<E>(&self, entity: E) -> Result<E, InternalError>
40    where
41        E: PersistedRow<Canister = C> + EntityValue,
42    {
43        self.execute_save_entity(|save| save.insert(entity))
44    }
45
46    /// Insert one authored typed input.
47    pub fn create<I>(&self, input: I) -> Result<I::Entity, InternalError>
48    where
49        I: EntityCreateInput,
50        I::Entity: PersistedRow<Canister = C> + EntityValue,
51    {
52        self.execute_save_entity(|save| save.create(input))
53    }
54
55    /// Insert a single-entity-type batch atomically in one commit window.
56    ///
57    /// If any item fails pre-commit validation, no row in the batch is persisted.
58    ///
59    /// This API is not a multi-entity transaction surface.
60    pub fn insert_many_atomic<E>(
61        &self,
62        entities: impl IntoIterator<Item = E>,
63    ) -> Result<WriteBatchResponse<E>, InternalError>
64    where
65        E: PersistedRow<Canister = C> + EntityValue,
66    {
67        self.execute_save_batch(|save| save.insert_many_atomic(entities))
68    }
69
70    /// Insert a batch with explicitly non-atomic semantics.
71    ///
72    /// WARNING: fail-fast and non-atomic. Earlier inserts may commit before an error.
73    pub fn insert_many_non_atomic<E>(
74        &self,
75        entities: impl IntoIterator<Item = E>,
76    ) -> Result<WriteBatchResponse<E>, InternalError>
77    where
78        E: PersistedRow<Canister = C> + EntityValue,
79    {
80        self.execute_save_batch(|save| save.insert_many_non_atomic(entities))
81    }
82
83    /// Replace one existing entity row.
84    pub fn replace<E>(&self, entity: E) -> Result<E, InternalError>
85    where
86        E: PersistedRow<Canister = C> + EntityValue,
87    {
88        self.execute_save_entity(|save| save.replace(entity))
89    }
90
91    /// Apply one structural mutation under one explicit write-mode contract.
92    ///
93    /// This is the public core session boundary for structural writes:
94    /// callers provide the key, field patch, and intended mutation mode, and
95    /// the session routes that through the shared structural mutation pipeline.
96    pub fn mutate_structural<E>(
97        &self,
98        key: E::Key,
99        patch: StructuralPatch,
100        mode: MutationMode,
101    ) -> Result<E, InternalError>
102    where
103        E: PersistedRow<Canister = C> + EntityValue,
104    {
105        self.execute_save_entity(|save| save.apply_structural_mutation(mode, key, patch))
106    }
107
108    /// Build one structural patch through the accepted schema row layout.
109    ///
110    /// This is the session-owned patch construction boundary for callers that
111    /// can provide all dynamic field updates at once. It resolves field names
112    /// through the accepted row-layout descriptor before the patch reaches the
113    /// generated-compatible write codec bridge.
114    pub fn structural_patch<E, I, S>(&self, fields: I) -> Result<StructuralPatch, InternalError>
115    where
116        E: PersistedRow<Canister = C> + EntityValue,
117        I: IntoIterator<Item = (S, Value)>,
118        S: AsRef<str>,
119    {
120        let (accepted_schema, _) =
121            self.ensure_accepted_schema_snapshot_and_authority(EntityAuthority::for_type::<E>())?;
122        let descriptor =
123            AcceptedRowLayoutRuntimeDescriptor::from_accepted_schema(&accepted_schema)?;
124        let mut patch = StructuralPatch::new();
125
126        // Phase 1: resolve every caller-provided field name against the
127        // accepted descriptor so public structural patch construction no
128        // longer has to choose slots from generated model field order.
129        for (field_name, value) in fields {
130            let field_name = field_name.as_ref();
131            patch = append_accepted_structural_patch_field(
132                E::PATH,
133                &descriptor,
134                patch,
135                field_name,
136                value,
137            )?;
138        }
139
140        Ok(patch)
141    }
142
143    /// Apply one structural replacement, inserting if missing.
144    ///
145    /// Replace semantics still do not inherit omitted fields from the old row.
146    /// Missing fields must materialize through explicit defaults or managed
147    /// field preflight, or the write fails closed.
148    #[cfg(test)]
149    pub(in crate::db) fn replace_structural<E>(
150        &self,
151        key: E::Key,
152        patch: StructuralPatch,
153    ) -> Result<E, InternalError>
154    where
155        E: PersistedRow<Canister = C> + EntityValue,
156    {
157        self.mutate_structural(key, patch, MutationMode::Replace)
158    }
159
160    /// Replace a single-entity-type batch atomically in one commit window.
161    ///
162    /// If any item fails pre-commit validation, no row in the batch is persisted.
163    ///
164    /// This API is not a multi-entity transaction surface.
165    pub fn replace_many_atomic<E>(
166        &self,
167        entities: impl IntoIterator<Item = E>,
168    ) -> Result<WriteBatchResponse<E>, InternalError>
169    where
170        E: PersistedRow<Canister = C> + EntityValue,
171    {
172        self.execute_save_batch(|save| save.replace_many_atomic(entities))
173    }
174
175    /// Replace a batch with explicitly non-atomic semantics.
176    ///
177    /// WARNING: fail-fast and non-atomic. Earlier replaces may commit before an error.
178    pub fn replace_many_non_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.replace_many_non_atomic(entities))
186    }
187
188    /// Update one existing entity row.
189    pub fn update<E>(&self, entity: E) -> Result<E, InternalError>
190    where
191        E: PersistedRow<Canister = C> + EntityValue,
192    {
193        self.execute_save_entity(|save| save.update(entity))
194    }
195
196    /// Apply one structural insert from a patch-defined after-image.
197    ///
198    /// Insert semantics no longer require a pre-built full row image.
199    /// Missing fields still fail closed unless derive-owned materialization can
200    /// supply them through explicit defaults or managed-field preflight.
201    #[cfg(test)]
202    pub(in crate::db) fn insert_structural<E>(
203        &self,
204        key: E::Key,
205        patch: StructuralPatch,
206    ) -> Result<E, InternalError>
207    where
208        E: PersistedRow<Canister = C> + EntityValue,
209    {
210        self.mutate_structural(key, patch, MutationMode::Insert)
211    }
212
213    /// Apply one structural field patch to an existing entity row.
214    ///
215    /// This session-owned boundary keeps structural mutation out of the raw
216    /// executor surface while still routing through the same typed save
217    /// preflight before commit staging.
218    #[cfg(test)]
219    pub(in crate::db) fn update_structural<E>(
220        &self,
221        key: E::Key,
222        patch: StructuralPatch,
223    ) -> Result<E, InternalError>
224    where
225        E: PersistedRow<Canister = C> + EntityValue,
226    {
227        self.mutate_structural(key, patch, MutationMode::Update)
228    }
229
230    /// Update a single-entity-type batch atomically in one commit window.
231    ///
232    /// If any item fails pre-commit validation, no row in the batch is persisted.
233    ///
234    /// This API is not a multi-entity transaction surface.
235    pub fn update_many_atomic<E>(
236        &self,
237        entities: impl IntoIterator<Item = E>,
238    ) -> Result<WriteBatchResponse<E>, InternalError>
239    where
240        E: PersistedRow<Canister = C> + EntityValue,
241    {
242        self.execute_save_batch(|save| save.update_many_atomic(entities))
243    }
244
245    /// Update a batch with explicitly non-atomic semantics.
246    ///
247    /// WARNING: fail-fast and non-atomic. Earlier updates may commit before an error.
248    pub fn update_many_non_atomic<E>(
249        &self,
250        entities: impl IntoIterator<Item = E>,
251    ) -> Result<WriteBatchResponse<E>, InternalError>
252    where
253        E: PersistedRow<Canister = C> + EntityValue,
254    {
255        self.execute_save_batch(|save| save.update_many_non_atomic(entities))
256    }
257}