use crate::{
error::InternalError,
value::{InputValue, OutputValue},
};
use candid::CandidType;
use icydb_schema::ScalarType;
use serde::Deserialize;
use std::collections::BTreeSet;
#[doc(hidden)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DynamicWriteCell {
Omitted,
Default,
Null,
Value(InputValue),
}
#[doc(hidden)]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DynamicStructuralPatch {
fields: Vec<(String, DynamicWriteCell)>,
}
impl DynamicStructuralPatch {
#[must_use]
pub const fn new(fields: Vec<(String, DynamicWriteCell)>) -> Self {
Self { fields }
}
#[must_use]
pub const fn fields(&self) -> &[(String, DynamicWriteCell)] {
self.fields.as_slice()
}
}
#[doc(hidden)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DynamicMutation {
Insert {
entity: String,
patch: DynamicStructuralPatch,
},
Update {
entity: String,
key: InputValue,
patch: DynamicStructuralPatch,
},
Replace {
entity: String,
key: InputValue,
patch: DynamicStructuralPatch,
},
Delete {
entity: String,
key: InputValue,
},
}
impl DynamicMutation {
#[must_use]
pub const fn entity(&self) -> &str {
match self {
Self::Insert { entity, .. }
| Self::Update { entity, .. }
| Self::Replace { entity, .. }
| Self::Delete { entity, .. } => entity.as_str(),
}
}
}
#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
pub struct DynamicMutationResult {
pub entity: String,
pub columns: Vec<String>,
pub rows: Vec<Vec<OutputValue>>,
pub affected_rows: u32,
}
impl DynamicMutationResult {
#[must_use]
pub const fn len(&self) -> usize {
self.rows.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.rows.is_empty()
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TypedFieldType {
Scalar(ScalarType),
List(&'static Self),
Named(&'static str),
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TypedFieldDescriptor {
pub(crate) field_type: TypedFieldType,
pub(crate) nullable: bool,
pub(crate) source_key: &'static str,
}
impl TypedFieldDescriptor {
#[must_use]
pub const fn new(source_key: &'static str, field_type: TypedFieldType, nullable: bool) -> Self {
Self {
field_type,
nullable,
source_key,
}
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TypedEntityDescriptor {
pub(crate) entity_source_key: &'static str,
pub(crate) fields: &'static [TypedFieldDescriptor],
pub(crate) primary_key_source_keys: &'static [&'static str],
}
impl TypedEntityDescriptor {
#[must_use]
pub const fn new(
entity_source_key: &'static str,
primary_key_source_keys: &'static [&'static str],
fields: &'static [TypedFieldDescriptor],
) -> Self {
Self {
entity_source_key,
fields,
primary_key_source_keys,
}
}
}
#[doc(hidden)]
#[derive(Debug)]
pub enum DynamicTypedBindingError {
FieldUnavailable,
IncompatibleField,
Internal(InternalError),
}
impl From<InternalError> for DynamicTypedBindingError {
fn from(error: InternalError) -> Self {
Self::Internal(error)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct DynamicTypedFieldBinding {
source_key: String,
field_id: u32,
slot: u16,
label: String,
}
#[doc(hidden)]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DynamicTypedStructuralPatch {
entity_source: String,
entity_tag: u64,
accepted_fingerprint: [u8; 16],
fields: Vec<(usize, DynamicWriteCell)>,
}
impl DynamicTypedStructuralPatch {
#[must_use]
pub(crate) const fn fields(&self) -> &[(usize, DynamicWriteCell)] {
self.fields.as_slice()
}
pub(crate) fn is_bound_to(&self, binding: &DynamicTypedEntityBinding) -> bool {
self.entity_source == binding.entity_source
&& self.entity_tag == binding.entity_tag
&& self.accepted_fingerprint == binding.accepted_fingerprint
}
}
#[doc(hidden)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DynamicTypedMutation {
Insert {
patch: DynamicTypedStructuralPatch,
},
Update {
key: InputValue,
patch: DynamicTypedStructuralPatch,
},
Replace {
key: InputValue,
patch: DynamicTypedStructuralPatch,
},
Delete {
key: InputValue,
},
}
#[doc(hidden)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DynamicTypedEntityBinding {
pub(crate) database_incarnation: [u8; 16],
pub(crate) entity_source: String,
pub(crate) entity_label: String,
pub(crate) entity_tag: u64,
pub(crate) accepted_revision: u64,
pub(crate) accepted_fingerprint: [u8; 16],
pub(crate) entity_generation: u32,
fields: Vec<DynamicTypedFieldBinding>,
pub(crate) named_types: Vec<(String, String)>,
pub(crate) enum_variants: Vec<(String, String, String)>,
pub(crate) composite_fields: Vec<(String, String, String)>,
}
impl DynamicTypedEntityBinding {
#[expect(
clippy::too_many_arguments,
reason = "the opaque binding keeps every accepted authority component explicit"
)]
pub(crate) fn new(
database_incarnation: [u8; 16],
entity_source: String,
entity_label: String,
entity_tag: u64,
accepted_revision: u64,
accepted_fingerprint: [u8; 16],
entity_generation: u32,
fields: Vec<(String, u32, u16, String)>,
named_types: Vec<(String, String)>,
enum_variants: Vec<(String, String, String)>,
composite_fields: Vec<(String, String, String)>,
) -> Result<Self, InternalError> {
let mut sources = BTreeSet::new();
let mut ids = BTreeSet::new();
let mut slots = BTreeSet::new();
let fields = fields
.into_iter()
.map(|(source_key, field_id, slot, label)| {
if !sources.insert(source_key.clone())
|| !ids.insert(field_id)
|| !slots.insert(slot)
{
return Err(InternalError::store_invariant());
}
Ok(DynamicTypedFieldBinding {
source_key,
field_id,
slot,
label,
})
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Self {
database_incarnation,
entity_source,
entity_label,
entity_tag,
accepted_revision,
accepted_fingerprint,
entity_generation,
fields,
named_types,
enum_variants,
composite_fields,
})
}
#[must_use]
pub const fn entity(&self) -> &str {
self.entity_label.as_str()
}
#[must_use]
pub const fn entity_source(&self) -> &str {
self.entity_source.as_str()
}
#[must_use]
pub fn field_slot(&self, source_key: &str) -> Option<u16> {
self.fields
.iter()
.find_map(|field| (field.source_key == source_key).then_some(field.slot))
}
#[must_use]
pub fn output_field_slot(&self, label: &str) -> Option<u16> {
self.fields
.iter()
.find_map(|field| (field.label == label).then_some(field.slot))
}
pub(crate) fn field_identity_bindings(&self) -> impl Iterator<Item = (&str, u32, u16)> {
self.fields
.iter()
.map(|field| (field.source_key.as_str(), field.field_id, field.slot))
}
pub(crate) fn field_identity_binding(&self, descriptor_ordinal: usize) -> Option<(u32, u16)> {
self.fields
.get(descriptor_ordinal)
.map(|field| (field.field_id, field.slot))
}
#[must_use]
pub fn bind_write_ordinals(
&self,
fields: Vec<(usize, DynamicWriteCell)>,
) -> Option<DynamicTypedStructuralPatch> {
let mut previous_ordinal = None;
for (descriptor_ordinal, _) in &fields {
if previous_ordinal.is_some_and(|previous| *descriptor_ordinal <= previous) {
return None;
}
let _ = self.fields.get(*descriptor_ordinal)?;
previous_ordinal = Some(*descriptor_ordinal);
}
Some(DynamicTypedStructuralPatch {
entity_source: self.entity_source.clone(),
entity_tag: self.entity_tag,
accepted_fingerprint: self.accepted_fingerprint,
fields,
})
}
#[must_use]
pub fn named_type_name(&self, source_key: &str) -> Option<&str> {
self.named_types
.iter()
.find_map(|(source, name)| (source == source_key).then_some(name.as_str()))
}
#[must_use]
pub fn enum_variant_name(&self, type_source_key: &str, source_key: &str) -> Option<&str> {
self.enum_variants
.iter()
.find_map(|(bound_type, source, name)| {
(bound_type == type_source_key && source == source_key).then_some(name.as_str())
})
}
#[must_use]
pub fn enum_variant_source_key(
&self,
type_source_key: &str,
accepted_name: &str,
) -> Option<&str> {
self.enum_variants
.iter()
.find_map(|(bound_type, source, name)| {
(bound_type == type_source_key && name == accepted_name).then_some(source.as_str())
})
}
#[must_use]
pub fn composite_field_name(&self, type_source_key: &str, source_key: &str) -> Option<&str> {
self.composite_fields
.iter()
.find_map(|(bound_type, source, name)| {
(bound_type == type_source_key && source == source_key).then_some(name.as_str())
})
}
}
#[cfg(test)]
mod tests {
use super::{DynamicMutationResult, DynamicTypedEntityBinding};
fn binding() -> DynamicTypedEntityBinding {
DynamicTypedEntityBinding::new(
[0; 16],
"EntitySource".to_string(),
"Entity".to_string(),
1,
1,
[1; 16],
1,
Vec::new(),
vec![("ChoiceSource".to_string(), "RenamedChoice".to_string())],
vec![
(
"ChoiceSource".to_string(),
"FirstSource".to_string(),
"RenamedFirst".to_string(),
),
(
"ChoiceSource".to_string(),
"SecondSource".to_string(),
"Second".to_string(),
),
],
Vec::new(),
)
.expect("test binding should be internally consistent")
}
#[test]
fn accepted_enum_variant_name_resolves_to_immutable_source_key() {
let binding = binding();
assert_eq!(
binding.enum_variant_source_key("ChoiceSource", "RenamedFirst"),
Some("FirstSource"),
);
assert_eq!(
binding.enum_variant_source_key("ChoiceSource", "Second"),
Some("SecondSource"),
);
assert_eq!(
binding.enum_variant_source_key("OtherSource", "RenamedFirst"),
None,
);
assert_eq!(
binding.enum_variant_source_key("ChoiceSource", "FirstSource"),
None,
);
}
#[test]
fn dynamic_mutation_result_derives_cardinality_from_rows() {
let result = DynamicMutationResult {
entity: "Entity".to_string(),
columns: Vec::new(),
rows: vec![Vec::new()],
affected_rows: 1,
};
assert_eq!(result.len(), 1);
assert!(!result.is_empty());
}
}