1use crate::{
7 db::{
8 data::decode_admitted_value_from_accepted_field_contract,
9 relation::{
10 RelationFieldCardinality, RelationFieldMetadata, relation_field_metadata_for_model_iter,
11 },
12 schema::{
13 AcceptedFieldKind, AcceptedFieldPersistenceContract, AcceptedInsertOmissionPolicy,
14 AcceptedRowLayoutRuntimeContract, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
15 PersistedIndexKeyItemSnapshot, PersistedIndexKeySnapshot, PersistedNestedLeafSnapshot,
16 SchemaHistoricalFill,
17 composite_catalog::{AcceptedCompositeElement, AcceptedCompositeShape},
18 field_type_from_persisted_kind, output_value_from_runtime,
19 runtime::AcceptedRowLayoutRuntimeField,
20 },
21 },
22 error::InternalError,
23 model::{
24 entity::EntityModel,
25 field::{
26 CompositeCodec, CompositeElementModel, CompositeShapeModel, FieldDatabaseDefault,
27 FieldKind, FieldModel,
28 },
29 },
30 value::{OutputValue, render_output_value_text},
31};
32use std::fmt::Write;
33
34use candid::CandidType;
35use serde::Deserialize;
36use sha2::{Digest, Sha256};
37
38const ENTITY_FIELD_DESCRIPTION_NO_SLOT: u16 = u16::MAX;
39const MAX_SCHEMA_VALUE_RENDER_CHARS: usize = 128;
40
41#[cfg_attr(
42 doc,
43 doc = "EntitySchemaDescription\n\nStable describe payload for one entity model."
44)]
45#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
46pub struct EntitySchemaDescription {
47 pub(crate) entity_path: String,
48 pub(crate) entity_name: String,
49 pub(crate) primary_key: String,
50 pub(crate) primary_key_fields: Vec<String>,
51 pub(crate) fields: Vec<EntityFieldDescription>,
52 pub(crate) indexes: Vec<EntityIndexDescription>,
53 pub(crate) relations: Vec<EntityRelationDescription>,
54 pub(crate) row_layout_current: u32,
55 pub(crate) row_layout_history_floor: u32,
56}
57
58#[cfg_attr(
59 doc,
60 doc = "EntitySchemaCheckDescription\n\nGenerated-vs-accepted schema description payload for one entity."
61)]
62#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
63pub struct EntitySchemaCheckDescription {
64 pub(crate) generated: EntitySchemaDescription,
65 pub(crate) accepted: EntitySchemaDescription,
66}
67
68impl EntitySchemaCheckDescription {
69 #[must_use]
71 pub const fn new(
72 generated: EntitySchemaDescription,
73 accepted: EntitySchemaDescription,
74 ) -> Self {
75 Self {
76 generated,
77 accepted,
78 }
79 }
80
81 #[must_use]
83 pub const fn generated(&self) -> &EntitySchemaDescription {
84 &self.generated
85 }
86
87 #[must_use]
89 pub const fn accepted(&self) -> &EntitySchemaDescription {
90 &self.accepted
91 }
92}
93
94impl EntitySchemaDescription {
95 #[expect(
97 clippy::too_many_arguments,
98 reason = "schema description construction keeps identity, collections, and layout explicit"
99 )]
100 #[must_use]
101 pub const fn new(
102 entity_path: String,
103 entity_name: String,
104 primary_key: String,
105 primary_key_fields: Vec<String>,
106 fields: Vec<EntityFieldDescription>,
107 indexes: Vec<EntityIndexDescription>,
108 relations: Vec<EntityRelationDescription>,
109 row_layout_current: u32,
110 row_layout_history_floor: u32,
111 ) -> Self {
112 Self {
113 entity_path,
114 entity_name,
115 primary_key,
116 primary_key_fields,
117 fields,
118 indexes,
119 relations,
120 row_layout_current,
121 row_layout_history_floor,
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 #[must_use]
169 pub const fn row_layout_current(&self) -> u32 {
170 self.row_layout_current
171 }
172
173 #[must_use]
175 pub const fn row_layout_history_floor(&self) -> u32 {
176 self.row_layout_history_floor
177 }
178}
179
180#[cfg_attr(
181 doc,
182 doc = "EntityFieldDescription\n\nOne field entry in a describe payload."
183)]
184#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
185pub struct EntityFieldDescription {
186 pub(crate) name: String,
187 pub(crate) slot: u16,
188 pub(crate) kind: String,
189 pub(crate) nullable: bool,
190 pub(crate) primary_key: bool,
191 pub(crate) queryable: bool,
192 pub(crate) origin: String,
193 pub(crate) insert_omission: Option<String>,
194 pub(crate) insert_default: Option<String>,
195 pub(crate) insert_default_bytes: Option<u32>,
196 pub(crate) insert_default_hash: Option<String>,
197 pub(crate) introduced_in_layout: Option<u32>,
198 pub(crate) historical_fill: Option<String>,
199 pub(crate) historical_fill_bytes: Option<u32>,
200 pub(crate) historical_fill_hash: Option<String>,
201}
202
203struct EntityFieldTemporalFacts {
211 insert_omission: Option<String>,
212 insert_default: Option<String>,
213 insert_default_bytes: Option<u32>,
214 insert_default_hash: Option<String>,
215 introduced_in_layout: Option<u32>,
216 historical_fill: Option<String>,
217 historical_fill_bytes: Option<u32>,
218 historical_fill_hash: Option<String>,
219}
220
221impl EntityFieldTemporalFacts {
222 const fn nested() -> Self {
223 Self {
224 insert_omission: None,
225 insert_default: None,
226 insert_default_bytes: None,
227 insert_default_hash: None,
228 introduced_in_layout: None,
229 historical_fill: None,
230 historical_fill_bytes: None,
231 historical_fill_hash: None,
232 }
233 }
234
235 fn generated(field: &FieldModel) -> Self {
236 let insert_omission = if field.insert_generation().is_some() {
237 "generated"
238 } else if field.write_management().is_some() {
239 "managed"
240 } else {
241 match field.database_default() {
242 FieldDatabaseDefault::EncodedSlotPayload(_)
243 | FieldDatabaseDefault::AuthoredEnumUnit { .. } => "default",
244 FieldDatabaseDefault::None if field.nullable() => "null",
245 FieldDatabaseDefault::None => "required",
246 }
247 };
248 let (insert_default, insert_default_bytes, insert_default_hash) =
249 generated_insert_default_facts(field.database_default());
250
251 Self {
252 insert_omission: Some(insert_omission.to_string()),
253 insert_default,
254 insert_default_bytes,
255 insert_default_hash,
256 introduced_in_layout: Some(1),
257 historical_fill: Some("reject".to_string()),
258 historical_fill_bytes: None,
259 historical_fill_hash: None,
260 }
261 }
262}
263
264impl EntityFieldDescription {
265 #[expect(
267 clippy::too_many_arguments,
268 reason = "schema description construction keeps every temporal field fact explicit"
269 )]
270 #[must_use]
271 pub fn new(
272 name: String,
273 slot: Option<u16>,
274 kind: String,
275 nullable: bool,
276 primary_key: bool,
277 queryable: bool,
278 origin: String,
279 insert_omission: Option<String>,
280 insert_default: Option<String>,
281 insert_default_bytes: Option<u32>,
282 insert_default_hash: Option<String>,
283 introduced_in_layout: Option<u32>,
284 historical_fill: Option<String>,
285 historical_fill_bytes: Option<u32>,
286 historical_fill_hash: Option<String>,
287 ) -> Self {
288 Self::new_with_temporal_facts(
289 name,
290 slot,
291 primary_key,
292 DescribeFieldMetadata::new(kind, nullable, queryable, origin),
293 EntityFieldTemporalFacts {
294 insert_omission,
295 insert_default,
296 insert_default_bytes,
297 insert_default_hash,
298 introduced_in_layout,
299 historical_fill,
300 historical_fill_bytes,
301 historical_fill_hash,
302 },
303 )
304 }
305
306 fn new_with_temporal_facts(
307 name: String,
308 slot: Option<u16>,
309 primary_key: bool,
310 metadata: DescribeFieldMetadata,
311 temporal: EntityFieldTemporalFacts,
312 ) -> Self {
313 let slot = match slot {
314 Some(slot) => slot,
315 None => ENTITY_FIELD_DESCRIPTION_NO_SLOT,
316 };
317
318 Self {
319 name,
320 slot,
321 kind: metadata.kind,
322 nullable: metadata.nullable,
323 primary_key,
324 queryable: metadata.queryable,
325 origin: metadata.origin,
326 insert_omission: temporal.insert_omission,
327 insert_default: temporal.insert_default,
328 insert_default_bytes: temporal.insert_default_bytes,
329 insert_default_hash: temporal.insert_default_hash,
330 introduced_in_layout: temporal.introduced_in_layout,
331 historical_fill: temporal.historical_fill,
332 historical_fill_bytes: temporal.historical_fill_bytes,
333 historical_fill_hash: temporal.historical_fill_hash,
334 }
335 }
336
337 #[must_use]
339 pub const fn name(&self) -> &str {
340 self.name.as_str()
341 }
342
343 #[must_use]
345 pub const fn slot(&self) -> Option<u16> {
346 if self.slot == ENTITY_FIELD_DESCRIPTION_NO_SLOT {
347 None
348 } else {
349 Some(self.slot)
350 }
351 }
352
353 #[must_use]
355 pub const fn kind(&self) -> &str {
356 self.kind.as_str()
357 }
358
359 #[must_use]
361 pub const fn nullable(&self) -> bool {
362 self.nullable
363 }
364
365 #[must_use]
367 pub const fn primary_key(&self) -> bool {
368 self.primary_key
369 }
370
371 #[must_use]
373 pub const fn queryable(&self) -> bool {
374 self.queryable
375 }
376
377 #[must_use]
379 pub const fn origin(&self) -> &str {
380 self.origin.as_str()
381 }
382
383 #[must_use]
385 pub fn insert_omission(&self) -> Option<&str> {
386 self.insert_omission.as_deref()
387 }
388
389 #[must_use]
391 pub fn insert_default(&self) -> Option<&str> {
392 self.insert_default.as_deref()
393 }
394
395 #[must_use]
397 pub const fn insert_default_bytes(&self) -> Option<u32> {
398 self.insert_default_bytes
399 }
400
401 #[must_use]
403 pub fn insert_default_hash(&self) -> Option<&str> {
404 self.insert_default_hash.as_deref()
405 }
406
407 #[must_use]
409 pub const fn introduced_in_layout(&self) -> Option<u32> {
410 self.introduced_in_layout
411 }
412
413 #[must_use]
415 pub fn historical_fill(&self) -> Option<&str> {
416 self.historical_fill.as_deref()
417 }
418
419 #[must_use]
421 pub const fn historical_fill_bytes(&self) -> Option<u32> {
422 self.historical_fill_bytes
423 }
424
425 #[must_use]
427 pub fn historical_fill_hash(&self) -> Option<&str> {
428 self.historical_fill_hash.as_deref()
429 }
430}
431
432#[cfg_attr(
433 doc,
434 doc = "EntityIndexDescription\n\nOne index entry in a describe payload."
435)]
436#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
437pub struct EntityIndexDescription {
438 pub(crate) name: String,
439 pub(crate) unique: bool,
440 pub(crate) fields: Vec<String>,
441 pub(crate) origin: String,
442}
443
444impl EntityIndexDescription {
445 #[must_use]
447 pub const fn new(name: String, unique: bool, fields: Vec<String>, origin: String) -> Self {
448 Self {
449 name,
450 unique,
451 fields,
452 origin,
453 }
454 }
455
456 #[must_use]
458 pub const fn name(&self) -> &str {
459 self.name.as_str()
460 }
461
462 #[must_use]
464 pub const fn unique(&self) -> bool {
465 self.unique
466 }
467
468 #[must_use]
470 pub const fn fields(&self) -> &[String] {
471 self.fields.as_slice()
472 }
473
474 #[must_use]
476 pub const fn origin(&self) -> &str {
477 self.origin.as_str()
478 }
479}
480
481#[cfg_attr(
482 doc,
483 doc = "EntityRelationDescription\n\nOne relation entry in a describe payload."
484)]
485#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
486pub struct EntityRelationDescription {
487 pub(crate) field: String,
488 pub(crate) target_path: String,
489 pub(crate) target_entity_name: String,
490 pub(crate) target_store_path: String,
491 pub(crate) cardinality: EntityRelationCardinality,
492}
493
494impl EntityRelationDescription {
495 #[must_use]
497 pub const fn new(
498 field: String,
499 target_path: String,
500 target_entity_name: String,
501 target_store_path: String,
502 cardinality: EntityRelationCardinality,
503 ) -> Self {
504 Self {
505 field,
506 target_path,
507 target_entity_name,
508 target_store_path,
509 cardinality,
510 }
511 }
512
513 #[must_use]
515 pub const fn field(&self) -> &str {
516 self.field.as_str()
517 }
518
519 #[must_use]
521 pub const fn target_path(&self) -> &str {
522 self.target_path.as_str()
523 }
524
525 #[must_use]
527 pub const fn target_entity_name(&self) -> &str {
528 self.target_entity_name.as_str()
529 }
530
531 #[must_use]
533 pub const fn target_store_path(&self) -> &str {
534 self.target_store_path.as_str()
535 }
536
537 #[must_use]
539 pub const fn cardinality(&self) -> EntityRelationCardinality {
540 self.cardinality
541 }
542}
543
544#[cfg_attr(
545 doc,
546 doc = "EntityRelationCardinality\n\nDescribe relation cardinality."
547)]
548#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
549pub enum EntityRelationCardinality {
550 Single,
551 List,
552 Set,
553}
554
555#[cfg_attr(
556 doc,
557 doc = "Build one stable entity-schema description from one runtime `EntityModel`."
558)]
559#[must_use]
560pub(in crate::db) fn describe_entity_model(model: &EntityModel) -> EntitySchemaDescription {
561 let fields = describe_entity_fields(model);
562 let primary_key_fields = primary_key_field_names_from_model(model);
563 let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
564
565 describe_entity_model_from_description_rows(
566 model.path,
567 model.entity_name,
568 primary_key.as_str(),
569 primary_key_fields,
570 fields,
571 describe_entity_indexes_from_model(model),
572 describe_entity_relations_from_model(model),
573 1,
574 1,
575 )
576}
577
578#[cfg_attr(
579 doc,
580 doc = "Build one entity-schema description using accepted persisted schema slot metadata."
581)]
582pub(in crate::db) fn describe_entity_model_with_persisted_schema(
583 model: &EntityModel,
584 schema: &AcceptedSchemaSnapshot,
585 value_catalog: &AcceptedValueCatalogHandle,
586) -> Result<EntitySchemaDescription, InternalError> {
587 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
588 let fields = describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)?;
589 let primary_key_fields = schema.primary_key_field_names();
590 let primary_key_fields = if primary_key_fields.is_empty() {
591 vec![model.primary_key.name.to_string()]
592 } else {
593 primary_key_fields
594 .into_iter()
595 .map(str::to_string)
596 .collect::<Vec<_>>()
597 };
598 let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
599
600 Ok(describe_entity_model_from_description_rows(
601 schema.entity_path(),
602 schema.entity_name(),
603 primary_key.as_str(),
604 primary_key_fields,
605 fields,
606 describe_entity_indexes_with_persisted_schema(schema),
607 describe_entity_relations_with_persisted_schema(schema),
608 row_layout.current_layout_version().get(),
609 row_layout.history_floor().get(),
610 ))
611}
612
613#[expect(
618 clippy::too_many_arguments,
619 reason = "one final schema DTO assembly keeps every already-owned section explicit"
620)]
621fn describe_entity_model_from_description_rows(
622 entity_path: &str,
623 entity_name: &str,
624 primary_key: &str,
625 primary_key_fields: Vec<String>,
626 fields: Vec<EntityFieldDescription>,
627 indexes: Vec<EntityIndexDescription>,
628 relations: Vec<EntityRelationDescription>,
629 row_layout_current: u32,
630 row_layout_history_floor: u32,
631) -> EntitySchemaDescription {
632 EntitySchemaDescription::new(
633 entity_path.to_string(),
634 entity_name.to_string(),
635 primary_key.to_string(),
636 primary_key_fields,
637 fields,
638 indexes,
639 relations,
640 row_layout_current,
641 row_layout_history_floor,
642 )
643}
644
645fn describe_entity_relations_from_model(model: &EntityModel) -> Vec<EntityRelationDescription> {
646 relation_field_metadata_for_model_iter(model)
647 .map(relation_description_from_metadata)
648 .collect()
649}
650
651fn primary_key_field_names_from_model(model: &EntityModel) -> Vec<String> {
652 model
653 .primary_key_model()
654 .fields()
655 .iter()
656 .map(|field| field.name.to_string())
657 .collect()
658}
659
660fn render_primary_key_fields(fields: &[String]) -> String {
661 fields.join(", ")
662}
663
664fn describe_entity_indexes_from_model(model: &EntityModel) -> Vec<EntityIndexDescription> {
665 let mut indexes = Vec::with_capacity(model.indexes.len());
666 for index in model.indexes {
667 indexes.push(EntityIndexDescription::new(
668 index.name().to_string(),
669 index.is_unique(),
670 index
671 .fields()
672 .iter()
673 .map(|field| (*field).to_string())
674 .collect(),
675 "generated".to_string(),
676 ));
677 }
678
679 indexes
680}
681
682fn describe_entity_indexes_with_persisted_schema(
683 schema: &AcceptedSchemaSnapshot,
684) -> Vec<EntityIndexDescription> {
685 schema
686 .persisted_snapshot()
687 .indexes()
688 .iter()
689 .map(|index| {
690 EntityIndexDescription::new(
691 index.name().to_string(),
692 index.unique(),
693 describe_persisted_index_fields(index.key()),
694 if index.generated() {
695 "generated".to_string()
696 } else {
697 "ddl".to_string()
698 },
699 )
700 })
701 .collect()
702}
703
704fn describe_persisted_index_fields(key: &PersistedIndexKeySnapshot) -> Vec<String> {
705 match key {
706 PersistedIndexKeySnapshot::FieldPath(paths) => paths
707 .iter()
708 .map(|field_path| field_path.path().join("."))
709 .collect(),
710 PersistedIndexKeySnapshot::Items(items) => items
711 .iter()
712 .map(|item| match item {
713 PersistedIndexKeyItemSnapshot::FieldPath(field_path) => field_path.path().join("."),
714 PersistedIndexKeyItemSnapshot::Expression(expression) => {
715 expression.canonical_text().to_string()
716 }
717 })
718 .collect(),
719 }
720}
721
722#[must_use]
726pub(in crate::db) fn describe_entity_fields(model: &EntityModel) -> Vec<EntityFieldDescription> {
727 describe_entity_fields_with_slot_lookup(model, |slot, _field| {
728 Some(u16::try_from(slot).expect("generated field slot should fit in u16"))
729 })
730}
731
732#[cfg_attr(
733 doc,
734 doc = "Build field descriptors using accepted persisted schema slot metadata."
735)]
736pub(in crate::db) fn describe_entity_fields_with_persisted_schema(
737 schema: &AcceptedSchemaSnapshot,
738 value_catalog: &AcceptedValueCatalogHandle,
739) -> Result<Vec<EntityFieldDescription>, InternalError> {
740 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
741 describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)
742}
743
744fn describe_entity_fields_with_runtime_contract(
745 schema: &AcceptedSchemaSnapshot,
746 row_layout: &AcceptedRowLayoutRuntimeContract<'_>,
747 value_catalog: &AcceptedValueCatalogHandle,
748) -> Result<Vec<EntityFieldDescription>, InternalError> {
749 let snapshot = schema.persisted_snapshot();
750 if snapshot.fields().len() != row_layout.fields().len() {
751 return Err(InternalError::store_invariant());
752 }
753 let mut fields = Vec::with_capacity(snapshot.fields().len());
754
755 for (field, runtime_field) in snapshot.fields().iter().zip(row_layout.fields()) {
758 if field.id() != runtime_field.field_id() {
759 return Err(InternalError::store_invariant());
760 }
761 let primary_key = snapshot.primary_key_field_ids().contains(&field.id());
762 let slot = Some(runtime_field.slot().get());
763 let metadata = DescribeFieldMetadata::new(
764 summarize_persisted_field_kind(field.kind(), value_catalog)?,
765 field.nullable(),
766 field_type_from_persisted_kind(field.kind())
767 .value_kind()
768 .is_queryable(),
769 field_origin_label(field.generated()),
770 );
771 let temporal = accepted_field_temporal_facts(runtime_field, value_catalog)?;
772
773 push_described_field_row(
774 &mut fields,
775 field.name(),
776 slot,
777 primary_key,
778 None,
779 metadata,
780 temporal,
781 );
782
783 if !field.nested_leaves().is_empty() {
784 describe_persisted_nested_leaves(
785 &mut fields,
786 field.nested_leaves(),
787 field_origin_label(field.generated()),
788 value_catalog,
789 )?;
790 }
791 }
792
793 Ok(fields)
794}
795
796fn describe_entity_fields_with_slot_lookup(
799 model: &EntityModel,
800 mut slot_for_field: impl FnMut(usize, &FieldModel) -> Option<u16>,
801) -> Vec<EntityFieldDescription> {
802 let mut fields = Vec::with_capacity(model.fields.len());
803 let primary_key_fields = primary_key_field_names_from_model(model);
804
805 for (slot, field) in model.fields.iter().enumerate() {
806 let primary_key = primary_key_fields
807 .iter()
808 .any(|primary_key_field| primary_key_field == field.name);
809 describe_field_recursive(
810 &mut fields,
811 field.name,
812 slot_for_field(slot, field),
813 field,
814 primary_key,
815 None,
816 None,
817 );
818 }
819
820 fields
821}
822
823struct DescribeFieldMetadata {
830 kind: String,
831 nullable: bool,
832 queryable: bool,
833 origin: String,
834}
835
836impl DescribeFieldMetadata {
837 const fn new(kind: String, nullable: bool, queryable: bool, origin: String) -> Self {
839 Self {
840 kind,
841 nullable,
842 queryable,
843 origin,
844 }
845 }
846}
847
848fn describe_field_recursive(
851 fields: &mut Vec<EntityFieldDescription>,
852 name: &str,
853 slot: Option<u16>,
854 field: &FieldModel,
855 primary_key: bool,
856 tree_prefix: Option<&'static str>,
857 metadata_override: Option<DescribeFieldMetadata>,
858) {
859 let temporal = if slot.is_some() {
860 EntityFieldTemporalFacts::generated(field)
861 } else {
862 EntityFieldTemporalFacts::nested()
863 };
864 let metadata = metadata_override.unwrap_or_else(|| {
865 DescribeFieldMetadata::new(
866 summarize_field_kind(&field.kind),
867 field.nullable(),
868 field.kind.value_kind().is_queryable(),
869 "generated".to_string(),
870 )
871 });
872
873 push_described_field_row(
874 fields,
875 name,
876 slot,
877 primary_key,
878 tree_prefix,
879 metadata,
880 temporal,
881 );
882 describe_generated_nested_fields(fields, field.nested_fields());
883}
884
885fn push_described_field_row(
888 fields: &mut Vec<EntityFieldDescription>,
889 name: &str,
890 slot: Option<u16>,
891 primary_key: bool,
892 tree_prefix: Option<&'static str>,
893 metadata: DescribeFieldMetadata,
894 temporal: EntityFieldTemporalFacts,
895) {
896 let display_name = if let Some(prefix) = tree_prefix {
899 format!("{prefix}{name}")
900 } else {
901 name.to_string()
902 };
903
904 fields.push(EntityFieldDescription::new_with_temporal_facts(
905 display_name,
906 slot,
907 primary_key,
908 metadata,
909 temporal,
910 ));
911}
912
913fn describe_generated_nested_fields(
916 fields: &mut Vec<EntityFieldDescription>,
917 nested_fields: &[FieldModel],
918) {
919 for (index, nested) in nested_fields.iter().enumerate() {
920 let prefix = if index + 1 == nested_fields.len() {
921 "└─ "
922 } else {
923 "├─ "
924 };
925 describe_field_recursive(
926 fields,
927 nested.name(),
928 None,
929 nested,
930 false,
931 Some(prefix),
932 None,
933 );
934 }
935}
936
937fn describe_persisted_nested_leaves(
940 fields: &mut Vec<EntityFieldDescription>,
941 nested_leaves: &[PersistedNestedLeafSnapshot],
942 origin: String,
943 value_catalog: &AcceptedValueCatalogHandle,
944) -> Result<(), InternalError> {
945 for (index, leaf) in nested_leaves.iter().enumerate() {
946 let prefix = if index + 1 == nested_leaves.len() {
947 "└─ "
948 } else {
949 "├─ "
950 };
951 let name = leaf.path().last().map_or("", String::as_str);
952 let metadata = DescribeFieldMetadata::new(
953 summarize_persisted_field_kind(leaf.kind(), value_catalog)?,
954 leaf.nullable(),
955 field_type_from_persisted_kind(leaf.kind())
956 .value_kind()
957 .is_queryable(),
958 origin.clone(),
959 );
960
961 push_described_field_row(
962 fields,
963 name,
964 None,
965 false,
966 Some(prefix),
967 metadata,
968 EntityFieldTemporalFacts::nested(),
969 );
970 }
971
972 Ok(())
973}
974
975fn field_origin_label(generated: bool) -> String {
976 if generated {
977 "generated".to_string()
978 } else {
979 "ddl".to_string()
980 }
981}
982
983fn describe_entity_relations_with_persisted_schema(
984 schema: &AcceptedSchemaSnapshot,
985) -> Vec<EntityRelationDescription> {
986 schema
987 .persisted_snapshot()
988 .fields()
989 .iter()
990 .filter_map(relation_description_from_persisted_field)
991 .collect()
992}
993
994fn relation_description_from_persisted_field(
995 field: &crate::db::schema::PersistedFieldSnapshot,
996) -> Option<EntityRelationDescription> {
997 let relation = persisted_relation_description_metadata(field.kind())?;
998
999 Some(EntityRelationDescription::new(
1000 field.name().to_string(),
1001 relation.target_path.to_string(),
1002 relation.target_entity_name.to_string(),
1003 relation.target_store_path.to_string(),
1004 relation.cardinality,
1005 ))
1006}
1007
1008struct PersistedRelationDescriptionMetadata<'a> {
1009 target_path: &'a str,
1010 target_entity_name: &'a str,
1011 target_store_path: &'a str,
1012 cardinality: EntityRelationCardinality,
1013}
1014
1015fn persisted_relation_description_metadata(
1016 kind: &AcceptedFieldKind,
1017) -> Option<PersistedRelationDescriptionMetadata<'_>> {
1018 const fn from_relation_kind(
1019 kind: &AcceptedFieldKind,
1020 cardinality: EntityRelationCardinality,
1021 ) -> Option<PersistedRelationDescriptionMetadata<'_>> {
1022 let AcceptedFieldKind::Relation {
1023 target_path,
1024 target_entity_name,
1025 target_store_path,
1026 ..
1027 } = kind
1028 else {
1029 return None;
1030 };
1031
1032 Some(PersistedRelationDescriptionMetadata {
1033 target_path: target_path.as_str(),
1034 target_entity_name: target_entity_name.as_str(),
1035 target_store_path: target_store_path.as_str(),
1036 cardinality,
1037 })
1038 }
1039
1040 match kind {
1041 AcceptedFieldKind::Relation { .. } => {
1042 from_relation_kind(kind, EntityRelationCardinality::Single)
1043 }
1044 AcceptedFieldKind::List(inner) => {
1045 from_relation_kind(inner, EntityRelationCardinality::List)
1046 }
1047 AcceptedFieldKind::Set(inner) => from_relation_kind(inner, EntityRelationCardinality::Set),
1048 _ => None,
1049 }
1050}
1051
1052fn relation_description_from_metadata(
1054 metadata: RelationFieldMetadata,
1055) -> EntityRelationDescription {
1056 let cardinality = match metadata.cardinality() {
1057 RelationFieldCardinality::Single => EntityRelationCardinality::Single,
1058 RelationFieldCardinality::List => EntityRelationCardinality::List,
1059 RelationFieldCardinality::Set => EntityRelationCardinality::Set,
1060 };
1061
1062 EntityRelationDescription::new(
1063 metadata.field_name().to_string(),
1064 metadata.target_path().to_string(),
1065 metadata.target_entity_name().to_string(),
1066 metadata.target_store_path().to_string(),
1067 cardinality,
1068 )
1069}
1070
1071#[cfg_attr(doc, doc = "Render one stable field-kind label for describe output.")]
1072fn summarize_field_kind(kind: &FieldKind) -> String {
1073 let mut out = String::new();
1074 write_field_kind_summary(&mut out, kind);
1075
1076 out
1077}
1078
1079fn write_field_kind_summary(out: &mut String, kind: &FieldKind) {
1082 if let Some(name) = kind.describe_kind_name() {
1083 out.push_str(name);
1084 return;
1085 }
1086
1087 match kind {
1088 FieldKind::Blob { max_len } => {
1089 write_length_bounded_field_kind_summary(out, "blob", *max_len);
1090 }
1091 FieldKind::Decimal { scale } => {
1092 let _ = write!(out, "decimal(scale={scale})");
1093 }
1094 FieldKind::IntBig { max_bytes } => {
1095 write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
1096 }
1097 FieldKind::Enum { path, .. } => {
1098 out.push_str("enum(");
1099 out.push_str(path);
1100 out.push(')');
1101 }
1102 FieldKind::Text { max_len } => {
1103 write_length_bounded_field_kind_summary(out, "text", *max_len);
1104 }
1105 FieldKind::Relation {
1106 target_entity_name,
1107 key_kind,
1108 ..
1109 } => {
1110 out.push_str("relation(target=");
1111 out.push_str(target_entity_name);
1112 out.push_str(", key=");
1113 write_field_kind_summary(out, key_kind);
1114 out.push(')');
1115 }
1116 FieldKind::List(inner) => {
1117 out.push_str("list<");
1118 write_field_kind_summary(out, inner);
1119 out.push('>');
1120 }
1121 FieldKind::Set(inner) => {
1122 out.push_str("set<");
1123 write_field_kind_summary(out, inner);
1124 out.push('>');
1125 }
1126 FieldKind::Map { key, value } => {
1127 out.push_str("map<");
1128 write_field_kind_summary(out, key);
1129 out.push_str(", ");
1130 write_field_kind_summary(out, value);
1131 out.push('>');
1132 }
1133 FieldKind::Composite { path, codec, shape } => {
1134 out.push_str("composite(path=");
1135 out.push_str(path);
1136 out.push_str(", codec=");
1137 write_composite_codec_summary(out, *codec);
1138 out.push_str(", shape=");
1139 write_generated_composite_shape_summary(out, shape);
1140 out.push(')');
1141 }
1142 FieldKind::Account
1143 | FieldKind::Bool
1144 | FieldKind::Date
1145 | FieldKind::Duration
1146 | FieldKind::Float32
1147 | FieldKind::Float64
1148 | FieldKind::Int8
1149 | FieldKind::Int16
1150 | FieldKind::Int32
1151 | FieldKind::Int64
1152 | FieldKind::Int128
1153 | FieldKind::Principal
1154 | FieldKind::Subaccount
1155 | FieldKind::Timestamp
1156 | FieldKind::Nat8
1157 | FieldKind::Nat16
1158 | FieldKind::Nat32
1159 | FieldKind::Nat64
1160 | FieldKind::Nat128
1161 | FieldKind::Ulid
1162 | FieldKind::Unit => unreachable!("schema describe invariant"),
1163 FieldKind::NatBig { max_bytes } => {
1164 write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
1165 }
1166 }
1167}
1168
1169fn write_composite_codec_summary(out: &mut String, codec: CompositeCodec) {
1170 match codec {
1171 CompositeCodec::StructuralV1 => out.push_str("structural_v1"),
1172 }
1173}
1174
1175fn write_generated_composite_shape_summary(out: &mut String, shape: &CompositeShapeModel) {
1176 match shape {
1177 CompositeShapeModel::Record(fields) => {
1178 out.push_str("record{");
1179 for (index, field) in fields.iter().enumerate() {
1180 if index > 0 {
1181 out.push_str(", ");
1182 }
1183 out.push_str(field.name());
1184 out.push(':');
1185 write_field_kind_summary(out, &field.kind());
1186 write_composite_nullability_summary(out, field.nullable());
1187 }
1188 out.push('}');
1189 }
1190 CompositeShapeModel::Tuple(elements) => {
1191 out.push_str("tuple<");
1192 for (index, element) in elements.iter().enumerate() {
1193 if index > 0 {
1194 out.push_str(", ");
1195 }
1196 write_generated_composite_element_summary(out, element);
1197 }
1198 out.push('>');
1199 }
1200 CompositeShapeModel::Newtype(inner) => {
1201 out.push_str("newtype<");
1202 write_generated_composite_element_summary(out, inner);
1203 out.push('>');
1204 }
1205 }
1206}
1207
1208fn write_generated_composite_element_summary(out: &mut String, element: &CompositeElementModel) {
1209 write_field_kind_summary(out, &element.kind());
1210 write_composite_nullability_summary(out, element.nullable());
1211}
1212
1213fn write_accepted_composite_shape_summary(
1214 out: &mut String,
1215 shape: &AcceptedCompositeShape,
1216 value_catalog: &AcceptedValueCatalogHandle,
1217) -> Result<(), InternalError> {
1218 match shape {
1219 AcceptedCompositeShape::Record(fields) => {
1220 out.push_str("record{");
1221 for (index, field) in fields.iter().enumerate() {
1222 if index > 0 {
1223 out.push_str(", ");
1224 }
1225 out.push_str(field.name());
1226 out.push(':');
1227 write_accepted_composite_element_summary(out, field.contract(), value_catalog)?;
1228 }
1229 out.push('}');
1230 }
1231 AcceptedCompositeShape::Tuple(elements) => {
1232 out.push_str("tuple<");
1233 for (index, element) in elements.iter().enumerate() {
1234 if index > 0 {
1235 out.push_str(", ");
1236 }
1237 write_accepted_composite_element_summary(out, element, value_catalog)?;
1238 }
1239 out.push('>');
1240 }
1241 AcceptedCompositeShape::Newtype(inner) => {
1242 out.push_str("newtype<");
1243 write_accepted_composite_element_summary(out, inner, value_catalog)?;
1244 out.push('>');
1245 }
1246 }
1247
1248 Ok(())
1249}
1250
1251fn write_accepted_composite_element_summary(
1252 out: &mut String,
1253 element: &AcceptedCompositeElement,
1254 value_catalog: &AcceptedValueCatalogHandle,
1255) -> Result<(), InternalError> {
1256 write_persisted_field_kind_summary(out, element.kind(), value_catalog)?;
1257 write_composite_nullability_summary(out, element.nullable());
1258 Ok(())
1259}
1260
1261fn write_composite_nullability_summary(out: &mut String, nullable: bool) {
1262 if nullable {
1263 out.push('?');
1264 }
1265}
1266
1267trait DescribeKindName {
1268 fn describe_kind_name(&self) -> Option<&'static str>;
1269}
1270
1271impl DescribeKindName for FieldKind {
1272 fn describe_kind_name(&self) -> Option<&'static str> {
1273 Some(match self {
1274 Self::Account => "account",
1275 Self::Bool => "bool",
1276 Self::Date => "date",
1277 Self::Duration => "duration",
1278 Self::Float32 => "float32",
1279 Self::Float64 => "float64",
1280 Self::Int8 => "int8",
1281 Self::Int16 => "int16",
1282 Self::Int32 => "int32",
1283 Self::Int64 => "int64",
1284 Self::Int128 => "int128",
1285 Self::Principal => "principal",
1286 Self::Subaccount => "subaccount",
1287 Self::Timestamp => "timestamp",
1288 Self::Nat8 => "nat8",
1289 Self::Nat16 => "nat16",
1290 Self::Nat32 => "nat32",
1291 Self::Nat64 => "nat64",
1292 Self::Nat128 => "nat128",
1293 Self::Ulid => "ulid",
1294 Self::Unit => "unit",
1295 Self::Blob { .. }
1296 | Self::Decimal { .. }
1297 | Self::Enum { .. }
1298 | Self::IntBig { .. }
1299 | Self::NatBig { .. }
1300 | Self::Text { .. }
1301 | Self::Relation { .. }
1302 | Self::List(_)
1303 | Self::Set(_)
1304 | Self::Map { .. }
1305 | Self::Composite { .. } => return None,
1306 })
1307 }
1308}
1309
1310fn write_length_bounded_field_kind_summary(
1314 out: &mut String,
1315 kind_name: &str,
1316 max_len: Option<u32>,
1317) {
1318 out.push_str(kind_name);
1319 if let Some(max_len) = max_len {
1320 out.push_str("(max_len=");
1321 out.push_str(&max_len.to_string());
1322 out.push(')');
1323 } else {
1324 out.push_str("(unbounded)");
1325 }
1326}
1327
1328fn write_byte_bounded_field_kind_summary(out: &mut String, kind_name: &str, max_bytes: u32) {
1329 out.push_str(kind_name);
1330 out.push_str("(max_bytes=");
1331 out.push_str(&max_bytes.to_string());
1332 out.push(')');
1333}
1334
1335fn generated_insert_default_facts(
1339 default: FieldDatabaseDefault,
1340) -> (Option<String>, Option<u32>, Option<String>) {
1341 match default {
1342 FieldDatabaseDefault::None => (None, None, None),
1343 FieldDatabaseDefault::EncodedSlotPayload(payload) => {
1344 let bytes = u32::try_from(payload.len()).ok();
1345 let hash = short_default_payload_fingerprint(payload);
1346 (
1347 Some(format!(
1348 "slot_payload(bytes={}, sha256={hash})",
1349 payload.len()
1350 )),
1351 bytes,
1352 Some(hash),
1353 )
1354 }
1355 FieldDatabaseDefault::AuthoredEnumUnit { enum_path, variant } => {
1356 (Some(format!("{enum_path}::{variant}")), None, None)
1357 }
1358 }
1359}
1360
1361struct RenderedTemporalPayload {
1369 value: String,
1370 bytes: u32,
1371 hash: String,
1372}
1373
1374fn accepted_field_temporal_facts(
1375 field: &AcceptedRowLayoutRuntimeField<'_>,
1376 value_catalog: &AcceptedValueCatalogHandle,
1377) -> Result<EntityFieldTemporalFacts, InternalError> {
1378 let write_policy = field.write_policy();
1379 let insert_omission = if write_policy.insert_generation().is_some() {
1380 "generated"
1381 } else if write_policy.write_management().is_some() {
1382 "managed"
1383 } else {
1384 match field.insert_omission_policy() {
1385 AcceptedInsertOmissionPolicy::NullIfMissing => "null",
1386 AcceptedInsertOmissionPolicy::DefaultIfMissing => "default",
1387 AcceptedInsertOmissionPolicy::Required => "required",
1388 }
1389 };
1390 let insert_default = field
1391 .insert_default()
1392 .slot_payload()
1393 .map(|payload| accepted_payload_facts(field, value_catalog, payload))
1394 .transpose()?;
1395 let (insert_default, insert_default_bytes, insert_default_hash) = match insert_default {
1396 Some(payload) => (Some(payload.value), Some(payload.bytes), Some(payload.hash)),
1397 None => (None, None, None),
1398 };
1399 let (historical_fill, historical_fill_bytes, historical_fill_hash) =
1400 match field.historical_fill() {
1401 SchemaHistoricalFill::Reject => (Some("reject".to_string()), None, None),
1402 SchemaHistoricalFill::Null => (Some("null".to_string()), None, None),
1403 SchemaHistoricalFill::SlotPayload(payload) => {
1404 let rendered = accepted_payload_facts(field, value_catalog, payload.as_slice())?;
1405 (
1406 Some(rendered.value),
1407 Some(rendered.bytes),
1408 Some(rendered.hash),
1409 )
1410 }
1411 };
1412
1413 Ok(EntityFieldTemporalFacts {
1414 insert_omission: Some(insert_omission.to_string()),
1415 insert_default,
1416 insert_default_bytes,
1417 insert_default_hash,
1418 introduced_in_layout: Some(field.introduced_in_layout().get()),
1419 historical_fill,
1420 historical_fill_bytes,
1421 historical_fill_hash,
1422 })
1423}
1424
1425fn accepted_payload_facts(
1426 field: &AcceptedRowLayoutRuntimeField<'_>,
1427 value_catalog: &AcceptedValueCatalogHandle,
1428 payload: &[u8],
1429) -> Result<RenderedTemporalPayload, InternalError> {
1430 let persistence = AcceptedFieldPersistenceContract::new(value_catalog, field.decode_contract())
1431 .map_err(|_| InternalError::store_invariant())?;
1432 let admitted = decode_admitted_value_from_accepted_field_contract(persistence, payload)?;
1433 let output = output_value_from_runtime(value_catalog.enum_catalog(), admitted.value())
1434 .map_err(|_| InternalError::store_invariant())?;
1435 let hash = short_default_payload_fingerprint(payload);
1436 let rendered = bounded_schema_value_rendering(&output, payload, hash.as_str());
1437 let bytes = u32::try_from(payload.len()).map_err(|_| InternalError::store_invariant())?;
1438
1439 Ok(RenderedTemporalPayload {
1440 value: rendered,
1441 bytes,
1442 hash,
1443 })
1444}
1445
1446fn bounded_schema_value_rendering(value: &OutputValue, payload: &[u8], hash: &str) -> String {
1447 let rendered = match value {
1448 OutputValue::Text(value) => format!("'{}'", value.escape_default()),
1449 _ => render_output_value_text(value),
1450 };
1451 if rendered.len() <= MAX_SCHEMA_VALUE_RENDER_CHARS {
1452 return rendered;
1453 }
1454
1455 format!(
1456 "{}(bytes={}, sha256={})",
1457 output_value_kind_label(value),
1458 payload.len(),
1459 hash,
1460 )
1461}
1462
1463const fn output_value_kind_label(value: &OutputValue) -> &'static str {
1464 match value {
1465 OutputValue::Account(_) => "account",
1466 OutputValue::Blob(_) => "blob",
1467 OutputValue::Bool(_) => "bool",
1468 OutputValue::Date(_) => "date",
1469 OutputValue::Decimal(_) => "decimal",
1470 OutputValue::Duration(_) => "duration",
1471 OutputValue::Enum(_) => "enum",
1472 OutputValue::Float32(_) => "float32",
1473 OutputValue::Float64(_) => "float64",
1474 OutputValue::Int64(_) => "int64",
1475 OutputValue::Int128(_) => "int128",
1476 OutputValue::IntBig(_) => "int_big",
1477 OutputValue::List(_) => "list",
1478 OutputValue::Map(_) => "map",
1479 OutputValue::Null => "null",
1480 OutputValue::Principal(_) => "principal",
1481 OutputValue::Subaccount(_) => "subaccount",
1482 OutputValue::Text(_) => "text",
1483 OutputValue::Timestamp(_) => "timestamp",
1484 OutputValue::Nat64(_) => "nat64",
1485 OutputValue::Nat128(_) => "nat128",
1486 OutputValue::NatBig(_) => "nat_big",
1487 OutputValue::Ulid(_) => "ulid",
1488 OutputValue::Unit => "unit",
1489 }
1490}
1491
1492fn short_default_payload_fingerprint(payload: &[u8]) -> String {
1493 let digest = Sha256::digest(payload);
1494 let mut out = String::with_capacity(16);
1495 for byte in &digest[..8] {
1496 let _ = write!(out, "{byte:02x}");
1497 }
1498 out
1499}
1500
1501#[cfg_attr(
1502 doc,
1503 doc = "Render one stable field-kind label from accepted persisted schema metadata."
1504)]
1505fn summarize_persisted_field_kind(
1506 kind: &AcceptedFieldKind,
1507 value_catalog: &AcceptedValueCatalogHandle,
1508) -> Result<String, InternalError> {
1509 let mut out = String::new();
1510 write_persisted_field_kind_summary(&mut out, kind, value_catalog)?;
1511
1512 Ok(out)
1513}
1514
1515fn write_persisted_field_kind_summary(
1519 out: &mut String,
1520 kind: &AcceptedFieldKind,
1521 value_catalog: &AcceptedValueCatalogHandle,
1522) -> Result<(), InternalError> {
1523 if let Some(name) = kind.describe_kind_name() {
1524 out.push_str(name);
1525 return Ok(());
1526 }
1527
1528 match kind {
1529 AcceptedFieldKind::Blob { max_len } => {
1530 write_length_bounded_field_kind_summary(out, "blob", *max_len);
1531 }
1532 AcceptedFieldKind::Decimal { scale } => {
1533 let _ = write!(out, "decimal(scale={scale})");
1534 }
1535 AcceptedFieldKind::IntBig { max_bytes } => {
1536 write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
1537 }
1538 AcceptedFieldKind::Enum { type_id } => {
1539 let definition = value_catalog
1540 .enum_catalog()
1541 .enum_type(*type_id)
1542 .ok_or_else(InternalError::store_invariant)?;
1543 out.push_str("enum(");
1544 out.push_str(definition.path());
1545 out.push(')');
1546 }
1547 AcceptedFieldKind::Text { max_len } => {
1548 write_length_bounded_field_kind_summary(out, "text", *max_len);
1549 }
1550 AcceptedFieldKind::Relation {
1551 target_entity_name,
1552 key_kind,
1553 ..
1554 } => {
1555 out.push_str("relation(target=");
1556 out.push_str(target_entity_name);
1557 out.push_str(", key=");
1558 write_persisted_field_kind_summary(out, key_kind, value_catalog)?;
1559 out.push(')');
1560 }
1561 AcceptedFieldKind::List(inner) => {
1562 out.push_str("list<");
1563 write_persisted_field_kind_summary(out, inner, value_catalog)?;
1564 out.push('>');
1565 }
1566 AcceptedFieldKind::Set(inner) => {
1567 out.push_str("set<");
1568 write_persisted_field_kind_summary(out, inner, value_catalog)?;
1569 out.push('>');
1570 }
1571 AcceptedFieldKind::Map { key, value } => {
1572 out.push_str("map<");
1573 write_persisted_field_kind_summary(out, key, value_catalog)?;
1574 out.push_str(", ");
1575 write_persisted_field_kind_summary(out, value, value_catalog)?;
1576 out.push('>');
1577 }
1578 AcceptedFieldKind::Composite { type_id } => {
1579 let composite_catalog = value_catalog.composite_catalog();
1580 let definition = composite_catalog
1581 .composite_type(*type_id)
1582 .ok_or_else(InternalError::store_invariant)?;
1583 out.push_str("composite(path=");
1584 out.push_str(definition.path());
1585 out.push_str(", codec=");
1586 write_composite_codec_summary(out, definition.codec());
1587 out.push_str(", shape=");
1588 write_accepted_composite_shape_summary(out, definition.shape(), value_catalog)?;
1589 out.push(')');
1590 }
1591 AcceptedFieldKind::Account
1592 | AcceptedFieldKind::Bool
1593 | AcceptedFieldKind::Date
1594 | AcceptedFieldKind::Duration
1595 | AcceptedFieldKind::Float32
1596 | AcceptedFieldKind::Float64
1597 | AcceptedFieldKind::Int8
1598 | AcceptedFieldKind::Int16
1599 | AcceptedFieldKind::Int32
1600 | AcceptedFieldKind::Int64
1601 | AcceptedFieldKind::Int128
1602 | AcceptedFieldKind::Principal
1603 | AcceptedFieldKind::Subaccount
1604 | AcceptedFieldKind::Timestamp
1605 | AcceptedFieldKind::Nat8
1606 | AcceptedFieldKind::Nat16
1607 | AcceptedFieldKind::Nat32
1608 | AcceptedFieldKind::Nat64
1609 | AcceptedFieldKind::Nat128
1610 | AcceptedFieldKind::Ulid
1611 | AcceptedFieldKind::Unit => unreachable!("schema describe invariant"),
1612 AcceptedFieldKind::NatBig { max_bytes } => {
1613 write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
1614 }
1615 }
1616
1617 Ok(())
1618}
1619
1620impl DescribeKindName for AcceptedFieldKind {
1621 fn describe_kind_name(&self) -> Option<&'static str> {
1622 Some(match self {
1623 Self::Account => "account",
1624 Self::Bool => "bool",
1625 Self::Date => "date",
1626 Self::Duration => "duration",
1627 Self::Float32 => "float32",
1628 Self::Float64 => "float64",
1629 Self::Int8 => "int8",
1630 Self::Int16 => "int16",
1631 Self::Int32 => "int32",
1632 Self::Int64 => "int64",
1633 Self::Int128 => "int128",
1634 Self::Principal => "principal",
1635 Self::Subaccount => "subaccount",
1636 Self::Timestamp => "timestamp",
1637 Self::Nat8 => "nat8",
1638 Self::Nat16 => "nat16",
1639 Self::Nat32 => "nat32",
1640 Self::Nat64 => "nat64",
1641 Self::Nat128 => "nat128",
1642 Self::Ulid => "ulid",
1643 Self::Unit => "unit",
1644 Self::Blob { .. }
1645 | Self::Decimal { .. }
1646 | Self::Enum { .. }
1647 | Self::IntBig { .. }
1648 | Self::NatBig { .. }
1649 | Self::Text { .. }
1650 | Self::Relation { .. }
1651 | Self::List(_)
1652 | Self::Set(_)
1653 | Self::Map { .. }
1654 | Self::Composite { .. } => return None,
1655 })
1656 }
1657}
1658
1659#[cfg(test)]
1664mod tests;