1use crate::{
7 db::{
8 relation::{
9 RelationFieldCardinality, RelationFieldMetadata, relation_field_metadata_for_model_iter,
10 },
11 schema::{
12 AcceptedSchemaSnapshot, PersistedFieldKind, PersistedIndexKeyItemSnapshot,
13 PersistedIndexKeySnapshot, PersistedNestedLeafSnapshot, PersistedRelationStrength,
14 SchemaFieldDefault, SchemaFieldSlot, field_type_from_persisted_kind,
15 },
16 },
17 model::{
18 entity::EntityModel,
19 field::{FieldDatabaseDefault, FieldKind, FieldModel, RelationStrength},
20 },
21};
22use candid::CandidType;
23use serde::Deserialize;
24use sha2::{Digest, Sha256};
25use std::fmt::Write;
26
27const ENTITY_FIELD_DESCRIPTION_NO_SLOT: u16 = u16::MAX;
28
29#[cfg_attr(
30 doc,
31 doc = "EntitySchemaDescription\n\nStable describe payload for one entity model."
32)]
33#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
34pub struct EntitySchemaDescription {
35 pub(crate) entity_path: String,
36 pub(crate) entity_name: String,
37 pub(crate) primary_key: String,
38 pub(crate) primary_key_fields: Vec<String>,
39 pub(crate) fields: Vec<EntityFieldDescription>,
40 pub(crate) indexes: Vec<EntityIndexDescription>,
41 pub(crate) relations: Vec<EntityRelationDescription>,
42}
43
44#[cfg_attr(
45 doc,
46 doc = "EntitySchemaCheckDescription\n\nGenerated-vs-accepted schema description payload for one entity."
47)]
48#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
49pub struct EntitySchemaCheckDescription {
50 pub(crate) generated: EntitySchemaDescription,
51 pub(crate) accepted: EntitySchemaDescription,
52}
53
54impl EntitySchemaCheckDescription {
55 #[must_use]
57 pub const fn new(
58 generated: EntitySchemaDescription,
59 accepted: EntitySchemaDescription,
60 ) -> Self {
61 Self {
62 generated,
63 accepted,
64 }
65 }
66
67 #[must_use]
69 pub const fn generated(&self) -> &EntitySchemaDescription {
70 &self.generated
71 }
72
73 #[must_use]
75 pub const fn accepted(&self) -> &EntitySchemaDescription {
76 &self.accepted
77 }
78}
79
80impl EntitySchemaDescription {
81 #[must_use]
83 pub fn new(
84 entity_path: String,
85 entity_name: String,
86 primary_key: String,
87 fields: Vec<EntityFieldDescription>,
88 indexes: Vec<EntityIndexDescription>,
89 relations: Vec<EntityRelationDescription>,
90 ) -> Self {
91 Self::new_with_primary_key_fields(
92 entity_path,
93 entity_name,
94 primary_key.clone(),
95 vec![primary_key],
96 fields,
97 indexes,
98 relations,
99 )
100 }
101
102 #[must_use]
105 pub const fn new_with_primary_key_fields(
106 entity_path: String,
107 entity_name: String,
108 primary_key: String,
109 primary_key_fields: Vec<String>,
110 fields: Vec<EntityFieldDescription>,
111 indexes: Vec<EntityIndexDescription>,
112 relations: Vec<EntityRelationDescription>,
113 ) -> Self {
114 Self {
115 entity_path,
116 entity_name,
117 primary_key,
118 primary_key_fields,
119 fields,
120 indexes,
121 relations,
122 }
123 }
124
125 #[must_use]
127 pub const fn entity_path(&self) -> &str {
128 self.entity_path.as_str()
129 }
130
131 #[must_use]
133 pub const fn entity_name(&self) -> &str {
134 self.entity_name.as_str()
135 }
136
137 #[must_use]
139 pub const fn primary_key(&self) -> &str {
140 self.primary_key.as_str()
141 }
142
143 #[must_use]
145 pub const fn primary_key_fields(&self) -> &[String] {
146 self.primary_key_fields.as_slice()
147 }
148
149 #[must_use]
151 pub const fn fields(&self) -> &[EntityFieldDescription] {
152 self.fields.as_slice()
153 }
154
155 #[must_use]
157 pub const fn indexes(&self) -> &[EntityIndexDescription] {
158 self.indexes.as_slice()
159 }
160
161 #[must_use]
163 pub const fn relations(&self) -> &[EntityRelationDescription] {
164 self.relations.as_slice()
165 }
166}
167
168#[cfg_attr(
169 doc,
170 doc = "EntityFieldDescription\n\nOne field entry in a describe payload."
171)]
172#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
173pub struct EntityFieldDescription {
174 pub(crate) name: String,
175 pub(crate) slot: u16,
176 pub(crate) kind: String,
177 pub(crate) nullable: bool,
178 pub(crate) primary_key: bool,
179 pub(crate) queryable: bool,
180 pub(crate) origin: String,
181}
182
183impl EntityFieldDescription {
184 #[must_use]
186 pub const fn new(
187 name: String,
188 slot: Option<u16>,
189 kind: String,
190 nullable: bool,
191 primary_key: bool,
192 queryable: bool,
193 origin: String,
194 ) -> Self {
195 let slot = match slot {
196 Some(slot) => slot,
197 None => ENTITY_FIELD_DESCRIPTION_NO_SLOT,
198 };
199
200 Self {
201 name,
202 slot,
203 kind,
204 nullable,
205 primary_key,
206 queryable,
207 origin,
208 }
209 }
210
211 #[must_use]
213 pub const fn name(&self) -> &str {
214 self.name.as_str()
215 }
216
217 #[must_use]
219 pub const fn slot(&self) -> Option<u16> {
220 if self.slot == ENTITY_FIELD_DESCRIPTION_NO_SLOT {
221 None
222 } else {
223 Some(self.slot)
224 }
225 }
226
227 #[must_use]
229 pub const fn kind(&self) -> &str {
230 self.kind.as_str()
231 }
232
233 #[must_use]
235 pub const fn nullable(&self) -> bool {
236 self.nullable
237 }
238
239 #[must_use]
241 pub const fn primary_key(&self) -> bool {
242 self.primary_key
243 }
244
245 #[must_use]
247 pub const fn queryable(&self) -> bool {
248 self.queryable
249 }
250
251 #[must_use]
253 pub const fn origin(&self) -> &str {
254 self.origin.as_str()
255 }
256}
257
258#[cfg_attr(
259 doc,
260 doc = "EntityIndexDescription\n\nOne index entry in a describe payload."
261)]
262#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
263pub struct EntityIndexDescription {
264 pub(crate) name: String,
265 pub(crate) unique: bool,
266 pub(crate) fields: Vec<String>,
267 pub(crate) origin: String,
268}
269
270impl EntityIndexDescription {
271 #[must_use]
273 pub const fn new(name: String, unique: bool, fields: Vec<String>, origin: String) -> Self {
274 Self {
275 name,
276 unique,
277 fields,
278 origin,
279 }
280 }
281
282 #[must_use]
284 pub const fn name(&self) -> &str {
285 self.name.as_str()
286 }
287
288 #[must_use]
290 pub const fn unique(&self) -> bool {
291 self.unique
292 }
293
294 #[must_use]
296 pub const fn fields(&self) -> &[String] {
297 self.fields.as_slice()
298 }
299
300 #[must_use]
302 pub const fn origin(&self) -> &str {
303 self.origin.as_str()
304 }
305}
306
307#[cfg_attr(
308 doc,
309 doc = "EntityRelationDescription\n\nOne relation entry in a describe payload."
310)]
311#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
312pub struct EntityRelationDescription {
313 pub(crate) field: String,
314 pub(crate) target_path: String,
315 pub(crate) target_entity_name: String,
316 pub(crate) target_store_path: String,
317 pub(crate) strength: EntityRelationStrength,
318 pub(crate) cardinality: EntityRelationCardinality,
319}
320
321impl EntityRelationDescription {
322 #[must_use]
324 pub const fn new(
325 field: String,
326 target_path: String,
327 target_entity_name: String,
328 target_store_path: String,
329 strength: EntityRelationStrength,
330 cardinality: EntityRelationCardinality,
331 ) -> Self {
332 Self {
333 field,
334 target_path,
335 target_entity_name,
336 target_store_path,
337 strength,
338 cardinality,
339 }
340 }
341
342 #[must_use]
344 pub const fn field(&self) -> &str {
345 self.field.as_str()
346 }
347
348 #[must_use]
350 pub const fn target_path(&self) -> &str {
351 self.target_path.as_str()
352 }
353
354 #[must_use]
356 pub const fn target_entity_name(&self) -> &str {
357 self.target_entity_name.as_str()
358 }
359
360 #[must_use]
362 pub const fn target_store_path(&self) -> &str {
363 self.target_store_path.as_str()
364 }
365
366 #[must_use]
368 pub const fn strength(&self) -> EntityRelationStrength {
369 self.strength
370 }
371
372 #[must_use]
374 pub const fn cardinality(&self) -> EntityRelationCardinality {
375 self.cardinality
376 }
377}
378
379#[cfg_attr(doc, doc = "EntityRelationStrength\n\nDescribe relation strength.")]
380#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
381pub enum EntityRelationStrength {
382 Strong,
383 Weak,
384}
385
386#[cfg_attr(
387 doc,
388 doc = "EntityRelationCardinality\n\nDescribe relation cardinality."
389)]
390#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
391pub enum EntityRelationCardinality {
392 Single,
393 List,
394 Set,
395}
396
397#[cfg_attr(
398 doc,
399 doc = "Build one stable entity-schema description from one runtime `EntityModel`."
400)]
401#[must_use]
402pub(in crate::db) fn describe_entity_model(model: &EntityModel) -> EntitySchemaDescription {
403 let fields = describe_entity_fields(model);
404 let primary_key_fields = primary_key_field_names_from_model(model);
405 let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
406
407 describe_entity_model_from_description_rows(
408 model.path,
409 model.entity_name,
410 primary_key.as_str(),
411 primary_key_fields,
412 fields,
413 describe_entity_indexes_from_model(model),
414 describe_entity_relations_from_model(model),
415 )
416}
417
418#[cfg_attr(
419 doc,
420 doc = "Build one entity-schema description using accepted persisted schema slot metadata."
421)]
422#[must_use]
423pub(in crate::db) fn describe_entity_model_with_persisted_schema(
424 model: &EntityModel,
425 schema: &AcceptedSchemaSnapshot,
426) -> EntitySchemaDescription {
427 let fields = describe_entity_fields_with_persisted_schema(schema);
428 let primary_key_fields = schema.primary_key_field_names();
429 let primary_key_fields = if primary_key_fields.is_empty() {
430 vec![model.primary_key.name.to_string()]
431 } else {
432 primary_key_fields
433 .into_iter()
434 .map(str::to_string)
435 .collect::<Vec<_>>()
436 };
437 let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
438
439 describe_entity_model_from_description_rows(
440 schema.entity_path(),
441 schema.entity_name(),
442 primary_key.as_str(),
443 primary_key_fields,
444 fields,
445 describe_entity_indexes_with_persisted_schema(schema),
446 describe_entity_relations_with_persisted_schema(schema),
447 )
448}
449
450fn describe_entity_model_from_description_rows(
455 entity_path: &str,
456 entity_name: &str,
457 primary_key: &str,
458 primary_key_fields: Vec<String>,
459 fields: Vec<EntityFieldDescription>,
460 indexes: Vec<EntityIndexDescription>,
461 relations: Vec<EntityRelationDescription>,
462) -> EntitySchemaDescription {
463 EntitySchemaDescription::new_with_primary_key_fields(
464 entity_path.to_string(),
465 entity_name.to_string(),
466 primary_key.to_string(),
467 primary_key_fields,
468 fields,
469 indexes,
470 relations,
471 )
472}
473
474fn describe_entity_relations_from_model(model: &EntityModel) -> Vec<EntityRelationDescription> {
475 relation_field_metadata_for_model_iter(model)
476 .map(relation_description_from_metadata)
477 .collect()
478}
479
480fn primary_key_field_names_from_model(model: &EntityModel) -> Vec<String> {
481 model
482 .primary_key_model()
483 .fields()
484 .iter()
485 .map(|field| field.name.to_string())
486 .collect()
487}
488
489fn render_primary_key_fields(fields: &[String]) -> String {
490 fields.join(", ")
491}
492
493fn describe_entity_indexes_from_model(model: &EntityModel) -> Vec<EntityIndexDescription> {
494 let mut indexes = Vec::with_capacity(model.indexes.len());
495 for index in model.indexes {
496 indexes.push(EntityIndexDescription::new(
497 index.name().to_string(),
498 index.is_unique(),
499 index
500 .fields()
501 .iter()
502 .map(|field| (*field).to_string())
503 .collect(),
504 "generated".to_string(),
505 ));
506 }
507
508 indexes
509}
510
511fn describe_entity_indexes_with_persisted_schema(
512 schema: &AcceptedSchemaSnapshot,
513) -> Vec<EntityIndexDescription> {
514 schema
515 .persisted_snapshot()
516 .indexes()
517 .iter()
518 .map(|index| {
519 EntityIndexDescription::new(
520 index.name().to_string(),
521 index.unique(),
522 describe_persisted_index_fields(index.key()),
523 if index.generated() {
524 "generated".to_string()
525 } else {
526 "ddl".to_string()
527 },
528 )
529 })
530 .collect()
531}
532
533fn describe_persisted_index_fields(key: &PersistedIndexKeySnapshot) -> Vec<String> {
534 match key {
535 PersistedIndexKeySnapshot::FieldPath(paths) => paths
536 .iter()
537 .map(|field_path| field_path.path().join("."))
538 .collect(),
539 PersistedIndexKeySnapshot::Items(items) => items
540 .iter()
541 .map(|item| match item {
542 PersistedIndexKeyItemSnapshot::FieldPath(field_path) => field_path.path().join("."),
543 PersistedIndexKeyItemSnapshot::Expression(expression) => {
544 expression.canonical_text().to_string()
545 }
546 })
547 .collect(),
548 }
549}
550
551#[must_use]
555pub(in crate::db) fn describe_entity_fields(model: &EntityModel) -> Vec<EntityFieldDescription> {
556 describe_entity_fields_with_slot_lookup(model, |slot, _field| {
557 Some(u16::try_from(slot).expect("generated field slot should fit in u16"))
558 })
559}
560
561#[cfg_attr(
562 doc,
563 doc = "Build field descriptors using accepted persisted schema slot metadata."
564)]
565#[must_use]
566pub(in crate::db) fn describe_entity_fields_with_persisted_schema(
567 schema: &AcceptedSchemaSnapshot,
568) -> Vec<EntityFieldDescription> {
569 let snapshot = schema.persisted_snapshot();
570 let mut fields = Vec::with_capacity(snapshot.fields().len());
571
572 for field in snapshot.fields() {
575 let primary_key = snapshot.primary_key_field_ids().contains(&field.id());
576 let slot = snapshot
577 .row_layout()
578 .slot_for_field(field.id())
579 .map(SchemaFieldSlot::get);
580 let mut kind = summarize_persisted_field_kind(field.kind());
581 write_schema_default_summary(&mut kind, field.default());
582 let metadata = DescribeFieldMetadata::new(
583 kind,
584 field.nullable(),
585 field_type_from_persisted_kind(field.kind())
586 .value_kind()
587 .is_queryable(),
588 field_origin_label(field.generated()),
589 );
590
591 push_described_field_row(&mut fields, field.name(), slot, primary_key, None, metadata);
592
593 if !field.nested_leaves().is_empty() {
594 describe_persisted_nested_leaves(
595 &mut fields,
596 field.nested_leaves(),
597 field_origin_label(field.generated()),
598 );
599 }
600 }
601
602 fields
603}
604
605fn describe_entity_fields_with_slot_lookup(
609 model: &EntityModel,
610 mut slot_for_field: impl FnMut(usize, &FieldModel) -> Option<u16>,
611) -> Vec<EntityFieldDescription> {
612 let mut fields = Vec::with_capacity(model.fields.len());
613 let primary_key_fields = primary_key_field_names_from_model(model);
614
615 for (slot, field) in model.fields.iter().enumerate() {
616 let primary_key = primary_key_fields
617 .iter()
618 .any(|primary_key_field| primary_key_field == field.name);
619 describe_field_recursive(
620 &mut fields,
621 field.name,
622 slot_for_field(slot, field),
623 field,
624 primary_key,
625 None,
626 None,
627 );
628 }
629
630 fields
631}
632
633struct DescribeFieldMetadata {
642 kind: String,
643 nullable: bool,
644 queryable: bool,
645 origin: String,
646}
647
648impl DescribeFieldMetadata {
649 const fn new(kind: String, nullable: bool, queryable: bool, origin: String) -> Self {
651 Self {
652 kind,
653 nullable,
654 queryable,
655 origin,
656 }
657 }
658}
659
660fn describe_field_recursive(
663 fields: &mut Vec<EntityFieldDescription>,
664 name: &str,
665 slot: Option<u16>,
666 field: &FieldModel,
667 primary_key: bool,
668 tree_prefix: Option<&'static str>,
669 metadata_override: Option<DescribeFieldMetadata>,
670) {
671 let metadata = metadata_override.unwrap_or_else(|| {
672 let mut kind = summarize_field_kind(&field.kind);
673 write_model_default_summary(&mut kind, field.database_default());
674
675 DescribeFieldMetadata::new(
676 kind,
677 field.nullable(),
678 field.kind.value_kind().is_queryable(),
679 "generated".to_string(),
680 )
681 });
682
683 push_described_field_row(fields, name, slot, primary_key, tree_prefix, metadata);
684 describe_generated_nested_fields(fields, field.nested_fields());
685}
686
687fn push_described_field_row(
690 fields: &mut Vec<EntityFieldDescription>,
691 name: &str,
692 slot: Option<u16>,
693 primary_key: bool,
694 tree_prefix: Option<&'static str>,
695 metadata: DescribeFieldMetadata,
696) {
697 let display_name = if let Some(prefix) = tree_prefix {
700 format!("{prefix}{name}")
701 } else {
702 name.to_string()
703 };
704
705 fields.push(EntityFieldDescription::new(
706 display_name,
707 slot,
708 metadata.kind,
709 metadata.nullable,
710 primary_key,
711 metadata.queryable,
712 metadata.origin,
713 ));
714}
715
716fn describe_generated_nested_fields(
720 fields: &mut Vec<EntityFieldDescription>,
721 nested_fields: &[FieldModel],
722) {
723 for (index, nested) in nested_fields.iter().enumerate() {
724 let prefix = if index + 1 == nested_fields.len() {
725 "└─ "
726 } else {
727 "├─ "
728 };
729 describe_field_recursive(
730 fields,
731 nested.name(),
732 None,
733 nested,
734 false,
735 Some(prefix),
736 None,
737 );
738 }
739}
740
741fn describe_persisted_nested_leaves(
744 fields: &mut Vec<EntityFieldDescription>,
745 nested_leaves: &[PersistedNestedLeafSnapshot],
746 origin: String,
747) {
748 for (index, leaf) in nested_leaves.iter().enumerate() {
749 let prefix = if index + 1 == nested_leaves.len() {
750 "└─ "
751 } else {
752 "├─ "
753 };
754 let name = leaf.path().last().map_or("", String::as_str);
755 let metadata = DescribeFieldMetadata::new(
756 summarize_persisted_field_kind(leaf.kind()),
757 leaf.nullable(),
758 field_type_from_persisted_kind(leaf.kind())
759 .value_kind()
760 .is_queryable(),
761 origin.clone(),
762 );
763
764 push_described_field_row(fields, name, None, false, Some(prefix), metadata);
765 }
766}
767
768fn field_origin_label(generated: bool) -> String {
769 if generated {
770 "generated".to_string()
771 } else {
772 "ddl".to_string()
773 }
774}
775
776fn describe_entity_relations_with_persisted_schema(
777 schema: &AcceptedSchemaSnapshot,
778) -> Vec<EntityRelationDescription> {
779 schema
780 .persisted_snapshot()
781 .fields()
782 .iter()
783 .filter_map(relation_description_from_persisted_field)
784 .collect()
785}
786
787fn relation_description_from_persisted_field(
788 field: &crate::db::schema::PersistedFieldSnapshot,
789) -> Option<EntityRelationDescription> {
790 let relation = persisted_relation_description_metadata(field.kind())?;
791
792 Some(EntityRelationDescription::new(
793 field.name().to_string(),
794 relation.target_path.to_string(),
795 relation.target_entity_name.to_string(),
796 relation.target_store_path.to_string(),
797 relation.strength,
798 relation.cardinality,
799 ))
800}
801
802struct PersistedRelationDescriptionMetadata<'a> {
803 target_path: &'a str,
804 target_entity_name: &'a str,
805 target_store_path: &'a str,
806 strength: EntityRelationStrength,
807 cardinality: EntityRelationCardinality,
808}
809
810fn persisted_relation_description_metadata(
811 kind: &PersistedFieldKind,
812) -> Option<PersistedRelationDescriptionMetadata<'_>> {
813 const fn from_relation_kind(
814 kind: &PersistedFieldKind,
815 cardinality: EntityRelationCardinality,
816 ) -> Option<PersistedRelationDescriptionMetadata<'_>> {
817 let PersistedFieldKind::Relation {
818 target_path,
819 target_entity_name,
820 target_store_path,
821 strength,
822 ..
823 } = kind
824 else {
825 return None;
826 };
827
828 Some(PersistedRelationDescriptionMetadata {
829 target_path: target_path.as_str(),
830 target_entity_name: target_entity_name.as_str(),
831 target_store_path: target_store_path.as_str(),
832 strength: entity_relation_strength_from_persisted(*strength),
833 cardinality,
834 })
835 }
836
837 match kind {
838 PersistedFieldKind::Relation { .. } => {
839 from_relation_kind(kind, EntityRelationCardinality::Single)
840 }
841 PersistedFieldKind::List(inner) => {
842 from_relation_kind(inner, EntityRelationCardinality::List)
843 }
844 PersistedFieldKind::Set(inner) => from_relation_kind(inner, EntityRelationCardinality::Set),
845 _ => None,
846 }
847}
848
849const fn entity_relation_strength_from_persisted(
850 strength: PersistedRelationStrength,
851) -> EntityRelationStrength {
852 match strength {
853 PersistedRelationStrength::Strong => EntityRelationStrength::Strong,
854 PersistedRelationStrength::Weak => EntityRelationStrength::Weak,
855 }
856}
857
858fn relation_description_from_metadata(
860 metadata: RelationFieldMetadata,
861) -> EntityRelationDescription {
862 let strength = match metadata.strength() {
863 RelationStrength::Strong => EntityRelationStrength::Strong,
864 RelationStrength::Weak => EntityRelationStrength::Weak,
865 };
866
867 let cardinality = match metadata.cardinality() {
868 RelationFieldCardinality::Single => EntityRelationCardinality::Single,
869 RelationFieldCardinality::List => EntityRelationCardinality::List,
870 RelationFieldCardinality::Set => EntityRelationCardinality::Set,
871 };
872
873 EntityRelationDescription::new(
874 metadata.field_name().to_string(),
875 metadata.target_path().to_string(),
876 metadata.target_entity_name().to_string(),
877 metadata.target_store_path().to_string(),
878 strength,
879 cardinality,
880 )
881}
882
883#[cfg_attr(doc, doc = "Render one stable field-kind label for describe output.")]
884fn summarize_field_kind(kind: &FieldKind) -> String {
885 let mut out = String::new();
886 write_field_kind_summary(&mut out, kind);
887
888 out
889}
890
891fn write_field_kind_summary(out: &mut String, kind: &FieldKind) {
894 if let Some(name) = kind.describe_kind_name() {
895 out.push_str(name);
896 return;
897 }
898
899 match kind {
900 FieldKind::Blob { max_len } => {
901 write_length_bounded_field_kind_summary(out, "blob", *max_len);
902 }
903 FieldKind::Decimal { scale } => {
904 let _ = write!(out, "decimal(scale={scale})");
905 }
906 FieldKind::IntBig { max_bytes } => {
907 write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
908 }
909 FieldKind::Enum { path, .. } => {
910 out.push_str("enum(");
911 out.push_str(path);
912 out.push(')');
913 }
914 FieldKind::Text { max_len } => {
915 write_length_bounded_field_kind_summary(out, "text", *max_len);
916 }
917 FieldKind::Relation {
918 target_entity_name,
919 key_kind,
920 strength,
921 ..
922 } => {
923 out.push_str("relation(target=");
924 out.push_str(target_entity_name);
925 out.push_str(", key=");
926 write_field_kind_summary(out, key_kind);
927 out.push_str(", strength=");
928 out.push_str(summarize_relation_strength(*strength));
929 out.push(')');
930 }
931 FieldKind::List(inner) => {
932 out.push_str("list<");
933 write_field_kind_summary(out, inner);
934 out.push('>');
935 }
936 FieldKind::Set(inner) => {
937 out.push_str("set<");
938 write_field_kind_summary(out, inner);
939 out.push('>');
940 }
941 FieldKind::Map { key, value } => {
942 out.push_str("map<");
943 write_field_kind_summary(out, key);
944 out.push_str(", ");
945 write_field_kind_summary(out, value);
946 out.push('>');
947 }
948 FieldKind::Structured { .. } => {
949 out.push_str("structured");
950 }
951 FieldKind::Account
952 | FieldKind::Bool
953 | FieldKind::Date
954 | FieldKind::Duration
955 | FieldKind::Float32
956 | FieldKind::Float64
957 | FieldKind::Int8
958 | FieldKind::Int16
959 | FieldKind::Int32
960 | FieldKind::Int64
961 | FieldKind::Int128
962 | FieldKind::Principal
963 | FieldKind::Subaccount
964 | FieldKind::Timestamp
965 | FieldKind::Nat8
966 | FieldKind::Nat16
967 | FieldKind::Nat32
968 | FieldKind::Nat64
969 | FieldKind::Nat128
970 | FieldKind::Ulid
971 | FieldKind::Unit => unreachable!("plain field kind labels return before recursive render"),
972 FieldKind::NatBig { max_bytes } => {
973 write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
974 }
975 }
976}
977
978trait DescribeKindName {
979 fn describe_kind_name(&self) -> Option<&'static str>;
980}
981
982impl DescribeKindName for FieldKind {
983 fn describe_kind_name(&self) -> Option<&'static str> {
984 Some(match self {
985 Self::Account => "account",
986 Self::Bool => "bool",
987 Self::Date => "date",
988 Self::Duration => "duration",
989 Self::Float32 => "float32",
990 Self::Float64 => "float64",
991 Self::Int8 => "int8",
992 Self::Int16 => "int16",
993 Self::Int32 => "int32",
994 Self::Int64 => "int64",
995 Self::Int128 => "int128",
996 Self::Principal => "principal",
997 Self::Subaccount => "subaccount",
998 Self::Timestamp => "timestamp",
999 Self::Nat8 => "nat8",
1000 Self::Nat16 => "nat16",
1001 Self::Nat32 => "nat32",
1002 Self::Nat64 => "nat64",
1003 Self::Nat128 => "nat128",
1004 Self::Ulid => "ulid",
1005 Self::Unit => "unit",
1006 Self::Blob { .. }
1007 | Self::Decimal { .. }
1008 | Self::Enum { .. }
1009 | Self::IntBig { .. }
1010 | Self::NatBig { .. }
1011 | Self::Text { .. }
1012 | Self::Relation { .. }
1013 | Self::List(_)
1014 | Self::Set(_)
1015 | Self::Map { .. }
1016 | Self::Structured { .. } => return None,
1017 })
1018 }
1019}
1020
1021fn write_length_bounded_field_kind_summary(
1025 out: &mut String,
1026 kind_name: &str,
1027 max_len: Option<u32>,
1028) {
1029 out.push_str(kind_name);
1030 if let Some(max_len) = max_len {
1031 out.push_str("(max_len=");
1032 out.push_str(&max_len.to_string());
1033 out.push(')');
1034 } else {
1035 out.push_str("(unbounded)");
1036 }
1037}
1038
1039fn write_byte_bounded_field_kind_summary(out: &mut String, kind_name: &str, max_bytes: u32) {
1040 out.push_str(kind_name);
1041 out.push_str("(max_bytes=");
1042 out.push_str(&max_bytes.to_string());
1043 out.push(')');
1044}
1045
1046fn write_model_default_summary(out: &mut String, default: FieldDatabaseDefault) {
1050 match default {
1051 FieldDatabaseDefault::None => {}
1052 FieldDatabaseDefault::EncodedSlotPayload(payload) => {
1053 write_encoded_default_payload_summary(out, payload);
1054 }
1055 }
1056}
1057
1058fn write_schema_default_summary(out: &mut String, default: &SchemaFieldDefault) {
1061 if let Some(payload) = default.slot_payload() {
1062 write_encoded_default_payload_summary(out, payload);
1063 }
1064}
1065
1066fn write_encoded_default_payload_summary(out: &mut String, payload: &[u8]) {
1070 let _ = write!(
1071 out,
1072 " default=slot_payload(bytes={}, sha256={})",
1073 payload.len(),
1074 short_default_payload_fingerprint(payload),
1075 );
1076}
1077
1078fn short_default_payload_fingerprint(payload: &[u8]) -> String {
1079 let digest = Sha256::digest(payload);
1080 let mut out = String::with_capacity(16);
1081 for byte in &digest[..8] {
1082 let _ = write!(out, "{byte:02x}");
1083 }
1084 out
1085}
1086
1087#[cfg_attr(
1088 doc,
1089 doc = "Render one stable field-kind label from accepted persisted schema metadata."
1090)]
1091fn summarize_persisted_field_kind(kind: &PersistedFieldKind) -> String {
1092 let mut out = String::new();
1093 write_persisted_field_kind_summary(&mut out, kind);
1094
1095 out
1096}
1097
1098fn write_persisted_field_kind_summary(out: &mut String, kind: &PersistedFieldKind) {
1102 if let Some(name) = kind.describe_kind_name() {
1103 out.push_str(name);
1104 return;
1105 }
1106
1107 match kind {
1108 PersistedFieldKind::Blob { max_len } => {
1109 write_length_bounded_field_kind_summary(out, "blob", *max_len);
1110 }
1111 PersistedFieldKind::Decimal { scale } => {
1112 let _ = write!(out, "decimal(scale={scale})");
1113 }
1114 PersistedFieldKind::IntBig { max_bytes } => {
1115 write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
1116 }
1117 PersistedFieldKind::Enum { path, .. } => {
1118 out.push_str("enum(");
1119 out.push_str(path);
1120 out.push(')');
1121 }
1122 PersistedFieldKind::Text { max_len } => {
1123 write_length_bounded_field_kind_summary(out, "text", *max_len);
1124 }
1125 PersistedFieldKind::Relation {
1126 target_entity_name,
1127 key_kind,
1128 strength,
1129 ..
1130 } => {
1131 out.push_str("relation(target=");
1132 out.push_str(target_entity_name);
1133 out.push_str(", key=");
1134 write_persisted_field_kind_summary(out, key_kind);
1135 out.push_str(", strength=");
1136 out.push_str(summarize_persisted_relation_strength(*strength));
1137 out.push(')');
1138 }
1139 PersistedFieldKind::List(inner) => {
1140 out.push_str("list<");
1141 write_persisted_field_kind_summary(out, inner);
1142 out.push('>');
1143 }
1144 PersistedFieldKind::Set(inner) => {
1145 out.push_str("set<");
1146 write_persisted_field_kind_summary(out, inner);
1147 out.push('>');
1148 }
1149 PersistedFieldKind::Map { key, value } => {
1150 out.push_str("map<");
1151 write_persisted_field_kind_summary(out, key);
1152 out.push_str(", ");
1153 write_persisted_field_kind_summary(out, value);
1154 out.push('>');
1155 }
1156 PersistedFieldKind::Structured { .. } => {
1157 out.push_str("structured");
1158 }
1159 PersistedFieldKind::Account
1160 | PersistedFieldKind::Bool
1161 | PersistedFieldKind::Date
1162 | PersistedFieldKind::Duration
1163 | PersistedFieldKind::Float32
1164 | PersistedFieldKind::Float64
1165 | PersistedFieldKind::Int8
1166 | PersistedFieldKind::Int16
1167 | PersistedFieldKind::Int32
1168 | PersistedFieldKind::Int64
1169 | PersistedFieldKind::Int128
1170 | PersistedFieldKind::Principal
1171 | PersistedFieldKind::Subaccount
1172 | PersistedFieldKind::Timestamp
1173 | PersistedFieldKind::Nat8
1174 | PersistedFieldKind::Nat16
1175 | PersistedFieldKind::Nat32
1176 | PersistedFieldKind::Nat64
1177 | PersistedFieldKind::Nat128
1178 | PersistedFieldKind::Ulid
1179 | PersistedFieldKind::Unit => {
1180 unreachable!("plain persisted field kind labels return before recursive render")
1181 }
1182 PersistedFieldKind::NatBig { max_bytes } => {
1183 write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
1184 }
1185 }
1186}
1187
1188impl DescribeKindName for PersistedFieldKind {
1189 fn describe_kind_name(&self) -> Option<&'static str> {
1190 Some(match self {
1191 Self::Account => "account",
1192 Self::Bool => "bool",
1193 Self::Date => "date",
1194 Self::Duration => "duration",
1195 Self::Float32 => "float32",
1196 Self::Float64 => "float64",
1197 Self::Int8 => "int8",
1198 Self::Int16 => "int16",
1199 Self::Int32 => "int32",
1200 Self::Int64 => "int64",
1201 Self::Int128 => "int128",
1202 Self::Principal => "principal",
1203 Self::Subaccount => "subaccount",
1204 Self::Timestamp => "timestamp",
1205 Self::Nat8 => "nat8",
1206 Self::Nat16 => "nat16",
1207 Self::Nat32 => "nat32",
1208 Self::Nat64 => "nat64",
1209 Self::Nat128 => "nat128",
1210 Self::Ulid => "ulid",
1211 Self::Unit => "unit",
1212 Self::Blob { .. }
1213 | Self::Decimal { .. }
1214 | Self::Enum { .. }
1215 | Self::IntBig { .. }
1216 | Self::NatBig { .. }
1217 | Self::Text { .. }
1218 | Self::Relation { .. }
1219 | Self::List(_)
1220 | Self::Set(_)
1221 | Self::Map { .. }
1222 | Self::Structured { .. } => return None,
1223 })
1224 }
1225}
1226
1227#[cfg_attr(
1228 doc,
1229 doc = "Render one stable relation-strength label from persisted schema metadata."
1230)]
1231const fn summarize_persisted_relation_strength(
1232 strength: PersistedRelationStrength,
1233) -> &'static str {
1234 match strength {
1235 PersistedRelationStrength::Strong => "strong",
1236 PersistedRelationStrength::Weak => "weak",
1237 }
1238}
1239
1240#[cfg_attr(
1241 doc,
1242 doc = "Render one stable relation-strength label for field-kind summaries."
1243)]
1244const fn summarize_relation_strength(strength: RelationStrength) -> &'static str {
1245 match strength {
1246 RelationStrength::Strong => "strong",
1247 RelationStrength::Weak => "weak",
1248 }
1249}
1250
1251#[cfg(test)]
1256mod tests;