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::{MAX_SOURCE_KEY_BYTES, 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/// Bounded generated-source context for an unavailable accepted binding.
218///
219/// Only static descriptor keys are retained, never row values or a schema dump.
220/// Malformed oversized keys are represented by UTF-8 prefixes of at most
221/// `MAX_SOURCE_KEY_BYTES` bytes; valid source keys are retained in full.
222
223#[derive(Clone, Copy, Debug, Eq, PartialEq)]
224pub struct TypedBindingContext {
225    entity_source: &'static str,
226    field_source: Option<&'static str>,
227}
228
229impl TypedBindingContext {
230    pub(crate) fn new(entity_source: &'static str, field_source: Option<&'static str>) -> Self {
231        // Move back at most three bytes to a valid UTF-8 boundary. Zero is
232        // always a boundary, so oversized descriptors cannot underflow here.
233        let bounded = |source: &'static str| {
234            let mut end = source.len().min(MAX_SOURCE_KEY_BYTES);
235            while !source.is_char_boundary(end) {
236                end -= 1;
237            }
238            &source[..end]
239        };
240        Self {
241            entity_source: bounded(entity_source),
242            field_source: field_source.map(bounded),
243        }
244    }
245
246    /// Borrow the bounded entity source key supplied by the generated adapter.
247    #[must_use]
248    pub const fn entity_source(self) -> &'static str {
249        self.entity_source
250    }
251
252    /// Borrow the bounded field source key, or `None` for an entity lookup failure.
253    #[must_use]
254    pub const fn field_source(self) -> Option<&'static str> {
255        self.field_source
256    }
257}
258
259/// Typed binding issuance failure before an opaque binding exists.
260#[doc(hidden)]
261#[derive(Debug)]
262pub enum DynamicTypedBindingError {
263    /// The requested logical field contract disagrees with accepted authority.
264    IncompatibleField,
265
266    /// Accepted database inspection failed.
267    Internal(InternalError),
268
269    /// A generated entity or field source identity cannot bind to accepted authority.
270    SourceUnavailable(TypedBindingContext),
271}
272
273impl DynamicTypedBindingError {
274    pub(crate) fn source_unavailable(
275        entity_source: &'static str,
276        field_source: Option<&'static str>,
277    ) -> Self {
278        Self::SourceUnavailable(TypedBindingContext::new(entity_source, field_source))
279    }
280}
281
282impl From<InternalError> for DynamicTypedBindingError {
283    fn from(error: InternalError) -> Self {
284        Self::Internal(error)
285    }
286}
287
288#[derive(Clone, Debug, Eq, PartialEq)]
289struct DynamicTypedFieldBinding {
290    source_key: String,
291    field_id: u32,
292    slot: u16,
293    label: String,
294}
295
296///
297/// DynamicTypedStructuralPatch
298///
299/// Opaque descriptor-ordinal patch sealed to one current typed binding.
300///
301
302#[doc(hidden)]
303#[derive(Clone, Debug, Default, Eq, PartialEq)]
304pub struct DynamicTypedStructuralPatch {
305    entity_source: String,
306    entity_tag: u64,
307    accepted_fingerprint: [u8; 16],
308    fields: Vec<(usize, DynamicWriteCell)>,
309}
310
311impl DynamicTypedStructuralPatch {
312    /// Borrow binding-local descriptor-ordinal intents for binding assertions.
313    #[cfg(test)]
314    #[must_use]
315    pub(crate) const fn fields(&self) -> &[(usize, DynamicWriteCell)] {
316        self.fields.as_slice()
317    }
318
319    /// Consume binding-local intents after the patch's binding is validated.
320    pub(crate) fn into_fields(self) -> Vec<(usize, DynamicWriteCell)> {
321        self.fields
322    }
323
324    pub(crate) fn is_bound_to(&self, binding: &DynamicTypedEntityBinding) -> bool {
325        self.entity_source == binding.entity_source
326            && self.entity_tag == binding.entity_tag
327            && self.accepted_fingerprint == binding.accepted_fingerprint
328    }
329}
330
331///
332/// DynamicTypedMutation
333///
334/// One source-bound generated mutation whose fields carry binding-local
335/// descriptor ordinals. Entity names, field names and ordinals are not routing
336/// authority; the sealed binding resolves accepted IDs and slots.
337///
338
339#[doc(hidden)]
340#[derive(Clone, Debug, Eq, PartialEq)]
341pub enum DynamicTypedMutation {
342    /// Insert one accepted row.
343    Insert {
344        /// Bound authored field intents.
345        patch: DynamicTypedStructuralPatch,
346    },
347    /// Patch one accepted row.
348    Update {
349        /// Scalar or composite public primary key.
350        key: InputValue,
351        /// Bound authored field intents.
352        patch: DynamicTypedStructuralPatch,
353    },
354    /// Replace one accepted row, inserting it when absent.
355    Replace {
356        /// Scalar or composite public primary key.
357        key: InputValue,
358        /// Bound authored field intents.
359        patch: DynamicTypedStructuralPatch,
360    },
361    /// Delete one accepted row.
362    Delete {
363        /// Scalar or composite public primary key.
364        key: InputValue,
365    },
366}
367
368/// Opaque accepted-schema identity issued for one generated typed adapter.
369///
370/// Public facade code may retain and return this value, but its accepted field
371/// mapping remains private to IcyDB.
372#[doc(hidden)]
373#[derive(Clone, Debug, Eq, PartialEq)]
374pub struct DynamicTypedEntityBinding {
375    pub(crate) database_incarnation: [u8; 16],
376    pub(crate) entity_source: String,
377    pub(crate) entity_label: String,
378    pub(crate) entity_tag: u64,
379    pub(crate) accepted_revision: u64,
380    pub(crate) accepted_fingerprint: [u8; 16],
381    pub(crate) entity_generation: u32,
382    fields: Vec<DynamicTypedFieldBinding>,
383    pub(crate) named_types: Vec<(String, String)>,
384    pub(crate) enum_variants: Vec<(String, String, String)>,
385    pub(crate) composite_fields: Vec<(String, String, String)>,
386}
387
388impl DynamicTypedEntityBinding {
389    #[expect(
390        clippy::too_many_arguments,
391        reason = "the opaque binding keeps every accepted authority component explicit"
392    )]
393    pub(crate) fn new(
394        database_incarnation: [u8; 16],
395        entity_source: String,
396        entity_label: String,
397        entity_tag: u64,
398        accepted_revision: u64,
399        accepted_fingerprint: [u8; 16],
400        entity_generation: u32,
401        fields: Vec<(String, u32, u16, String)>,
402        named_types: Vec<(String, String)>,
403        enum_variants: Vec<(String, String, String)>,
404        composite_fields: Vec<(String, String, String)>,
405    ) -> Result<Self, InternalError> {
406        let mut sources = BTreeSet::new();
407        let mut ids = BTreeSet::new();
408        let mut slots = BTreeSet::new();
409        let fields = fields
410            .into_iter()
411            .map(|(source_key, field_id, slot, label)| {
412                if !sources.insert(source_key.clone())
413                    || !ids.insert(field_id)
414                    || !slots.insert(slot)
415                {
416                    return Err(InternalError::store_invariant());
417                }
418                Ok(DynamicTypedFieldBinding {
419                    source_key,
420                    field_id,
421                    slot,
422                    label,
423                })
424            })
425            .collect::<Result<Vec<_>, _>>()?;
426
427        Ok(Self {
428            database_incarnation,
429            entity_source,
430            entity_label,
431            entity_tag,
432            accepted_revision,
433            accepted_fingerprint,
434            entity_generation,
435            fields,
436            named_types,
437            enum_variants,
438            composite_fields,
439        })
440    }
441
442    /// Borrow the accepted entity display label.
443    #[must_use]
444    pub const fn entity(&self) -> &str {
445        self.entity_label.as_str()
446    }
447
448    /// Borrow the immutable entity source identity.
449    #[must_use]
450    pub const fn entity_source(&self) -> &str {
451        self.entity_source.as_str()
452    }
453
454    /// Resolve one immutable field source key directly to its accepted slot.
455    #[must_use]
456    pub fn field_slot(&self, source_key: &str) -> Option<u16> {
457        self.fields
458            .iter()
459            .find_map(|field| (field.source_key == source_key).then_some(field.slot))
460    }
461
462    /// Resolve one accepted output label to its binding-owned accepted slot.
463    #[must_use]
464    pub fn output_field_slot(&self, label: &str) -> Option<u16> {
465        self.fields
466            .iter()
467            .find_map(|field| (field.label == label).then_some(field.slot))
468    }
469
470    pub(crate) fn field_identity_bindings(&self) -> impl Iterator<Item = (&str, u32, u16)> {
471        self.fields
472            .iter()
473            .map(|field| (field.source_key.as_str(), field.field_id, field.slot))
474    }
475
476    pub(crate) fn field_identity_binding(&self, descriptor_ordinal: usize) -> Option<(u32, u16)> {
477        self.fields
478            .get(descriptor_ordinal)
479            .map(|field| (field.field_id, field.slot))
480    }
481
482    /// Bind generated descriptor-ordinal write intent to accepted field IDs and slots.
483    #[must_use]
484    pub fn bind_write_ordinals(
485        &self,
486        fields: Vec<(usize, DynamicWriteCell)>,
487    ) -> Option<DynamicTypedStructuralPatch> {
488        let mut previous_ordinal = None;
489        for (descriptor_ordinal, _) in &fields {
490            if previous_ordinal.is_some_and(|previous| *descriptor_ordinal <= previous) {
491                return None;
492            }
493            let _ = self.fields.get(*descriptor_ordinal)?;
494            previous_ordinal = Some(*descriptor_ordinal);
495        }
496        Some(DynamicTypedStructuralPatch {
497            entity_source: self.entity_source.clone(),
498            entity_tag: self.entity_tag,
499            accepted_fingerprint: self.accepted_fingerprint,
500            fields,
501        })
502    }
503
504    /// Resolve one immutable named-type source key to its accepted display path.
505    #[must_use]
506    pub fn named_type_name(&self, source_key: &str) -> Option<&str> {
507        self.named_types
508            .iter()
509            .find_map(|(source, name)| (source == source_key).then_some(name.as_str()))
510    }
511
512    /// Resolve one immutable enum-variant source key to its accepted display name.
513    #[must_use]
514    pub fn enum_variant_name(&self, type_source_key: &str, source_key: &str) -> Option<&str> {
515        self.enum_variants
516            .iter()
517            .find_map(|(bound_type, source, name)| {
518                (bound_type == type_source_key && source == source_key).then_some(name.as_str())
519            })
520    }
521
522    /// Resolve one accepted enum-variant display name to its immutable source key.
523    #[must_use]
524    pub fn enum_variant_source_key(
525        &self,
526        type_source_key: &str,
527        accepted_name: &str,
528    ) -> Option<&str> {
529        self.enum_variants
530            .iter()
531            .find_map(|(bound_type, source, name)| {
532                (bound_type == type_source_key && name == accepted_name).then_some(source.as_str())
533            })
534    }
535
536    /// Resolve one immutable record-member source key to its accepted display name.
537    #[must_use]
538    pub fn composite_field_name(&self, type_source_key: &str, source_key: &str) -> Option<&str> {
539        self.composite_fields
540            .iter()
541            .find_map(|(bound_type, source, name)| {
542                (bound_type == type_source_key && source == source_key).then_some(name.as_str())
543            })
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::{DynamicMutationResult, DynamicTypedEntityBinding};
550
551    fn binding() -> DynamicTypedEntityBinding {
552        DynamicTypedEntityBinding::new(
553            [0; 16],
554            "EntitySource".to_string(),
555            "Entity".to_string(),
556            1,
557            1,
558            [1; 16],
559            1,
560            Vec::new(),
561            vec![("ChoiceSource".to_string(), "RenamedChoice".to_string())],
562            vec![
563                (
564                    "ChoiceSource".to_string(),
565                    "FirstSource".to_string(),
566                    "RenamedFirst".to_string(),
567                ),
568                (
569                    "ChoiceSource".to_string(),
570                    "SecondSource".to_string(),
571                    "Second".to_string(),
572                ),
573            ],
574            Vec::new(),
575        )
576        .expect("test binding should be internally consistent")
577    }
578
579    #[test]
580    fn accepted_enum_variant_name_resolves_to_immutable_source_key() {
581        let binding = binding();
582
583        assert_eq!(
584            binding.enum_variant_source_key("ChoiceSource", "RenamedFirst"),
585            Some("FirstSource"),
586        );
587        assert_eq!(
588            binding.enum_variant_source_key("ChoiceSource", "Second"),
589            Some("SecondSource"),
590        );
591        assert_eq!(
592            binding.enum_variant_source_key("OtherSource", "RenamedFirst"),
593            None,
594        );
595        assert_eq!(
596            binding.enum_variant_source_key("ChoiceSource", "FirstSource"),
597            None,
598        );
599    }
600
601    #[test]
602    fn dynamic_mutation_result_derives_cardinality_from_rows() {
603        let result = DynamicMutationResult {
604            entity: "Entity".to_string(),
605            columns: Vec::new(),
606            rows: vec![Vec::new()],
607            affected_rows: 1,
608        };
609
610        assert_eq!(result.len(), 1);
611        assert!(!result.is_empty());
612    }
613}