1use 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#[doc(hidden)]
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub enum DynamicWriteCell {
26 Omitted,
28 Default,
30 Null,
32 Value(InputValue),
34}
35
36#[doc(hidden)]
45#[derive(Clone, Debug, Default, Eq, PartialEq)]
46pub struct DynamicStructuralPatch {
47 fields: Vec<(String, DynamicWriteCell)>,
48}
49
50impl DynamicStructuralPatch {
51 #[must_use]
53 pub const fn new(fields: Vec<(String, DynamicWriteCell)>) -> Self {
54 Self { fields }
55 }
56
57 #[must_use]
59 pub const fn fields(&self) -> &[(String, DynamicWriteCell)] {
60 self.fields.as_slice()
61 }
62}
63
64#[doc(hidden)]
73#[derive(Clone, Debug, Eq, PartialEq)]
74pub enum DynamicMutation {
75 Insert {
77 entity: String,
79 patch: DynamicStructuralPatch,
81 },
82 Update {
84 entity: String,
86 key: InputValue,
88 patch: DynamicStructuralPatch,
90 },
91 Replace {
93 entity: String,
95 key: InputValue,
97 patch: DynamicStructuralPatch,
99 },
100 Delete {
102 entity: String,
104 key: InputValue,
106 },
107}
108
109impl DynamicMutation {
110 #[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#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
129pub struct DynamicMutationResult {
130 pub entity: String,
132 pub columns: Vec<String>,
134 pub rows: Vec<Vec<OutputValue>>,
136 pub affected_rows: u32,
138}
139
140impl DynamicMutationResult {
141 #[must_use]
143 pub const fn len(&self) -> usize {
144 self.rows.len()
145 }
146
147 #[must_use]
149 pub const fn is_empty(&self) -> bool {
150 self.rows.is_empty()
151 }
152}
153
154#[doc(hidden)]
156#[derive(Clone, Copy, Debug, Eq, PartialEq)]
157pub enum TypedFieldType {
158 Scalar(ScalarType),
160 List(&'static Self),
162 Named(&'static str),
164}
165
166#[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 #[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#[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 #[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#[doc(hidden)]
214#[derive(Debug)]
215pub enum DynamicTypedBindingError {
216 FieldUnavailable,
218 IncompatibleField,
220 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#[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 #[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#[doc(hidden)]
276#[derive(Clone, Debug, Eq, PartialEq)]
277pub enum DynamicTypedMutation {
278 Insert {
280 patch: DynamicTypedStructuralPatch,
282 },
283 Update {
285 key: InputValue,
287 patch: DynamicTypedStructuralPatch,
289 },
290 Replace {
292 key: InputValue,
294 patch: DynamicTypedStructuralPatch,
296 },
297 Delete {
299 key: InputValue,
301 },
302}
303
304#[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 #[must_use]
380 pub const fn entity(&self) -> &str {
381 self.entity_label.as_str()
382 }
383
384 #[must_use]
386 pub const fn entity_source(&self) -> &str {
387 self.entity_source.as_str()
388 }
389
390 #[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 #[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 #[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 #[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 #[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 #[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 #[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}