Skip to main content

icydb_core/db/
dynamic_write.rs

1//! Module: db::dynamic_write
2//! Responsibility: entity-name-driven structural write requests and results.
3//! Does not own: accepted policy resolution, row encoding, or commit execution.
4//! Boundary: public dynamic intent is lowered once by the session write owner.
5
6use crate::{
7    error::InternalError,
8    value::{InputValue, OutputValue},
9};
10use candid::CandidType;
11use icydb_schema::ScalarType;
12use serde::Deserialize;
13use std::collections::BTreeSet;
14
15///
16/// DynamicWriteCell
17///
18/// One structural field-write intent crossing the facade-to-core boundary.
19/// Omission remains distinct from an explicit default request, `NULL`, and an
20/// authored value until accepted write policy resolves the final after-image.
21///
22
23#[doc(hidden)]
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub enum DynamicWriteCell {
26    /// Supply no authored value for this field.
27    Omitted,
28    /// Explicitly request the accepted database default.
29    Default,
30    /// Explicitly author a nullable value.
31    Null,
32    /// Author one concrete public input value.
33    Value(InputValue),
34}
35
36///
37/// DynamicStructuralPatch
38///
39/// Field-name-driven structural patch consumed by the accepted write lane.
40/// Field names are resolved against the selected accepted snapshot; this type
41/// carries no physical slots or generated-model ordering.
42///
43
44#[doc(hidden)]
45#[derive(Clone, Debug, Default, Eq, PartialEq)]
46pub struct DynamicStructuralPatch {
47    fields: Vec<(String, DynamicWriteCell)>,
48}
49
50impl DynamicStructuralPatch {
51    /// Build one field-name-driven structural patch.
52    #[must_use]
53    pub const fn new(fields: Vec<(String, DynamicWriteCell)>) -> Self {
54        Self { fields }
55    }
56
57    /// Borrow the authored field intents in caller order.
58    #[must_use]
59    pub const fn fields(&self) -> &[(String, DynamicWriteCell)] {
60        self.fields.as_slice()
61    }
62}
63
64///
65/// DynamicMutation
66///
67/// One entity-name-driven structural mutation request.
68/// Variant shape owns row-existence and key requirements so callers cannot
69/// combine an insert-only identity mode with update/delete semantics.
70///
71
72#[doc(hidden)]
73#[derive(Clone, Debug, Eq, PartialEq)]
74pub enum DynamicMutation {
75    /// Insert one row, resolving its identity from the accepted after-image.
76    Insert {
77        /// Accepted entity display name.
78        entity: String,
79        /// Authored insert intent.
80        patch: DynamicStructuralPatch,
81    },
82    /// Patch one existing row selected by its public primary-key value.
83    Update {
84        /// Accepted entity display name.
85        entity: String,
86        /// Scalar or composite primary-key value.
87        key: InputValue,
88        /// Authored patch intent.
89        patch: DynamicStructuralPatch,
90    },
91    /// Replace one row, inserting when the selected key does not yet exist.
92    Replace {
93        /// Accepted entity display name.
94        entity: String,
95        /// Scalar or composite primary-key value.
96        key: InputValue,
97        /// Authored replacement intent.
98        patch: DynamicStructuralPatch,
99    },
100    /// Delete one existing row selected by its public primary-key value.
101    Delete {
102        /// Accepted entity display name.
103        entity: String,
104        /// Scalar or composite primary-key value.
105        key: InputValue,
106    },
107}
108
109impl DynamicMutation {
110    /// Borrow the accepted entity display name selected by this request.
111    #[must_use]
112    pub const fn entity(&self) -> &str {
113        match self {
114            Self::Insert { entity, .. }
115            | Self::Update { entity, .. }
116            | Self::Replace { entity, .. }
117            | Self::Delete { entity, .. } => entity.as_str(),
118        }
119    }
120}
121
122///
123/// DynamicMutationResult
124///
125/// Row-oriented result from one accepted-schema-driven structural mutation.
126///
127
128#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
129pub struct DynamicMutationResult {
130    /// Accepted entity name used for the mutation.
131    pub entity: String,
132    /// Complete accepted output-column names in row order.
133    pub columns: Vec<String>,
134    /// Canonical row values produced or removed by the mutation.
135    pub rows: Vec<Vec<OutputValue>>,
136    /// Number of rows whose logical or physical state changed.
137    pub affected_rows: u32,
138}
139
140///
141/// DynamicTypedFieldBindingRequest
142///
143/// Generated logical field contract supplied only while issuing an opaque
144/// accepted adapter binding.
145///
146
147#[doc(hidden)]
148#[derive(Clone, Debug, Eq, PartialEq)]
149pub struct DynamicTypedFieldBindingRequest {
150    pub(crate) field_type: DynamicTypedFieldType,
151    pub(crate) nullable: bool,
152    pub(crate) source_key: String,
153}
154
155impl DynamicTypedFieldBindingRequest {
156    /// Construct one generated field binding request.
157    #[must_use]
158    pub const fn new(
159        source_key: String,
160        field_type: DynamicTypedFieldType,
161        nullable: bool,
162    ) -> Self {
163        Self {
164            field_type,
165            nullable,
166            source_key,
167        }
168    }
169}
170
171/// Logical generated field shape used only for accepted compatibility checks.
172#[doc(hidden)]
173#[derive(Clone, Debug, Eq, PartialEq)]
174pub enum DynamicTypedFieldType {
175    /// Exact schema-owned scalar contract.
176    Scalar(ScalarType),
177    /// Ordered repeated values with one exact item contract.
178    List(Box<Self>),
179    /// Named contract selected by immutable source key.
180    Named(String),
181}
182
183/// Typed binding issuance failure before an opaque binding exists.
184#[doc(hidden)]
185#[derive(Debug)]
186pub enum DynamicTypedBindingError {
187    /// A requested immutable source identity is unavailable.
188    FieldUnavailable,
189    /// The requested logical field contract disagrees with accepted authority.
190    IncompatibleField,
191    /// Accepted database inspection failed.
192    Internal(InternalError),
193}
194
195impl From<InternalError> for DynamicTypedBindingError {
196    fn from(error: InternalError) -> Self {
197        Self::Internal(error)
198    }
199}
200
201#[derive(Clone, Debug, Eq, PartialEq)]
202struct DynamicTypedFieldBinding {
203    source_key: String,
204    field_id: u32,
205    slot: u16,
206    label: String,
207}
208
209///
210/// DynamicTypedStructuralPatch
211///
212/// Opaque accepted-ID/slot patch produced by a current typed binding.
213///
214
215#[doc(hidden)]
216#[derive(Clone, Debug, Default, Eq, PartialEq)]
217pub struct DynamicTypedStructuralPatch {
218    entity_source: String,
219    entity_tag: u64,
220    accepted_fingerprint: [u8; 16],
221    fields: Vec<(u32, u16, DynamicWriteCell)>,
222}
223
224impl DynamicTypedStructuralPatch {
225    /// Borrow accepted field ID/slot intents for core mutation lowering.
226    #[must_use]
227    pub(crate) const fn fields(&self) -> &[(u32, u16, DynamicWriteCell)] {
228        self.fields.as_slice()
229    }
230
231    pub(crate) fn is_bound_to(&self, binding: &DynamicTypedEntityBinding) -> bool {
232        self.entity_source == binding.entity_source
233            && self.entity_tag == binding.entity_tag
234            && self.accepted_fingerprint == binding.accepted_fingerprint
235    }
236}
237
238///
239/// DynamicTypedMutation
240///
241/// One source-bound generated mutation whose fields already carry accepted
242/// field IDs and slots. Entity and field names are not routing authority.
243///
244
245#[doc(hidden)]
246#[derive(Clone, Debug, Eq, PartialEq)]
247pub enum DynamicTypedMutation {
248    /// Insert one accepted row.
249    Insert {
250        /// Bound authored field intents.
251        patch: DynamicTypedStructuralPatch,
252    },
253    /// Patch one accepted row.
254    Update {
255        /// Scalar or composite public primary key.
256        key: InputValue,
257        /// Bound authored field intents.
258        patch: DynamicTypedStructuralPatch,
259    },
260    /// Replace one accepted row, inserting it when absent.
261    Replace {
262        /// Scalar or composite public primary key.
263        key: InputValue,
264        /// Bound authored field intents.
265        patch: DynamicTypedStructuralPatch,
266    },
267}
268
269/// Opaque accepted-schema identity issued for one generated typed adapter.
270///
271/// Public facade code may retain and return this value, but its accepted field
272/// mapping remains private to IcyDB.
273#[doc(hidden)]
274#[derive(Clone, Debug, Eq, PartialEq)]
275pub struct DynamicTypedEntityBinding {
276    pub(crate) database_incarnation: [u8; 16],
277    pub(crate) entity_source: String,
278    pub(crate) entity_label: String,
279    pub(crate) entity_tag: u64,
280    pub(crate) accepted_revision: u64,
281    pub(crate) accepted_fingerprint: [u8; 16],
282    pub(crate) entity_generation: u32,
283    fields: Vec<DynamicTypedFieldBinding>,
284    pub(crate) named_types: Vec<(String, String)>,
285    pub(crate) enum_variants: Vec<(String, String, String)>,
286    pub(crate) composite_fields: Vec<(String, String, String)>,
287}
288
289impl DynamicTypedEntityBinding {
290    #[expect(
291        clippy::too_many_arguments,
292        reason = "the opaque binding keeps every accepted authority component explicit"
293    )]
294    pub(crate) fn new(
295        database_incarnation: [u8; 16],
296        entity_source: String,
297        entity_label: String,
298        entity_tag: u64,
299        accepted_revision: u64,
300        accepted_fingerprint: [u8; 16],
301        entity_generation: u32,
302        fields: Vec<(String, u32, u16, String)>,
303        named_types: Vec<(String, String)>,
304        enum_variants: Vec<(String, String, String)>,
305        composite_fields: Vec<(String, String, String)>,
306    ) -> Result<Self, InternalError> {
307        let mut sources = BTreeSet::new();
308        let mut ids = BTreeSet::new();
309        let mut slots = BTreeSet::new();
310        let fields = fields
311            .into_iter()
312            .map(|(source_key, field_id, slot, label)| {
313                if !sources.insert(source_key.clone())
314                    || !ids.insert(field_id)
315                    || !slots.insert(slot)
316                {
317                    return Err(InternalError::store_invariant());
318                }
319                Ok(DynamicTypedFieldBinding {
320                    source_key,
321                    field_id,
322                    slot,
323                    label,
324                })
325            })
326            .collect::<Result<Vec<_>, _>>()?;
327
328        Ok(Self {
329            database_incarnation,
330            entity_source,
331            entity_label,
332            entity_tag,
333            accepted_revision,
334            accepted_fingerprint,
335            entity_generation,
336            fields,
337            named_types,
338            enum_variants,
339            composite_fields,
340        })
341    }
342
343    /// Borrow the accepted entity display label.
344    #[must_use]
345    pub const fn entity(&self) -> &str {
346        self.entity_label.as_str()
347    }
348
349    /// Borrow the immutable entity source identity.
350    #[must_use]
351    pub const fn entity_source(&self) -> &str {
352        self.entity_source.as_str()
353    }
354
355    /// Resolve one immutable field source key directly to its accepted slot.
356    #[must_use]
357    pub fn field_slot(&self, source_key: &str) -> Option<u16> {
358        self.fields
359            .iter()
360            .find_map(|field| (field.source_key == source_key).then_some(field.slot))
361    }
362
363    /// Resolve one accepted output label to its binding-owned accepted slot.
364    #[must_use]
365    pub fn output_field_slot(&self, label: &str) -> Option<u16> {
366        self.fields
367            .iter()
368            .find_map(|field| (field.label == label).then_some(field.slot))
369    }
370
371    pub(crate) fn field_identity_bindings(&self) -> impl Iterator<Item = (&str, u32, u16)> {
372        self.fields
373            .iter()
374            .map(|field| (field.source_key.as_str(), field.field_id, field.slot))
375    }
376
377    /// Bind generated source-key write intent to accepted field IDs and slots.
378    #[must_use]
379    pub fn bind_write_fields(
380        &self,
381        fields: Vec<(String, DynamicWriteCell)>,
382    ) -> Option<DynamicTypedStructuralPatch> {
383        let mut seen_ids = BTreeSet::new();
384        let mut seen_slots = BTreeSet::new();
385        let mut bound = Vec::with_capacity(fields.len());
386        for (source_key, cell) in fields {
387            let field = self
388                .fields
389                .iter()
390                .find(|field| field.source_key == source_key)?;
391            if !seen_ids.insert(field.field_id) || !seen_slots.insert(field.slot) {
392                return None;
393            }
394            bound.push((field.field_id, field.slot, cell));
395        }
396        Some(DynamicTypedStructuralPatch {
397            entity_source: self.entity_source.clone(),
398            entity_tag: self.entity_tag,
399            accepted_fingerprint: self.accepted_fingerprint,
400            fields: bound,
401        })
402    }
403
404    /// Resolve one immutable named-type source key to its accepted display path.
405    #[must_use]
406    pub fn named_type_name(&self, source_key: &str) -> Option<&str> {
407        self.named_types
408            .iter()
409            .find_map(|(source, name)| (source == source_key).then_some(name.as_str()))
410    }
411
412    /// Resolve one immutable enum-variant source key to its accepted display name.
413    #[must_use]
414    pub fn enum_variant_name(&self, type_source_key: &str, source_key: &str) -> Option<&str> {
415        self.enum_variants
416            .iter()
417            .find_map(|(bound_type, source, name)| {
418                (bound_type == type_source_key && source == source_key).then_some(name.as_str())
419            })
420    }
421
422    /// Resolve one immutable record-member source key to its accepted display name.
423    #[must_use]
424    pub fn composite_field_name(&self, type_source_key: &str, source_key: &str) -> Option<&str> {
425        self.composite_fields
426            .iter()
427            .find_map(|(bound_type, source, name)| {
428                (bound_type == type_source_key && source == source_key).then_some(name.as_str())
429            })
430    }
431}