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    /// Consume the authored field intents in caller order.
64    pub(crate) fn into_fields(self) -> Vec<(String, DynamicWriteCell)> {
65        self.fields
66    }
67}
68
69///
70/// DynamicMutation
71///
72/// One entity-name-driven structural mutation request.
73/// Variant shape owns row-existence and key requirements so callers cannot
74/// combine an insert-only identity mode with update/delete semantics.
75///
76
77#[doc(hidden)]
78#[derive(Clone, Debug, Eq, PartialEq)]
79pub enum DynamicMutation {
80    /// Insert one row, resolving its identity from the accepted after-image.
81    Insert {
82        /// Accepted entity display name.
83        entity: String,
84        /// Authored insert intent.
85        patch: DynamicStructuralPatch,
86    },
87    /// Patch one existing row selected by its public primary-key value.
88    Update {
89        /// Accepted entity display name.
90        entity: String,
91        /// Scalar or composite primary-key value.
92        key: InputValue,
93        /// Authored patch intent.
94        patch: DynamicStructuralPatch,
95    },
96    /// Replace one row, inserting when the selected key does not yet exist.
97    Replace {
98        /// Accepted entity display name.
99        entity: String,
100        /// Scalar or composite primary-key value.
101        key: InputValue,
102        /// Authored replacement intent.
103        patch: DynamicStructuralPatch,
104    },
105    /// Delete one existing row selected by its public primary-key value.
106    Delete {
107        /// Accepted entity display name.
108        entity: String,
109        /// Scalar or composite primary-key value.
110        key: InputValue,
111    },
112}
113
114impl DynamicMutation {
115    /// Borrow the accepted entity display name selected by this request.
116    #[must_use]
117    pub const fn entity(&self) -> &str {
118        match self {
119            Self::Insert { entity, .. }
120            | Self::Update { entity, .. }
121            | Self::Replace { entity, .. }
122            | Self::Delete { entity, .. } => entity.as_str(),
123        }
124    }
125}
126
127///
128/// DynamicMutationResult
129///
130/// Row-oriented result from one accepted-schema-driven structural mutation.
131///
132
133#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
134pub struct DynamicMutationResult {
135    /// Accepted entity name used for the mutation.
136    pub entity: String,
137    /// Complete accepted output-column names in row order.
138    pub columns: Vec<String>,
139    /// Canonical row values produced or removed by the mutation.
140    pub rows: Vec<Vec<OutputValue>>,
141    /// Number of rows whose logical or physical state changed.
142    pub affected_rows: u32,
143}
144
145impl DynamicMutationResult {
146    /// Return the number of row payloads carried by this result.
147    #[must_use]
148    pub const fn len(&self) -> usize {
149        self.rows.len()
150    }
151
152    /// Return whether this result carries no row payloads.
153    #[must_use]
154    pub const fn is_empty(&self) -> bool {
155        self.rows.is_empty()
156    }
157}
158
159/// Static logical field shape used only for accepted compatibility checks.
160#[doc(hidden)]
161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub enum TypedFieldType {
163    /// Exact schema-owned scalar contract.
164    Scalar(ScalarType),
165    /// Ordered repeated values with one exact item contract.
166    List(&'static Self),
167    /// Named contract selected by immutable source key.
168    Named(&'static str),
169}
170
171/// One generated static field descriptor supplied while issuing an opaque binding.
172#[doc(hidden)]
173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
174pub struct TypedFieldDescriptor {
175    pub(crate) field_type: TypedFieldType,
176    pub(crate) nullable: bool,
177    pub(crate) source_key: &'static str,
178}
179
180impl TypedFieldDescriptor {
181    /// Construct one generated static field descriptor.
182    #[must_use]
183    pub const fn new(source_key: &'static str, field_type: TypedFieldType, nullable: bool) -> Self {
184        Self {
185            field_type,
186            nullable,
187            source_key,
188        }
189    }
190}
191
192/// One generated static entity contract validated against accepted authority.
193#[doc(hidden)]
194#[derive(Clone, Copy, Debug, Eq, PartialEq)]
195pub struct TypedEntityDescriptor {
196    pub(crate) entity_source_key: &'static str,
197    pub(crate) fields: &'static [TypedFieldDescriptor],
198    pub(crate) primary_key_source_keys: &'static [&'static str],
199}
200
201impl TypedEntityDescriptor {
202    /// Construct one generated static entity descriptor.
203    #[must_use]
204    pub const fn new(
205        entity_source_key: &'static str,
206        primary_key_source_keys: &'static [&'static str],
207        fields: &'static [TypedFieldDescriptor],
208    ) -> Self {
209        Self {
210            entity_source_key,
211            fields,
212            primary_key_source_keys,
213        }
214    }
215}
216
217/// Typed binding issuance failure before an opaque binding exists.
218#[doc(hidden)]
219#[derive(Debug)]
220pub enum DynamicTypedBindingError {
221    /// A requested immutable source identity is unavailable.
222    FieldUnavailable,
223    /// The requested logical field contract disagrees with accepted authority.
224    IncompatibleField,
225    /// Accepted database inspection failed.
226    Internal(InternalError),
227}
228
229impl From<InternalError> for DynamicTypedBindingError {
230    fn from(error: InternalError) -> Self {
231        Self::Internal(error)
232    }
233}
234
235#[derive(Clone, Debug, Eq, PartialEq)]
236struct DynamicTypedFieldBinding {
237    source_key: String,
238    field_id: u32,
239    slot: u16,
240    label: String,
241}
242
243///
244/// DynamicTypedStructuralPatch
245///
246/// Opaque descriptor-ordinal patch sealed to one current typed binding.
247///
248
249#[doc(hidden)]
250#[derive(Clone, Debug, Default, Eq, PartialEq)]
251pub struct DynamicTypedStructuralPatch {
252    entity_source: String,
253    entity_tag: u64,
254    accepted_fingerprint: [u8; 16],
255    fields: Vec<(usize, DynamicWriteCell)>,
256}
257
258impl DynamicTypedStructuralPatch {
259    /// Borrow binding-local descriptor-ordinal intents for binding assertions.
260    #[cfg(test)]
261    #[must_use]
262    pub(crate) const fn fields(&self) -> &[(usize, DynamicWriteCell)] {
263        self.fields.as_slice()
264    }
265
266    /// Consume binding-local intents after the patch's binding is validated.
267    pub(crate) fn into_fields(self) -> Vec<(usize, DynamicWriteCell)> {
268        self.fields
269    }
270
271    pub(crate) fn is_bound_to(&self, binding: &DynamicTypedEntityBinding) -> bool {
272        self.entity_source == binding.entity_source
273            && self.entity_tag == binding.entity_tag
274            && self.accepted_fingerprint == binding.accepted_fingerprint
275    }
276}
277
278///
279/// DynamicTypedMutation
280///
281/// One source-bound generated mutation whose fields carry binding-local
282/// descriptor ordinals. Entity names, field names and ordinals are not routing
283/// authority; the sealed binding resolves accepted IDs and slots.
284///
285
286#[doc(hidden)]
287#[derive(Clone, Debug, Eq, PartialEq)]
288pub enum DynamicTypedMutation {
289    /// Insert one accepted row.
290    Insert {
291        /// Bound authored field intents.
292        patch: DynamicTypedStructuralPatch,
293    },
294    /// Patch one accepted row.
295    Update {
296        /// Scalar or composite public primary key.
297        key: InputValue,
298        /// Bound authored field intents.
299        patch: DynamicTypedStructuralPatch,
300    },
301    /// Replace one accepted row, inserting it when absent.
302    Replace {
303        /// Scalar or composite public primary key.
304        key: InputValue,
305        /// Bound authored field intents.
306        patch: DynamicTypedStructuralPatch,
307    },
308    /// Delete one accepted row.
309    Delete {
310        /// Scalar or composite public primary key.
311        key: InputValue,
312    },
313}
314
315/// Opaque accepted-schema identity issued for one generated typed adapter.
316///
317/// Public facade code may retain and return this value, but its accepted field
318/// mapping remains private to IcyDB.
319#[doc(hidden)]
320#[derive(Clone, Debug, Eq, PartialEq)]
321pub struct DynamicTypedEntityBinding {
322    pub(crate) database_incarnation: [u8; 16],
323    pub(crate) entity_source: String,
324    pub(crate) entity_label: String,
325    pub(crate) entity_tag: u64,
326    pub(crate) accepted_revision: u64,
327    pub(crate) accepted_fingerprint: [u8; 16],
328    pub(crate) entity_generation: u32,
329    fields: Vec<DynamicTypedFieldBinding>,
330    pub(crate) named_types: Vec<(String, String)>,
331    pub(crate) enum_variants: Vec<(String, String, String)>,
332    pub(crate) composite_fields: Vec<(String, String, String)>,
333}
334
335impl DynamicTypedEntityBinding {
336    #[expect(
337        clippy::too_many_arguments,
338        reason = "the opaque binding keeps every accepted authority component explicit"
339    )]
340    pub(crate) fn new(
341        database_incarnation: [u8; 16],
342        entity_source: String,
343        entity_label: String,
344        entity_tag: u64,
345        accepted_revision: u64,
346        accepted_fingerprint: [u8; 16],
347        entity_generation: u32,
348        fields: Vec<(String, u32, u16, String)>,
349        named_types: Vec<(String, String)>,
350        enum_variants: Vec<(String, String, String)>,
351        composite_fields: Vec<(String, String, String)>,
352    ) -> Result<Self, InternalError> {
353        let mut sources = BTreeSet::new();
354        let mut ids = BTreeSet::new();
355        let mut slots = BTreeSet::new();
356        let fields = fields
357            .into_iter()
358            .map(|(source_key, field_id, slot, label)| {
359                if !sources.insert(source_key.clone())
360                    || !ids.insert(field_id)
361                    || !slots.insert(slot)
362                {
363                    return Err(InternalError::store_invariant());
364                }
365                Ok(DynamicTypedFieldBinding {
366                    source_key,
367                    field_id,
368                    slot,
369                    label,
370                })
371            })
372            .collect::<Result<Vec<_>, _>>()?;
373
374        Ok(Self {
375            database_incarnation,
376            entity_source,
377            entity_label,
378            entity_tag,
379            accepted_revision,
380            accepted_fingerprint,
381            entity_generation,
382            fields,
383            named_types,
384            enum_variants,
385            composite_fields,
386        })
387    }
388
389    /// Borrow the accepted entity display label.
390    #[must_use]
391    pub const fn entity(&self) -> &str {
392        self.entity_label.as_str()
393    }
394
395    /// Borrow the immutable entity source identity.
396    #[must_use]
397    pub const fn entity_source(&self) -> &str {
398        self.entity_source.as_str()
399    }
400
401    /// Resolve one immutable field source key directly to its accepted slot.
402    #[must_use]
403    pub fn field_slot(&self, source_key: &str) -> Option<u16> {
404        self.fields
405            .iter()
406            .find_map(|field| (field.source_key == source_key).then_some(field.slot))
407    }
408
409    /// Resolve one accepted output label to its binding-owned accepted slot.
410    #[must_use]
411    pub fn output_field_slot(&self, label: &str) -> Option<u16> {
412        self.fields
413            .iter()
414            .find_map(|field| (field.label == label).then_some(field.slot))
415    }
416
417    pub(crate) fn field_identity_bindings(&self) -> impl Iterator<Item = (&str, u32, u16)> {
418        self.fields
419            .iter()
420            .map(|field| (field.source_key.as_str(), field.field_id, field.slot))
421    }
422
423    pub(crate) fn field_identity_binding(&self, descriptor_ordinal: usize) -> Option<(u32, u16)> {
424        self.fields
425            .get(descriptor_ordinal)
426            .map(|field| (field.field_id, field.slot))
427    }
428
429    /// Bind generated descriptor-ordinal write intent to accepted field IDs and slots.
430    #[must_use]
431    pub fn bind_write_ordinals(
432        &self,
433        fields: Vec<(usize, DynamicWriteCell)>,
434    ) -> Option<DynamicTypedStructuralPatch> {
435        let mut previous_ordinal = None;
436        for (descriptor_ordinal, _) in &fields {
437            if previous_ordinal.is_some_and(|previous| *descriptor_ordinal <= previous) {
438                return None;
439            }
440            let _ = self.fields.get(*descriptor_ordinal)?;
441            previous_ordinal = Some(*descriptor_ordinal);
442        }
443        Some(DynamicTypedStructuralPatch {
444            entity_source: self.entity_source.clone(),
445            entity_tag: self.entity_tag,
446            accepted_fingerprint: self.accepted_fingerprint,
447            fields,
448        })
449    }
450
451    /// Resolve one immutable named-type source key to its accepted display path.
452    #[must_use]
453    pub fn named_type_name(&self, source_key: &str) -> Option<&str> {
454        self.named_types
455            .iter()
456            .find_map(|(source, name)| (source == source_key).then_some(name.as_str()))
457    }
458
459    /// Resolve one immutable enum-variant source key to its accepted display name.
460    #[must_use]
461    pub fn enum_variant_name(&self, type_source_key: &str, source_key: &str) -> Option<&str> {
462        self.enum_variants
463            .iter()
464            .find_map(|(bound_type, source, name)| {
465                (bound_type == type_source_key && source == source_key).then_some(name.as_str())
466            })
467    }
468
469    /// Resolve one accepted enum-variant display name to its immutable source key.
470    #[must_use]
471    pub fn enum_variant_source_key(
472        &self,
473        type_source_key: &str,
474        accepted_name: &str,
475    ) -> Option<&str> {
476        self.enum_variants
477            .iter()
478            .find_map(|(bound_type, source, name)| {
479                (bound_type == type_source_key && name == accepted_name).then_some(source.as_str())
480            })
481    }
482
483    /// Resolve one immutable record-member source key to its accepted display name.
484    #[must_use]
485    pub fn composite_field_name(&self, type_source_key: &str, source_key: &str) -> Option<&str> {
486        self.composite_fields
487            .iter()
488            .find_map(|(bound_type, source, name)| {
489                (bound_type == type_source_key && source == source_key).then_some(name.as_str())
490            })
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::{DynamicMutationResult, DynamicTypedEntityBinding};
497
498    fn binding() -> DynamicTypedEntityBinding {
499        DynamicTypedEntityBinding::new(
500            [0; 16],
501            "EntitySource".to_string(),
502            "Entity".to_string(),
503            1,
504            1,
505            [1; 16],
506            1,
507            Vec::new(),
508            vec![("ChoiceSource".to_string(), "RenamedChoice".to_string())],
509            vec![
510                (
511                    "ChoiceSource".to_string(),
512                    "FirstSource".to_string(),
513                    "RenamedFirst".to_string(),
514                ),
515                (
516                    "ChoiceSource".to_string(),
517                    "SecondSource".to_string(),
518                    "Second".to_string(),
519                ),
520            ],
521            Vec::new(),
522        )
523        .expect("test binding should be internally consistent")
524    }
525
526    #[test]
527    fn accepted_enum_variant_name_resolves_to_immutable_source_key() {
528        let binding = binding();
529
530        assert_eq!(
531            binding.enum_variant_source_key("ChoiceSource", "RenamedFirst"),
532            Some("FirstSource"),
533        );
534        assert_eq!(
535            binding.enum_variant_source_key("ChoiceSource", "Second"),
536            Some("SecondSource"),
537        );
538        assert_eq!(
539            binding.enum_variant_source_key("OtherSource", "RenamedFirst"),
540            None,
541        );
542        assert_eq!(
543            binding.enum_variant_source_key("ChoiceSource", "FirstSource"),
544            None,
545        );
546    }
547
548    #[test]
549    fn dynamic_mutation_result_derives_cardinality_from_rows() {
550        let result = DynamicMutationResult {
551            entity: "Entity".to_string(),
552            columns: Vec::new(),
553            rows: vec![Vec::new()],
554            affected_rows: 1,
555        };
556
557        assert_eq!(result.len(), 1);
558        assert!(!result.is_empty());
559    }
560}