1use crate::{
7 db::{
8 relation::{
9 RelationFieldCardinality, RelationFieldMetadata, relation_field_metadata_for_model_iter,
10 },
11 schema::{
12 AcceptedFieldKind, AcceptedSchemaSnapshot, PersistedIndexKeyItemSnapshot,
13 PersistedIndexKeySnapshot, PersistedNestedLeafSnapshot, SchemaFieldDefault,
14 SchemaFieldSlot, field_type_from_persisted_kind,
15 },
16 },
17 model::{
18 entity::EntityModel,
19 field::{FieldDatabaseDefault, FieldKind, FieldModel},
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) cardinality: EntityRelationCardinality,
318}
319
320impl EntityRelationDescription {
321 #[must_use]
323 pub const fn new(
324 field: String,
325 target_path: String,
326 target_entity_name: String,
327 target_store_path: String,
328 cardinality: EntityRelationCardinality,
329 ) -> Self {
330 Self {
331 field,
332 target_path,
333 target_entity_name,
334 target_store_path,
335 cardinality,
336 }
337 }
338
339 #[must_use]
341 pub const fn field(&self) -> &str {
342 self.field.as_str()
343 }
344
345 #[must_use]
347 pub const fn target_path(&self) -> &str {
348 self.target_path.as_str()
349 }
350
351 #[must_use]
353 pub const fn target_entity_name(&self) -> &str {
354 self.target_entity_name.as_str()
355 }
356
357 #[must_use]
359 pub const fn target_store_path(&self) -> &str {
360 self.target_store_path.as_str()
361 }
362
363 #[must_use]
365 pub const fn cardinality(&self) -> EntityRelationCardinality {
366 self.cardinality
367 }
368}
369
370#[cfg_attr(
371 doc,
372 doc = "EntityRelationCardinality\n\nDescribe relation cardinality."
373)]
374#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
375pub enum EntityRelationCardinality {
376 Single,
377 List,
378 Set,
379}
380
381#[cfg_attr(
382 doc,
383 doc = "Build one stable entity-schema description from one runtime `EntityModel`."
384)]
385#[must_use]
386pub(in crate::db) fn describe_entity_model(model: &EntityModel) -> EntitySchemaDescription {
387 let fields = describe_entity_fields(model);
388 let primary_key_fields = primary_key_field_names_from_model(model);
389 let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
390
391 describe_entity_model_from_description_rows(
392 model.path,
393 model.entity_name,
394 primary_key.as_str(),
395 primary_key_fields,
396 fields,
397 describe_entity_indexes_from_model(model),
398 describe_entity_relations_from_model(model),
399 )
400}
401
402#[cfg_attr(
403 doc,
404 doc = "Build one entity-schema description using accepted persisted schema slot metadata."
405)]
406#[must_use]
407pub(in crate::db) fn describe_entity_model_with_persisted_schema(
408 model: &EntityModel,
409 schema: &AcceptedSchemaSnapshot,
410) -> EntitySchemaDescription {
411 let fields = describe_entity_fields_with_persisted_schema(schema);
412 let primary_key_fields = schema.primary_key_field_names();
413 let primary_key_fields = if primary_key_fields.is_empty() {
414 vec![model.primary_key.name.to_string()]
415 } else {
416 primary_key_fields
417 .into_iter()
418 .map(str::to_string)
419 .collect::<Vec<_>>()
420 };
421 let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
422
423 describe_entity_model_from_description_rows(
424 schema.entity_path(),
425 schema.entity_name(),
426 primary_key.as_str(),
427 primary_key_fields,
428 fields,
429 describe_entity_indexes_with_persisted_schema(schema),
430 describe_entity_relations_with_persisted_schema(schema),
431 )
432}
433
434fn describe_entity_model_from_description_rows(
439 entity_path: &str,
440 entity_name: &str,
441 primary_key: &str,
442 primary_key_fields: Vec<String>,
443 fields: Vec<EntityFieldDescription>,
444 indexes: Vec<EntityIndexDescription>,
445 relations: Vec<EntityRelationDescription>,
446) -> EntitySchemaDescription {
447 EntitySchemaDescription::new_with_primary_key_fields(
448 entity_path.to_string(),
449 entity_name.to_string(),
450 primary_key.to_string(),
451 primary_key_fields,
452 fields,
453 indexes,
454 relations,
455 )
456}
457
458fn describe_entity_relations_from_model(model: &EntityModel) -> Vec<EntityRelationDescription> {
459 relation_field_metadata_for_model_iter(model)
460 .map(relation_description_from_metadata)
461 .collect()
462}
463
464fn primary_key_field_names_from_model(model: &EntityModel) -> Vec<String> {
465 model
466 .primary_key_model()
467 .fields()
468 .iter()
469 .map(|field| field.name.to_string())
470 .collect()
471}
472
473fn render_primary_key_fields(fields: &[String]) -> String {
474 fields.join(", ")
475}
476
477fn describe_entity_indexes_from_model(model: &EntityModel) -> Vec<EntityIndexDescription> {
478 let mut indexes = Vec::with_capacity(model.indexes.len());
479 for index in model.indexes {
480 indexes.push(EntityIndexDescription::new(
481 index.name().to_string(),
482 index.is_unique(),
483 index
484 .fields()
485 .iter()
486 .map(|field| (*field).to_string())
487 .collect(),
488 "generated".to_string(),
489 ));
490 }
491
492 indexes
493}
494
495fn describe_entity_indexes_with_persisted_schema(
496 schema: &AcceptedSchemaSnapshot,
497) -> Vec<EntityIndexDescription> {
498 schema
499 .persisted_snapshot()
500 .indexes()
501 .iter()
502 .map(|index| {
503 EntityIndexDescription::new(
504 index.name().to_string(),
505 index.unique(),
506 describe_persisted_index_fields(index.key()),
507 if index.generated() {
508 "generated".to_string()
509 } else {
510 "ddl".to_string()
511 },
512 )
513 })
514 .collect()
515}
516
517fn describe_persisted_index_fields(key: &PersistedIndexKeySnapshot) -> Vec<String> {
518 match key {
519 PersistedIndexKeySnapshot::FieldPath(paths) => paths
520 .iter()
521 .map(|field_path| field_path.path().join("."))
522 .collect(),
523 PersistedIndexKeySnapshot::Items(items) => items
524 .iter()
525 .map(|item| match item {
526 PersistedIndexKeyItemSnapshot::FieldPath(field_path) => field_path.path().join("."),
527 PersistedIndexKeyItemSnapshot::Expression(expression) => {
528 expression.canonical_text().to_string()
529 }
530 })
531 .collect(),
532 }
533}
534
535#[must_use]
539pub(in crate::db) fn describe_entity_fields(model: &EntityModel) -> Vec<EntityFieldDescription> {
540 describe_entity_fields_with_slot_lookup(model, |slot, _field| {
541 Some(u16::try_from(slot).expect("generated field slot should fit in u16"))
542 })
543}
544
545#[cfg_attr(
546 doc,
547 doc = "Build field descriptors using accepted persisted schema slot metadata."
548)]
549#[must_use]
550pub(in crate::db) fn describe_entity_fields_with_persisted_schema(
551 schema: &AcceptedSchemaSnapshot,
552) -> Vec<EntityFieldDescription> {
553 let snapshot = schema.persisted_snapshot();
554 let mut fields = Vec::with_capacity(snapshot.fields().len());
555
556 for field in snapshot.fields() {
559 let primary_key = snapshot.primary_key_field_ids().contains(&field.id());
560 let slot = snapshot
561 .row_layout()
562 .slot_for_field(field.id())
563 .map(SchemaFieldSlot::get);
564 let mut kind = summarize_persisted_field_kind(field.kind());
565 write_schema_default_summary(&mut kind, field.default());
566 let metadata = DescribeFieldMetadata::new(
567 kind,
568 field.nullable(),
569 field_type_from_persisted_kind(field.kind())
570 .value_kind()
571 .is_queryable(),
572 field_origin_label(field.generated()),
573 );
574
575 push_described_field_row(&mut fields, field.name(), slot, primary_key, None, metadata);
576
577 if !field.nested_leaves().is_empty() {
578 describe_persisted_nested_leaves(
579 &mut fields,
580 field.nested_leaves(),
581 field_origin_label(field.generated()),
582 );
583 }
584 }
585
586 fields
587}
588
589fn describe_entity_fields_with_slot_lookup(
593 model: &EntityModel,
594 mut slot_for_field: impl FnMut(usize, &FieldModel) -> Option<u16>,
595) -> Vec<EntityFieldDescription> {
596 let mut fields = Vec::with_capacity(model.fields.len());
597 let primary_key_fields = primary_key_field_names_from_model(model);
598
599 for (slot, field) in model.fields.iter().enumerate() {
600 let primary_key = primary_key_fields
601 .iter()
602 .any(|primary_key_field| primary_key_field == field.name);
603 describe_field_recursive(
604 &mut fields,
605 field.name,
606 slot_for_field(slot, field),
607 field,
608 primary_key,
609 None,
610 None,
611 );
612 }
613
614 fields
615}
616
617struct DescribeFieldMetadata {
626 kind: String,
627 nullable: bool,
628 queryable: bool,
629 origin: String,
630}
631
632impl DescribeFieldMetadata {
633 const fn new(kind: String, nullable: bool, queryable: bool, origin: String) -> Self {
635 Self {
636 kind,
637 nullable,
638 queryable,
639 origin,
640 }
641 }
642}
643
644fn describe_field_recursive(
647 fields: &mut Vec<EntityFieldDescription>,
648 name: &str,
649 slot: Option<u16>,
650 field: &FieldModel,
651 primary_key: bool,
652 tree_prefix: Option<&'static str>,
653 metadata_override: Option<DescribeFieldMetadata>,
654) {
655 let metadata = metadata_override.unwrap_or_else(|| {
656 let mut kind = summarize_field_kind(&field.kind);
657 write_model_default_summary(&mut kind, field.database_default());
658
659 DescribeFieldMetadata::new(
660 kind,
661 field.nullable(),
662 field.kind.value_kind().is_queryable(),
663 "generated".to_string(),
664 )
665 });
666
667 push_described_field_row(fields, name, slot, primary_key, tree_prefix, metadata);
668 describe_generated_nested_fields(fields, field.nested_fields());
669}
670
671fn push_described_field_row(
674 fields: &mut Vec<EntityFieldDescription>,
675 name: &str,
676 slot: Option<u16>,
677 primary_key: bool,
678 tree_prefix: Option<&'static str>,
679 metadata: DescribeFieldMetadata,
680) {
681 let display_name = if let Some(prefix) = tree_prefix {
684 format!("{prefix}{name}")
685 } else {
686 name.to_string()
687 };
688
689 fields.push(EntityFieldDescription::new(
690 display_name,
691 slot,
692 metadata.kind,
693 metadata.nullable,
694 primary_key,
695 metadata.queryable,
696 metadata.origin,
697 ));
698}
699
700fn describe_generated_nested_fields(
704 fields: &mut Vec<EntityFieldDescription>,
705 nested_fields: &[FieldModel],
706) {
707 for (index, nested) in nested_fields.iter().enumerate() {
708 let prefix = if index + 1 == nested_fields.len() {
709 "└─ "
710 } else {
711 "├─ "
712 };
713 describe_field_recursive(
714 fields,
715 nested.name(),
716 None,
717 nested,
718 false,
719 Some(prefix),
720 None,
721 );
722 }
723}
724
725fn describe_persisted_nested_leaves(
728 fields: &mut Vec<EntityFieldDescription>,
729 nested_leaves: &[PersistedNestedLeafSnapshot],
730 origin: String,
731) {
732 for (index, leaf) in nested_leaves.iter().enumerate() {
733 let prefix = if index + 1 == nested_leaves.len() {
734 "└─ "
735 } else {
736 "├─ "
737 };
738 let name = leaf.path().last().map_or("", String::as_str);
739 let metadata = DescribeFieldMetadata::new(
740 summarize_persisted_field_kind(leaf.kind()),
741 leaf.nullable(),
742 field_type_from_persisted_kind(leaf.kind())
743 .value_kind()
744 .is_queryable(),
745 origin.clone(),
746 );
747
748 push_described_field_row(fields, name, None, false, Some(prefix), metadata);
749 }
750}
751
752fn field_origin_label(generated: bool) -> String {
753 if generated {
754 "generated".to_string()
755 } else {
756 "ddl".to_string()
757 }
758}
759
760fn describe_entity_relations_with_persisted_schema(
761 schema: &AcceptedSchemaSnapshot,
762) -> Vec<EntityRelationDescription> {
763 schema
764 .persisted_snapshot()
765 .fields()
766 .iter()
767 .filter_map(relation_description_from_persisted_field)
768 .collect()
769}
770
771fn relation_description_from_persisted_field(
772 field: &crate::db::schema::PersistedFieldSnapshot,
773) -> Option<EntityRelationDescription> {
774 let relation = persisted_relation_description_metadata(field.kind())?;
775
776 Some(EntityRelationDescription::new(
777 field.name().to_string(),
778 relation.target_path.to_string(),
779 relation.target_entity_name.to_string(),
780 relation.target_store_path.to_string(),
781 relation.cardinality,
782 ))
783}
784
785struct PersistedRelationDescriptionMetadata<'a> {
786 target_path: &'a str,
787 target_entity_name: &'a str,
788 target_store_path: &'a str,
789 cardinality: EntityRelationCardinality,
790}
791
792fn persisted_relation_description_metadata(
793 kind: &AcceptedFieldKind,
794) -> Option<PersistedRelationDescriptionMetadata<'_>> {
795 const fn from_relation_kind(
796 kind: &AcceptedFieldKind,
797 cardinality: EntityRelationCardinality,
798 ) -> Option<PersistedRelationDescriptionMetadata<'_>> {
799 let AcceptedFieldKind::Relation {
800 target_path,
801 target_entity_name,
802 target_store_path,
803 ..
804 } = kind
805 else {
806 return None;
807 };
808
809 Some(PersistedRelationDescriptionMetadata {
810 target_path: target_path.as_str(),
811 target_entity_name: target_entity_name.as_str(),
812 target_store_path: target_store_path.as_str(),
813 cardinality,
814 })
815 }
816
817 match kind {
818 AcceptedFieldKind::Relation { .. } => {
819 from_relation_kind(kind, EntityRelationCardinality::Single)
820 }
821 AcceptedFieldKind::List(inner) => {
822 from_relation_kind(inner, EntityRelationCardinality::List)
823 }
824 AcceptedFieldKind::Set(inner) => from_relation_kind(inner, EntityRelationCardinality::Set),
825 _ => None,
826 }
827}
828
829fn relation_description_from_metadata(
831 metadata: RelationFieldMetadata,
832) -> EntityRelationDescription {
833 let cardinality = match metadata.cardinality() {
834 RelationFieldCardinality::Single => EntityRelationCardinality::Single,
835 RelationFieldCardinality::List => EntityRelationCardinality::List,
836 RelationFieldCardinality::Set => EntityRelationCardinality::Set,
837 };
838
839 EntityRelationDescription::new(
840 metadata.field_name().to_string(),
841 metadata.target_path().to_string(),
842 metadata.target_entity_name().to_string(),
843 metadata.target_store_path().to_string(),
844 cardinality,
845 )
846}
847
848#[cfg_attr(doc, doc = "Render one stable field-kind label for describe output.")]
849fn summarize_field_kind(kind: &FieldKind) -> String {
850 let mut out = String::new();
851 write_field_kind_summary(&mut out, kind);
852
853 out
854}
855
856fn write_field_kind_summary(out: &mut String, kind: &FieldKind) {
859 if let Some(name) = kind.describe_kind_name() {
860 out.push_str(name);
861 return;
862 }
863
864 match kind {
865 FieldKind::Blob { max_len } => {
866 write_length_bounded_field_kind_summary(out, "blob", *max_len);
867 }
868 FieldKind::Decimal { scale } => {
869 let _ = write!(out, "decimal(scale={scale})");
870 }
871 FieldKind::IntBig { max_bytes } => {
872 write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
873 }
874 FieldKind::Enum { path, .. } => {
875 out.push_str("enum(");
876 out.push_str(path);
877 out.push(')');
878 }
879 FieldKind::Text { max_len } => {
880 write_length_bounded_field_kind_summary(out, "text", *max_len);
881 }
882 FieldKind::Relation {
883 target_entity_name,
884 key_kind,
885 ..
886 } => {
887 out.push_str("relation(target=");
888 out.push_str(target_entity_name);
889 out.push_str(", key=");
890 write_field_kind_summary(out, key_kind);
891 out.push(')');
892 }
893 FieldKind::List(inner) => {
894 out.push_str("list<");
895 write_field_kind_summary(out, inner);
896 out.push('>');
897 }
898 FieldKind::Set(inner) => {
899 out.push_str("set<");
900 write_field_kind_summary(out, inner);
901 out.push('>');
902 }
903 FieldKind::Map { key, value } => {
904 out.push_str("map<");
905 write_field_kind_summary(out, key);
906 out.push_str(", ");
907 write_field_kind_summary(out, value);
908 out.push('>');
909 }
910 FieldKind::Structured { .. } => {
911 out.push_str("structured");
912 }
913 FieldKind::Account
914 | FieldKind::Bool
915 | FieldKind::Date
916 | FieldKind::Duration
917 | FieldKind::Float32
918 | FieldKind::Float64
919 | FieldKind::Int8
920 | FieldKind::Int16
921 | FieldKind::Int32
922 | FieldKind::Int64
923 | FieldKind::Int128
924 | FieldKind::Principal
925 | FieldKind::Subaccount
926 | FieldKind::Timestamp
927 | FieldKind::Nat8
928 | FieldKind::Nat16
929 | FieldKind::Nat32
930 | FieldKind::Nat64
931 | FieldKind::Nat128
932 | FieldKind::Ulid
933 | FieldKind::Unit => unreachable!("schema describe invariant"),
934 FieldKind::NatBig { max_bytes } => {
935 write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
936 }
937 }
938}
939
940trait DescribeKindName {
941 fn describe_kind_name(&self) -> Option<&'static str>;
942}
943
944impl DescribeKindName for FieldKind {
945 fn describe_kind_name(&self) -> Option<&'static str> {
946 Some(match self {
947 Self::Account => "account",
948 Self::Bool => "bool",
949 Self::Date => "date",
950 Self::Duration => "duration",
951 Self::Float32 => "float32",
952 Self::Float64 => "float64",
953 Self::Int8 => "int8",
954 Self::Int16 => "int16",
955 Self::Int32 => "int32",
956 Self::Int64 => "int64",
957 Self::Int128 => "int128",
958 Self::Principal => "principal",
959 Self::Subaccount => "subaccount",
960 Self::Timestamp => "timestamp",
961 Self::Nat8 => "nat8",
962 Self::Nat16 => "nat16",
963 Self::Nat32 => "nat32",
964 Self::Nat64 => "nat64",
965 Self::Nat128 => "nat128",
966 Self::Ulid => "ulid",
967 Self::Unit => "unit",
968 Self::Blob { .. }
969 | Self::Decimal { .. }
970 | Self::Enum { .. }
971 | Self::IntBig { .. }
972 | Self::NatBig { .. }
973 | Self::Text { .. }
974 | Self::Relation { .. }
975 | Self::List(_)
976 | Self::Set(_)
977 | Self::Map { .. }
978 | Self::Structured { .. } => return None,
979 })
980 }
981}
982
983fn write_length_bounded_field_kind_summary(
987 out: &mut String,
988 kind_name: &str,
989 max_len: Option<u32>,
990) {
991 out.push_str(kind_name);
992 if let Some(max_len) = max_len {
993 out.push_str("(max_len=");
994 out.push_str(&max_len.to_string());
995 out.push(')');
996 } else {
997 out.push_str("(unbounded)");
998 }
999}
1000
1001fn write_byte_bounded_field_kind_summary(out: &mut String, kind_name: &str, max_bytes: u32) {
1002 out.push_str(kind_name);
1003 out.push_str("(max_bytes=");
1004 out.push_str(&max_bytes.to_string());
1005 out.push(')');
1006}
1007
1008fn write_model_default_summary(out: &mut String, default: FieldDatabaseDefault) {
1012 match default {
1013 FieldDatabaseDefault::None => {}
1014 FieldDatabaseDefault::EncodedSlotPayload(payload) => {
1015 write_encoded_default_payload_summary(out, payload);
1016 }
1017 FieldDatabaseDefault::AuthoredEnumUnit { enum_path, variant } => {
1018 out.push_str(" default=enum_unit(");
1019 out.push_str(enum_path);
1020 out.push_str("::");
1021 out.push_str(variant);
1022 out.push(')');
1023 }
1024 }
1025}
1026
1027fn write_schema_default_summary(out: &mut String, default: &SchemaFieldDefault) {
1031 if let Some(payload) = default.slot_payload() {
1032 write_encoded_default_payload_summary(out, payload);
1033 }
1034}
1035
1036fn write_encoded_default_payload_summary(out: &mut String, payload: &[u8]) {
1040 let _ = write!(
1041 out,
1042 " default=slot_payload(bytes={}, sha256={})",
1043 payload.len(),
1044 short_default_payload_fingerprint(payload),
1045 );
1046}
1047
1048fn short_default_payload_fingerprint(payload: &[u8]) -> String {
1049 let digest = Sha256::digest(payload);
1050 let mut out = String::with_capacity(16);
1051 for byte in &digest[..8] {
1052 let _ = write!(out, "{byte:02x}");
1053 }
1054 out
1055}
1056
1057#[cfg_attr(
1058 doc,
1059 doc = "Render one stable field-kind label from accepted persisted schema metadata."
1060)]
1061fn summarize_persisted_field_kind(kind: &AcceptedFieldKind) -> String {
1062 let mut out = String::new();
1063 write_persisted_field_kind_summary(&mut out, kind);
1064
1065 out
1066}
1067
1068fn write_persisted_field_kind_summary(out: &mut String, kind: &AcceptedFieldKind) {
1072 if let Some(name) = kind.describe_kind_name() {
1073 out.push_str(name);
1074 return;
1075 }
1076
1077 match kind {
1078 AcceptedFieldKind::Blob { max_len } => {
1079 write_length_bounded_field_kind_summary(out, "blob", *max_len);
1080 }
1081 AcceptedFieldKind::Decimal { scale } => {
1082 let _ = write!(out, "decimal(scale={scale})");
1083 }
1084 AcceptedFieldKind::IntBig { max_bytes } => {
1085 write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
1086 }
1087 AcceptedFieldKind::Enum { type_id } => {
1088 let _ = write!(out, "enum(type_id={})", type_id.get());
1089 }
1090 AcceptedFieldKind::Text { max_len } => {
1091 write_length_bounded_field_kind_summary(out, "text", *max_len);
1092 }
1093 AcceptedFieldKind::Relation {
1094 target_entity_name,
1095 key_kind,
1096 ..
1097 } => {
1098 out.push_str("relation(target=");
1099 out.push_str(target_entity_name);
1100 out.push_str(", key=");
1101 write_persisted_field_kind_summary(out, key_kind);
1102 out.push(')');
1103 }
1104 AcceptedFieldKind::List(inner) => {
1105 out.push_str("list<");
1106 write_persisted_field_kind_summary(out, inner);
1107 out.push('>');
1108 }
1109 AcceptedFieldKind::Set(inner) => {
1110 out.push_str("set<");
1111 write_persisted_field_kind_summary(out, inner);
1112 out.push('>');
1113 }
1114 AcceptedFieldKind::Map { key, value } => {
1115 out.push_str("map<");
1116 write_persisted_field_kind_summary(out, key);
1117 out.push_str(", ");
1118 write_persisted_field_kind_summary(out, value);
1119 out.push('>');
1120 }
1121 AcceptedFieldKind::Structured { .. } => {
1122 out.push_str("structured");
1123 }
1124 AcceptedFieldKind::Account
1125 | AcceptedFieldKind::Bool
1126 | AcceptedFieldKind::Date
1127 | AcceptedFieldKind::Duration
1128 | AcceptedFieldKind::Float32
1129 | AcceptedFieldKind::Float64
1130 | AcceptedFieldKind::Int8
1131 | AcceptedFieldKind::Int16
1132 | AcceptedFieldKind::Int32
1133 | AcceptedFieldKind::Int64
1134 | AcceptedFieldKind::Int128
1135 | AcceptedFieldKind::Principal
1136 | AcceptedFieldKind::Subaccount
1137 | AcceptedFieldKind::Timestamp
1138 | AcceptedFieldKind::Nat8
1139 | AcceptedFieldKind::Nat16
1140 | AcceptedFieldKind::Nat32
1141 | AcceptedFieldKind::Nat64
1142 | AcceptedFieldKind::Nat128
1143 | AcceptedFieldKind::Ulid
1144 | AcceptedFieldKind::Unit => unreachable!("schema describe invariant"),
1145 AcceptedFieldKind::NatBig { max_bytes } => {
1146 write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
1147 }
1148 }
1149}
1150
1151impl DescribeKindName for AcceptedFieldKind {
1152 fn describe_kind_name(&self) -> Option<&'static str> {
1153 Some(match self {
1154 Self::Account => "account",
1155 Self::Bool => "bool",
1156 Self::Date => "date",
1157 Self::Duration => "duration",
1158 Self::Float32 => "float32",
1159 Self::Float64 => "float64",
1160 Self::Int8 => "int8",
1161 Self::Int16 => "int16",
1162 Self::Int32 => "int32",
1163 Self::Int64 => "int64",
1164 Self::Int128 => "int128",
1165 Self::Principal => "principal",
1166 Self::Subaccount => "subaccount",
1167 Self::Timestamp => "timestamp",
1168 Self::Nat8 => "nat8",
1169 Self::Nat16 => "nat16",
1170 Self::Nat32 => "nat32",
1171 Self::Nat64 => "nat64",
1172 Self::Nat128 => "nat128",
1173 Self::Ulid => "ulid",
1174 Self::Unit => "unit",
1175 Self::Blob { .. }
1176 | Self::Decimal { .. }
1177 | Self::Enum { .. }
1178 | Self::IntBig { .. }
1179 | Self::NatBig { .. }
1180 | Self::Text { .. }
1181 | Self::Relation { .. }
1182 | Self::List(_)
1183 | Self::Set(_)
1184 | Self::Map { .. }
1185 | Self::Structured { .. } => return None,
1186 })
1187 }
1188}
1189
1190#[cfg(test)]
1195mod tests;