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