1use 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#[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 {
100 entity: String,
102 key: InputValue,
104 patch: DynamicStructuralPatch,
106 },
107 Delete {
109 entity: String,
111 key: InputValue,
113 },
114}
115
116impl DynamicMutation {
117 #[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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
136pub struct DynamicMutationResult {
137 pub entity: String,
139 pub columns: Vec<String>,
141 pub rows: Vec<Vec<OutputValue>>,
143 pub affected_rows: u32,
145}
146
147impl DynamicMutationResult {
148 #[must_use]
150 pub const fn len(&self) -> usize {
151 self.rows.len()
152 }
153
154 #[must_use]
156 pub const fn is_empty(&self) -> bool {
157 self.rows.is_empty()
158 }
159}
160
161#[doc(hidden)]
163#[derive(Clone, Copy, Debug, Eq, PartialEq)]
164pub enum TypedFieldType {
165 Scalar(ScalarType),
167 List(&'static Self),
169 Named(&'static str),
171}
172
173#[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 #[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#[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 #[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#[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 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 #[must_use]
250 pub const fn entity_source(self) -> &'static str {
251 self.entity_source
252 }
253
254 #[must_use]
256 pub const fn field_source(self) -> Option<&'static str> {
257 self.field_source
258 }
259}
260
261#[doc(hidden)]
263#[derive(Debug)]
264pub enum DynamicTypedBindingError {
265 IncompatibleField,
267
268 Internal(InternalError),
270
271 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#[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 #[cfg(test)]
316 #[must_use]
317 pub(crate) const fn fields(&self) -> &[(usize, DynamicWriteCell)] {
318 self.fields.as_slice()
319 }
320
321 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#[doc(hidden)]
342#[derive(Clone, Debug, Eq, PartialEq)]
343pub enum DynamicTypedMutation {
344 Insert {
346 patch: DynamicTypedStructuralPatch,
348 },
349 Update {
351 key: InputValue,
353 patch: DynamicTypedStructuralPatch,
355 },
356 Replace {
360 key: InputValue,
362 patch: DynamicTypedStructuralPatch,
364 },
365 Delete {
367 key: InputValue,
369 },
370}
371
372#[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 #[must_use]
448 pub const fn entity(&self) -> &str {
449 self.entity_label.as_str()
450 }
451
452 #[must_use]
454 pub const fn entity_source(&self) -> &str {
455 self.entity_source.as_str()
456 }
457
458 #[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 #[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 #[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 #[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 #[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 #[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 #[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}