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 pub(crate) fn into_fields(self) -> Vec<(String, DynamicWriteCell)> {
65 self.fields
66 }
67}
68
69#[doc(hidden)]
78#[derive(Clone, Debug, Eq, PartialEq)]
79pub enum DynamicMutation {
80 Insert {
82 entity: String,
84 patch: DynamicStructuralPatch,
86 },
87 Update {
89 entity: String,
91 key: InputValue,
93 patch: DynamicStructuralPatch,
95 },
96 Replace {
98 entity: String,
100 key: InputValue,
102 patch: DynamicStructuralPatch,
104 },
105 Delete {
107 entity: String,
109 key: InputValue,
111 },
112}
113
114impl DynamicMutation {
115 #[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#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
134pub struct DynamicMutationResult {
135 pub entity: String,
137 pub columns: Vec<String>,
139 pub rows: Vec<Vec<OutputValue>>,
141 pub affected_rows: u32,
143}
144
145impl DynamicMutationResult {
146 #[must_use]
148 pub const fn len(&self) -> usize {
149 self.rows.len()
150 }
151
152 #[must_use]
154 pub const fn is_empty(&self) -> bool {
155 self.rows.is_empty()
156 }
157}
158
159#[doc(hidden)]
161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub enum TypedFieldType {
163 Scalar(ScalarType),
165 List(&'static Self),
167 Named(&'static str),
169}
170
171#[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 #[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#[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 #[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#[doc(hidden)]
219#[derive(Debug)]
220pub enum DynamicTypedBindingError {
221 FieldUnavailable,
223 IncompatibleField,
225 Internal(InternalError),
227}
228
229impl From<InternalError> for DynamicTypedBindingError {
230 fn from(error: InternalError) -> Self {
231 Self::Internal(error)
232 }
233}
234
235#[derive(Clone, Debug, Eq, PartialEq)]
236struct DynamicTypedFieldBinding {
237 source_key: String,
238 field_id: u32,
239 slot: u16,
240 label: String,
241}
242
243#[doc(hidden)]
250#[derive(Clone, Debug, Default, Eq, PartialEq)]
251pub struct DynamicTypedStructuralPatch {
252 entity_source: String,
253 entity_tag: u64,
254 accepted_fingerprint: [u8; 16],
255 fields: Vec<(usize, DynamicWriteCell)>,
256}
257
258impl DynamicTypedStructuralPatch {
259 #[cfg(test)]
261 #[must_use]
262 pub(crate) const fn fields(&self) -> &[(usize, DynamicWriteCell)] {
263 self.fields.as_slice()
264 }
265
266 pub(crate) fn into_fields(self) -> Vec<(usize, DynamicWriteCell)> {
268 self.fields
269 }
270
271 pub(crate) fn is_bound_to(&self, binding: &DynamicTypedEntityBinding) -> bool {
272 self.entity_source == binding.entity_source
273 && self.entity_tag == binding.entity_tag
274 && self.accepted_fingerprint == binding.accepted_fingerprint
275 }
276}
277
278#[doc(hidden)]
287#[derive(Clone, Debug, Eq, PartialEq)]
288pub enum DynamicTypedMutation {
289 Insert {
291 patch: DynamicTypedStructuralPatch,
293 },
294 Update {
296 key: InputValue,
298 patch: DynamicTypedStructuralPatch,
300 },
301 Replace {
303 key: InputValue,
305 patch: DynamicTypedStructuralPatch,
307 },
308 Delete {
310 key: InputValue,
312 },
313}
314
315#[doc(hidden)]
320#[derive(Clone, Debug, Eq, PartialEq)]
321pub struct DynamicTypedEntityBinding {
322 pub(crate) database_incarnation: [u8; 16],
323 pub(crate) entity_source: String,
324 pub(crate) entity_label: String,
325 pub(crate) entity_tag: u64,
326 pub(crate) accepted_revision: u64,
327 pub(crate) accepted_fingerprint: [u8; 16],
328 pub(crate) entity_generation: u32,
329 fields: Vec<DynamicTypedFieldBinding>,
330 pub(crate) named_types: Vec<(String, String)>,
331 pub(crate) enum_variants: Vec<(String, String, String)>,
332 pub(crate) composite_fields: Vec<(String, String, String)>,
333}
334
335impl DynamicTypedEntityBinding {
336 #[expect(
337 clippy::too_many_arguments,
338 reason = "the opaque binding keeps every accepted authority component explicit"
339 )]
340 pub(crate) fn new(
341 database_incarnation: [u8; 16],
342 entity_source: String,
343 entity_label: String,
344 entity_tag: u64,
345 accepted_revision: u64,
346 accepted_fingerprint: [u8; 16],
347 entity_generation: u32,
348 fields: Vec<(String, u32, u16, String)>,
349 named_types: Vec<(String, String)>,
350 enum_variants: Vec<(String, String, String)>,
351 composite_fields: Vec<(String, String, String)>,
352 ) -> Result<Self, InternalError> {
353 let mut sources = BTreeSet::new();
354 let mut ids = BTreeSet::new();
355 let mut slots = BTreeSet::new();
356 let fields = fields
357 .into_iter()
358 .map(|(source_key, field_id, slot, label)| {
359 if !sources.insert(source_key.clone())
360 || !ids.insert(field_id)
361 || !slots.insert(slot)
362 {
363 return Err(InternalError::store_invariant());
364 }
365 Ok(DynamicTypedFieldBinding {
366 source_key,
367 field_id,
368 slot,
369 label,
370 })
371 })
372 .collect::<Result<Vec<_>, _>>()?;
373
374 Ok(Self {
375 database_incarnation,
376 entity_source,
377 entity_label,
378 entity_tag,
379 accepted_revision,
380 accepted_fingerprint,
381 entity_generation,
382 fields,
383 named_types,
384 enum_variants,
385 composite_fields,
386 })
387 }
388
389 #[must_use]
391 pub const fn entity(&self) -> &str {
392 self.entity_label.as_str()
393 }
394
395 #[must_use]
397 pub const fn entity_source(&self) -> &str {
398 self.entity_source.as_str()
399 }
400
401 #[must_use]
403 pub fn field_slot(&self, source_key: &str) -> Option<u16> {
404 self.fields
405 .iter()
406 .find_map(|field| (field.source_key == source_key).then_some(field.slot))
407 }
408
409 #[must_use]
411 pub fn output_field_slot(&self, label: &str) -> Option<u16> {
412 self.fields
413 .iter()
414 .find_map(|field| (field.label == label).then_some(field.slot))
415 }
416
417 pub(crate) fn field_identity_bindings(&self) -> impl Iterator<Item = (&str, u32, u16)> {
418 self.fields
419 .iter()
420 .map(|field| (field.source_key.as_str(), field.field_id, field.slot))
421 }
422
423 pub(crate) fn field_identity_binding(&self, descriptor_ordinal: usize) -> Option<(u32, u16)> {
424 self.fields
425 .get(descriptor_ordinal)
426 .map(|field| (field.field_id, field.slot))
427 }
428
429 #[must_use]
431 pub fn bind_write_ordinals(
432 &self,
433 fields: Vec<(usize, DynamicWriteCell)>,
434 ) -> Option<DynamicTypedStructuralPatch> {
435 let mut previous_ordinal = None;
436 for (descriptor_ordinal, _) in &fields {
437 if previous_ordinal.is_some_and(|previous| *descriptor_ordinal <= previous) {
438 return None;
439 }
440 let _ = self.fields.get(*descriptor_ordinal)?;
441 previous_ordinal = Some(*descriptor_ordinal);
442 }
443 Some(DynamicTypedStructuralPatch {
444 entity_source: self.entity_source.clone(),
445 entity_tag: self.entity_tag,
446 accepted_fingerprint: self.accepted_fingerprint,
447 fields,
448 })
449 }
450
451 #[must_use]
453 pub fn named_type_name(&self, source_key: &str) -> Option<&str> {
454 self.named_types
455 .iter()
456 .find_map(|(source, name)| (source == source_key).then_some(name.as_str()))
457 }
458
459 #[must_use]
461 pub fn enum_variant_name(&self, type_source_key: &str, source_key: &str) -> Option<&str> {
462 self.enum_variants
463 .iter()
464 .find_map(|(bound_type, source, name)| {
465 (bound_type == type_source_key && source == source_key).then_some(name.as_str())
466 })
467 }
468
469 #[must_use]
471 pub fn enum_variant_source_key(
472 &self,
473 type_source_key: &str,
474 accepted_name: &str,
475 ) -> Option<&str> {
476 self.enum_variants
477 .iter()
478 .find_map(|(bound_type, source, name)| {
479 (bound_type == type_source_key && name == accepted_name).then_some(source.as_str())
480 })
481 }
482
483 #[must_use]
485 pub fn composite_field_name(&self, type_source_key: &str, source_key: &str) -> Option<&str> {
486 self.composite_fields
487 .iter()
488 .find_map(|(bound_type, source, name)| {
489 (bound_type == type_source_key && source == source_key).then_some(name.as_str())
490 })
491 }
492}
493
494#[cfg(test)]
495mod tests {
496 use super::{DynamicMutationResult, DynamicTypedEntityBinding};
497
498 fn binding() -> DynamicTypedEntityBinding {
499 DynamicTypedEntityBinding::new(
500 [0; 16],
501 "EntitySource".to_string(),
502 "Entity".to_string(),
503 1,
504 1,
505 [1; 16],
506 1,
507 Vec::new(),
508 vec![("ChoiceSource".to_string(), "RenamedChoice".to_string())],
509 vec![
510 (
511 "ChoiceSource".to_string(),
512 "FirstSource".to_string(),
513 "RenamedFirst".to_string(),
514 ),
515 (
516 "ChoiceSource".to_string(),
517 "SecondSource".to_string(),
518 "Second".to_string(),
519 ),
520 ],
521 Vec::new(),
522 )
523 .expect("test binding should be internally consistent")
524 }
525
526 #[test]
527 fn accepted_enum_variant_name_resolves_to_immutable_source_key() {
528 let binding = binding();
529
530 assert_eq!(
531 binding.enum_variant_source_key("ChoiceSource", "RenamedFirst"),
532 Some("FirstSource"),
533 );
534 assert_eq!(
535 binding.enum_variant_source_key("ChoiceSource", "Second"),
536 Some("SecondSource"),
537 );
538 assert_eq!(
539 binding.enum_variant_source_key("OtherSource", "RenamedFirst"),
540 None,
541 );
542 assert_eq!(
543 binding.enum_variant_source_key("ChoiceSource", "FirstSource"),
544 None,
545 );
546 }
547
548 #[test]
549 fn dynamic_mutation_result_derives_cardinality_from_rows() {
550 let result = DynamicMutationResult {
551 entity: "Entity".to_string(),
552 columns: Vec::new(),
553 rows: vec![Vec::new()],
554 affected_rows: 1,
555 };
556
557 assert_eq!(result.len(), 1);
558 assert!(!result.is_empty());
559 }
560}