1use 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#[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#[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 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 #[must_use]
248 pub const fn entity_source(self) -> &'static str {
249 self.entity_source
250 }
251
252 #[must_use]
254 pub const fn field_source(self) -> Option<&'static str> {
255 self.field_source
256 }
257}
258
259#[doc(hidden)]
261#[derive(Debug)]
262pub enum DynamicTypedBindingError {
263 IncompatibleField,
265
266 Internal(InternalError),
268
269 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#[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 #[cfg(test)]
314 #[must_use]
315 pub(crate) const fn fields(&self) -> &[(usize, DynamicWriteCell)] {
316 self.fields.as_slice()
317 }
318
319 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#[doc(hidden)]
340#[derive(Clone, Debug, Eq, PartialEq)]
341pub enum DynamicTypedMutation {
342 Insert {
344 patch: DynamicTypedStructuralPatch,
346 },
347 Update {
349 key: InputValue,
351 patch: DynamicTypedStructuralPatch,
353 },
354 Replace {
356 key: InputValue,
358 patch: DynamicTypedStructuralPatch,
360 },
361 Delete {
363 key: InputValue,
365 },
366}
367
368#[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 #[must_use]
444 pub const fn entity(&self) -> &str {
445 self.entity_label.as_str()
446 }
447
448 #[must_use]
450 pub const fn entity_source(&self) -> &str {
451 self.entity_source.as_str()
452 }
453
454 #[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 #[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 #[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 #[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 #[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 #[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 #[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}