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