1use crate::{
7 db::{
8 relation::{
9 RelationDescriptor, RelationDescriptorCardinality, relation_descriptors_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_with_parts(
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 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_with_parts(
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 model,
447 )
448}
449
450fn describe_entity_model_with_parts(
454 entity_path: &str,
455 entity_name: &str,
456 primary_key: &str,
457 primary_key_fields: Vec<String>,
458 fields: Vec<EntityFieldDescription>,
459 indexes: Vec<EntityIndexDescription>,
460 model: &EntityModel,
461) -> EntitySchemaDescription {
462 let relations = relation_descriptors_for_model_iter(model)
463 .map(relation_description_from_descriptor)
464 .collect();
465
466 EntitySchemaDescription::new_with_primary_key_fields(
467 entity_path.to_string(),
468 entity_name.to_string(),
469 primary_key.to_string(),
470 primary_key_fields,
471 fields,
472 indexes,
473 relations,
474 )
475}
476
477fn primary_key_field_names_from_model(model: &EntityModel) -> Vec<String> {
478 model
479 .primary_key_model()
480 .fields()
481 .iter()
482 .map(|field| field.name.to_string())
483 .collect()
484}
485
486fn render_primary_key_fields(fields: &[String]) -> String {
487 fields.join(", ")
488}
489
490fn describe_entity_indexes_from_model(model: &EntityModel) -> Vec<EntityIndexDescription> {
491 let mut indexes = Vec::with_capacity(model.indexes.len());
492 for index in model.indexes {
493 indexes.push(EntityIndexDescription::new(
494 index.name().to_string(),
495 index.is_unique(),
496 index
497 .fields()
498 .iter()
499 .map(|field| (*field).to_string())
500 .collect(),
501 "generated".to_string(),
502 ));
503 }
504
505 indexes
506}
507
508fn describe_entity_indexes_with_persisted_schema(
509 schema: &AcceptedSchemaSnapshot,
510) -> Vec<EntityIndexDescription> {
511 schema
512 .persisted_snapshot()
513 .indexes()
514 .iter()
515 .map(|index| {
516 EntityIndexDescription::new(
517 index.name().to_string(),
518 index.unique(),
519 describe_persisted_index_fields(index.key()),
520 if index.generated() {
521 "generated".to_string()
522 } else {
523 "ddl".to_string()
524 },
525 )
526 })
527 .collect()
528}
529
530fn describe_persisted_index_fields(key: &PersistedIndexKeySnapshot) -> Vec<String> {
531 match key {
532 PersistedIndexKeySnapshot::FieldPath(paths) => paths
533 .iter()
534 .map(|field_path| field_path.path().join("."))
535 .collect(),
536 PersistedIndexKeySnapshot::Items(items) => items
537 .iter()
538 .map(|item| match item {
539 PersistedIndexKeyItemSnapshot::FieldPath(field_path) => field_path.path().join("."),
540 PersistedIndexKeyItemSnapshot::Expression(expression) => {
541 expression.canonical_text().to_string()
542 }
543 })
544 .collect(),
545 }
546}
547
548#[must_use]
552pub(in crate::db) fn describe_entity_fields(model: &EntityModel) -> Vec<EntityFieldDescription> {
553 describe_entity_fields_with_slot_lookup(model, |slot, _field| {
554 Some(u16::try_from(slot).expect("generated field slot should fit in u16"))
555 })
556}
557
558#[cfg_attr(
559 doc,
560 doc = "Build field descriptors using accepted persisted schema slot metadata."
561)]
562#[must_use]
563pub(in crate::db) fn describe_entity_fields_with_persisted_schema(
564 schema: &AcceptedSchemaSnapshot,
565) -> Vec<EntityFieldDescription> {
566 let snapshot = schema.persisted_snapshot();
567 let mut fields = Vec::with_capacity(snapshot.fields().len());
568
569 for field in snapshot.fields() {
572 let primary_key = snapshot.primary_key_field_ids().contains(&field.id());
573 let slot = snapshot
574 .row_layout()
575 .slot_for_field(field.id())
576 .map(SchemaFieldSlot::get);
577 let mut kind = summarize_persisted_field_kind(field.kind());
578 write_schema_default_summary(&mut kind, field.default());
579 let metadata = DescribeFieldMetadata::new(
580 kind,
581 field.nullable(),
582 field_type_from_persisted_kind(field.kind())
583 .value_kind()
584 .is_queryable(),
585 field_origin_label(field.generated()),
586 );
587
588 push_described_field_row(&mut fields, field.name(), slot, primary_key, None, metadata);
589
590 if !field.nested_leaves().is_empty() {
591 describe_persisted_nested_leaves(
592 &mut fields,
593 field.nested_leaves(),
594 field_origin_label(field.generated()),
595 );
596 }
597 }
598
599 fields
600}
601
602fn describe_entity_fields_with_slot_lookup(
606 model: &EntityModel,
607 mut slot_for_field: impl FnMut(usize, &FieldModel) -> Option<u16>,
608) -> Vec<EntityFieldDescription> {
609 let mut fields = Vec::with_capacity(model.fields.len());
610 let primary_key_fields = primary_key_field_names_from_model(model);
611
612 for (slot, field) in model.fields.iter().enumerate() {
613 let primary_key = primary_key_fields
614 .iter()
615 .any(|primary_key_field| primary_key_field == field.name);
616 describe_field_recursive(
617 &mut fields,
618 field.name,
619 slot_for_field(slot, field),
620 field,
621 primary_key,
622 None,
623 None,
624 );
625 }
626
627 fields
628}
629
630struct DescribeFieldMetadata {
639 kind: String,
640 nullable: bool,
641 queryable: bool,
642 origin: String,
643}
644
645impl DescribeFieldMetadata {
646 const fn new(kind: String, nullable: bool, queryable: bool, origin: String) -> Self {
648 Self {
649 kind,
650 nullable,
651 queryable,
652 origin,
653 }
654 }
655}
656
657fn describe_field_recursive(
660 fields: &mut Vec<EntityFieldDescription>,
661 name: &str,
662 slot: Option<u16>,
663 field: &FieldModel,
664 primary_key: bool,
665 tree_prefix: Option<&'static str>,
666 metadata_override: Option<DescribeFieldMetadata>,
667) {
668 let metadata = metadata_override.unwrap_or_else(|| {
669 let mut kind = summarize_field_kind(&field.kind);
670 write_model_default_summary(&mut kind, field.database_default());
671
672 DescribeFieldMetadata::new(
673 kind,
674 field.nullable(),
675 field.kind.value_kind().is_queryable(),
676 "generated".to_string(),
677 )
678 });
679
680 push_described_field_row(fields, name, slot, primary_key, tree_prefix, metadata);
681 describe_generated_nested_fields(fields, field.nested_fields());
682}
683
684fn push_described_field_row(
687 fields: &mut Vec<EntityFieldDescription>,
688 name: &str,
689 slot: Option<u16>,
690 primary_key: bool,
691 tree_prefix: Option<&'static str>,
692 metadata: DescribeFieldMetadata,
693) {
694 let display_name = if let Some(prefix) = tree_prefix {
697 format!("{prefix}{name}")
698 } else {
699 name.to_string()
700 };
701
702 fields.push(EntityFieldDescription::new(
703 display_name,
704 slot,
705 metadata.kind,
706 metadata.nullable,
707 primary_key,
708 metadata.queryable,
709 metadata.origin,
710 ));
711}
712
713fn describe_generated_nested_fields(
717 fields: &mut Vec<EntityFieldDescription>,
718 nested_fields: &[FieldModel],
719) {
720 for (index, nested) in nested_fields.iter().enumerate() {
721 let prefix = if index + 1 == nested_fields.len() {
722 "└─ "
723 } else {
724 "├─ "
725 };
726 describe_field_recursive(
727 fields,
728 nested.name(),
729 None,
730 nested,
731 false,
732 Some(prefix),
733 None,
734 );
735 }
736}
737
738fn describe_persisted_nested_leaves(
741 fields: &mut Vec<EntityFieldDescription>,
742 nested_leaves: &[PersistedNestedLeafSnapshot],
743 origin: String,
744) {
745 for (index, leaf) in nested_leaves.iter().enumerate() {
746 let prefix = if index + 1 == nested_leaves.len() {
747 "└─ "
748 } else {
749 "├─ "
750 };
751 let name = leaf.path().last().map_or("", String::as_str);
752 let metadata = DescribeFieldMetadata::new(
753 summarize_persisted_field_kind(leaf.kind()),
754 leaf.nullable(),
755 field_type_from_persisted_kind(leaf.kind())
756 .value_kind()
757 .is_queryable(),
758 origin.clone(),
759 );
760
761 push_described_field_row(fields, name, None, false, Some(prefix), metadata);
762 }
763}
764
765fn field_origin_label(generated: bool) -> String {
766 if generated {
767 "generated".to_string()
768 } else {
769 "ddl".to_string()
770 }
771}
772
773fn relation_description_from_descriptor(
775 descriptor: RelationDescriptor,
776) -> EntityRelationDescription {
777 let strength = match descriptor.strength() {
778 RelationStrength::Strong => EntityRelationStrength::Strong,
779 RelationStrength::Weak => EntityRelationStrength::Weak,
780 };
781
782 let cardinality = match descriptor.cardinality() {
783 RelationDescriptorCardinality::Single => EntityRelationCardinality::Single,
784 RelationDescriptorCardinality::List => EntityRelationCardinality::List,
785 RelationDescriptorCardinality::Set => EntityRelationCardinality::Set,
786 };
787
788 EntityRelationDescription::new(
789 descriptor.field_name().to_string(),
790 descriptor.target_path().to_string(),
791 descriptor.target_entity_name().to_string(),
792 descriptor.target_store_path().to_string(),
793 strength,
794 cardinality,
795 )
796}
797
798#[cfg_attr(doc, doc = "Render one stable field-kind label for describe output.")]
799fn summarize_field_kind(kind: &FieldKind) -> String {
800 let mut out = String::new();
801 write_field_kind_summary(&mut out, kind);
802
803 out
804}
805
806fn write_field_kind_summary(out: &mut String, kind: &FieldKind) {
809 match kind {
810 FieldKind::Account => out.push_str("account"),
811 FieldKind::Blob { max_len } => {
812 write_length_bounded_field_kind_summary(out, "blob", *max_len);
813 }
814 FieldKind::Bool => out.push_str("bool"),
815 FieldKind::Date => out.push_str("date"),
816 FieldKind::Decimal { scale } => {
817 let _ = write!(out, "decimal(scale={scale})");
818 }
819 FieldKind::Duration => out.push_str("duration"),
820 FieldKind::Enum { path, .. } => {
821 out.push_str("enum(");
822 out.push_str(path);
823 out.push(')');
824 }
825 FieldKind::Float32 => out.push_str("float32"),
826 FieldKind::Float64 => out.push_str("float64"),
827 FieldKind::Int => out.push_str("int"),
828 FieldKind::Int128 => out.push_str("int128"),
829 FieldKind::IntBig => out.push_str("int_big"),
830 FieldKind::Principal => out.push_str("principal"),
831 FieldKind::Subaccount => out.push_str("subaccount"),
832 FieldKind::Text { max_len } => {
833 write_length_bounded_field_kind_summary(out, "text", *max_len);
834 }
835 FieldKind::Timestamp => out.push_str("timestamp"),
836 FieldKind::Nat => out.push_str("nat"),
837 FieldKind::Nat128 => out.push_str("nat128"),
838 FieldKind::NatBig => out.push_str("nat_big"),
839 FieldKind::Ulid => out.push_str("ulid"),
840 FieldKind::Unit => out.push_str("unit"),
841 FieldKind::Relation {
842 target_entity_name,
843 key_kind,
844 strength,
845 ..
846 } => {
847 out.push_str("relation(target=");
848 out.push_str(target_entity_name);
849 out.push_str(", key=");
850 write_field_kind_summary(out, key_kind);
851 out.push_str(", strength=");
852 out.push_str(summarize_relation_strength(*strength));
853 out.push(')');
854 }
855 FieldKind::List(inner) => {
856 out.push_str("list<");
857 write_field_kind_summary(out, inner);
858 out.push('>');
859 }
860 FieldKind::Set(inner) => {
861 out.push_str("set<");
862 write_field_kind_summary(out, inner);
863 out.push('>');
864 }
865 FieldKind::Map { key, value } => {
866 out.push_str("map<");
867 write_field_kind_summary(out, key);
868 out.push_str(", ");
869 write_field_kind_summary(out, value);
870 out.push('>');
871 }
872 FieldKind::Structured { .. } => {
873 out.push_str("structured");
874 }
875 }
876}
877
878fn write_length_bounded_field_kind_summary(
882 out: &mut String,
883 kind_name: &str,
884 max_len: Option<u32>,
885) {
886 out.push_str(kind_name);
887 if let Some(max_len) = max_len {
888 out.push_str("(max_len=");
889 out.push_str(&max_len.to_string());
890 out.push(')');
891 } else {
892 out.push_str("(unbounded)");
893 }
894}
895
896fn write_model_default_summary(out: &mut String, default: FieldDatabaseDefault) {
900 match default {
901 FieldDatabaseDefault::None => {}
902 FieldDatabaseDefault::EncodedSlotPayload(payload) => {
903 write_encoded_default_payload_summary(out, payload);
904 }
905 }
906}
907
908fn write_schema_default_summary(out: &mut String, default: &SchemaFieldDefault) {
911 if let Some(payload) = default.slot_payload() {
912 write_encoded_default_payload_summary(out, payload);
913 }
914}
915
916fn write_encoded_default_payload_summary(out: &mut String, payload: &[u8]) {
920 let _ = write!(
921 out,
922 " default=slot_payload(bytes={}, sha256={})",
923 payload.len(),
924 short_default_payload_fingerprint(payload),
925 );
926}
927
928fn short_default_payload_fingerprint(payload: &[u8]) -> String {
929 let digest = Sha256::digest(payload);
930 let mut out = String::with_capacity(16);
931 for byte in &digest[..8] {
932 let _ = write!(out, "{byte:02x}");
933 }
934 out
935}
936
937#[cfg_attr(
938 doc,
939 doc = "Render one stable field-kind label from accepted persisted schema metadata."
940)]
941fn summarize_persisted_field_kind(kind: &PersistedFieldKind) -> String {
942 let mut out = String::new();
943 write_persisted_field_kind_summary(&mut out, kind);
944
945 out
946}
947
948fn write_persisted_field_kind_summary(out: &mut String, kind: &PersistedFieldKind) {
952 match kind {
953 PersistedFieldKind::Account => out.push_str("account"),
954 PersistedFieldKind::Blob { max_len } => {
955 write_length_bounded_field_kind_summary(out, "blob", *max_len);
956 }
957 PersistedFieldKind::Bool => out.push_str("bool"),
958 PersistedFieldKind::Date => out.push_str("date"),
959 PersistedFieldKind::Decimal { scale } => {
960 let _ = write!(out, "decimal(scale={scale})");
961 }
962 PersistedFieldKind::Duration => out.push_str("duration"),
963 PersistedFieldKind::Enum { path, .. } => {
964 out.push_str("enum(");
965 out.push_str(path);
966 out.push(')');
967 }
968 PersistedFieldKind::Float32 => out.push_str("float32"),
969 PersistedFieldKind::Float64 => out.push_str("float64"),
970 PersistedFieldKind::Int => out.push_str("int"),
971 PersistedFieldKind::Int128 => out.push_str("int128"),
972 PersistedFieldKind::IntBig => out.push_str("int_big"),
973 PersistedFieldKind::Principal => out.push_str("principal"),
974 PersistedFieldKind::Subaccount => out.push_str("subaccount"),
975 PersistedFieldKind::Text { max_len } => {
976 write_length_bounded_field_kind_summary(out, "text", *max_len);
977 }
978 PersistedFieldKind::Timestamp => out.push_str("timestamp"),
979 PersistedFieldKind::Nat => out.push_str("nat"),
980 PersistedFieldKind::Nat128 => out.push_str("nat128"),
981 PersistedFieldKind::NatBig => out.push_str("nat_big"),
982 PersistedFieldKind::Ulid => out.push_str("ulid"),
983 PersistedFieldKind::Unit => out.push_str("unit"),
984 PersistedFieldKind::Relation {
985 target_entity_name,
986 key_kind,
987 strength,
988 ..
989 } => {
990 out.push_str("relation(target=");
991 out.push_str(target_entity_name);
992 out.push_str(", key=");
993 write_persisted_field_kind_summary(out, key_kind);
994 out.push_str(", strength=");
995 out.push_str(summarize_persisted_relation_strength(*strength));
996 out.push(')');
997 }
998 PersistedFieldKind::List(inner) => {
999 out.push_str("list<");
1000 write_persisted_field_kind_summary(out, inner);
1001 out.push('>');
1002 }
1003 PersistedFieldKind::Set(inner) => {
1004 out.push_str("set<");
1005 write_persisted_field_kind_summary(out, inner);
1006 out.push('>');
1007 }
1008 PersistedFieldKind::Map { key, value } => {
1009 out.push_str("map<");
1010 write_persisted_field_kind_summary(out, key);
1011 out.push_str(", ");
1012 write_persisted_field_kind_summary(out, value);
1013 out.push('>');
1014 }
1015 PersistedFieldKind::Structured { .. } => {
1016 out.push_str("structured");
1017 }
1018 }
1019}
1020
1021#[cfg_attr(
1022 doc,
1023 doc = "Render one stable relation-strength label from persisted schema metadata."
1024)]
1025const fn summarize_persisted_relation_strength(
1026 strength: PersistedRelationStrength,
1027) -> &'static str {
1028 match strength {
1029 PersistedRelationStrength::Strong => "strong",
1030 PersistedRelationStrength::Weak => "weak",
1031 }
1032}
1033
1034#[cfg_attr(
1035 doc,
1036 doc = "Render one stable relation-strength label for field-kind summaries."
1037)]
1038const fn summarize_relation_strength(strength: RelationStrength) -> &'static str {
1039 match strength {
1040 RelationStrength::Strong => "strong",
1041 RelationStrength::Weak => "weak",
1042 }
1043}
1044
1045#[cfg(test)]
1050mod tests {
1051 use crate::{
1052 db::{
1053 EntityFieldDescription, EntityIndexDescription, EntityRelationCardinality,
1054 EntityRelationDescription, EntityRelationStrength, EntitySchemaDescription,
1055 relation::{RelationDescriptorCardinality, relation_descriptors_for_model_iter},
1056 schema::{
1057 AcceptedSchemaSnapshot, FieldId, PersistedFieldKind, PersistedFieldSnapshot,
1058 PersistedNestedLeafSnapshot, PersistedSchemaSnapshot, SchemaFieldDefault,
1059 SchemaFieldSlot, SchemaRowLayout, SchemaVersion,
1060 describe::{describe_entity_fields_with_persisted_schema, describe_entity_model},
1061 },
1062 },
1063 model::{
1064 entity::{EntityModel, PrimaryKeyModel},
1065 field::{
1066 FieldDatabaseDefault, FieldKind, FieldModel, FieldStorageDecode, LeafCodec,
1067 RelationStrength, ScalarCodec,
1068 },
1069 },
1070 types::EntityTag,
1071 };
1072 use candid::types::{CandidType, Label, Type, TypeInner};
1073
1074 static DESCRIBE_SINGLE_RELATION_KIND: FieldKind = FieldKind::Relation {
1075 target_path: "entities::Target",
1076 target_entity_name: "Target",
1077 target_entity_tag: EntityTag::new(0xD001),
1078 target_store_path: "stores::Target",
1079 key_kind: &FieldKind::Ulid,
1080 strength: RelationStrength::Strong,
1081 };
1082 static DESCRIBE_LIST_RELATION_INNER_KIND: FieldKind = FieldKind::Relation {
1083 target_path: "entities::Account",
1084 target_entity_name: "Account",
1085 target_entity_tag: EntityTag::new(0xD002),
1086 target_store_path: "stores::Account",
1087 key_kind: &FieldKind::Nat,
1088 strength: RelationStrength::Weak,
1089 };
1090 static DESCRIBE_SET_RELATION_INNER_KIND: FieldKind = FieldKind::Relation {
1091 target_path: "entities::Team",
1092 target_entity_name: "Team",
1093 target_entity_tag: EntityTag::new(0xD003),
1094 target_store_path: "stores::Team",
1095 key_kind: &FieldKind::Text { max_len: None },
1096 strength: RelationStrength::Strong,
1097 };
1098 static DESCRIBE_RELATION_FIELDS: [FieldModel; 4] = [
1099 FieldModel::generated("id", FieldKind::Ulid),
1100 FieldModel::generated("target", DESCRIBE_SINGLE_RELATION_KIND),
1101 FieldModel::generated(
1102 "accounts",
1103 FieldKind::List(&DESCRIBE_LIST_RELATION_INNER_KIND),
1104 ),
1105 FieldModel::generated("teams", FieldKind::Set(&DESCRIBE_SET_RELATION_INNER_KIND)),
1106 ];
1107 static DESCRIBE_RELATION_INDEXES: [&crate::model::index::IndexModel; 0] = [];
1108 static DESCRIBE_RELATION_MODEL: EntityModel = EntityModel::generated(
1109 "entities::Source",
1110 "Source",
1111 &DESCRIBE_RELATION_FIELDS[0],
1112 0,
1113 &DESCRIBE_RELATION_FIELDS,
1114 &DESCRIBE_RELATION_INDEXES,
1115 );
1116 static DESCRIBE_COMPOSITE_PK_FIELDS: [FieldModel; 3] = [
1117 FieldModel::generated("tenant_id", FieldKind::Nat),
1118 FieldModel::generated("local_id", FieldKind::Nat),
1119 FieldModel::generated("label", FieldKind::Text { max_len: None }),
1120 ];
1121 static DESCRIBE_COMPOSITE_PK_FIELD_REFS: [&FieldModel; 2] = [
1122 &DESCRIBE_COMPOSITE_PK_FIELDS[0],
1123 &DESCRIBE_COMPOSITE_PK_FIELDS[1],
1124 ];
1125 static DESCRIBE_COMPOSITE_PK_MODEL: EntityModel = EntityModel::generated_with_primary_key_model(
1126 "entities::Composite",
1127 "Composite",
1128 PrimaryKeyModel::ordered(&DESCRIBE_COMPOSITE_PK_FIELD_REFS),
1129 0,
1130 &DESCRIBE_COMPOSITE_PK_FIELDS,
1131 &DESCRIBE_RELATION_INDEXES,
1132 );
1133
1134 fn expect_record_fields(ty: Type) -> Vec<String> {
1135 match ty.as_ref() {
1136 TypeInner::Record(fields) => fields
1137 .iter()
1138 .map(|field| match field.id.as_ref() {
1139 Label::Named(name) => name.clone(),
1140 other => panic!("expected named record field, got {other:?}"),
1141 })
1142 .collect(),
1143 other => panic!("expected candid record, got {other:?}"),
1144 }
1145 }
1146
1147 fn expect_record_field_type(ty: Type, field_name: &str) -> Type {
1148 match ty.as_ref() {
1149 TypeInner::Record(fields) => fields
1150 .iter()
1151 .find_map(|field| match field.id.as_ref() {
1152 Label::Named(name) if name == field_name => Some(field.ty.clone()),
1153 _ => None,
1154 })
1155 .unwrap_or_else(|| panic!("expected record field `{field_name}`")),
1156 other => panic!("expected candid record, got {other:?}"),
1157 }
1158 }
1159
1160 fn expect_variant_labels(ty: Type) -> Vec<String> {
1161 match ty.as_ref() {
1162 TypeInner::Variant(fields) => fields
1163 .iter()
1164 .map(|field| match field.id.as_ref() {
1165 Label::Named(name) => name.clone(),
1166 other => panic!("expected named variant label, got {other:?}"),
1167 })
1168 .collect(),
1169 other => panic!("expected candid variant, got {other:?}"),
1170 }
1171 }
1172
1173 #[test]
1174 fn entity_schema_description_candid_shape_is_stable() {
1175 let fields = expect_record_fields(EntitySchemaDescription::ty());
1176
1177 for field in [
1178 "entity_path",
1179 "entity_name",
1180 "primary_key",
1181 "primary_key_fields",
1182 "fields",
1183 "indexes",
1184 "relations",
1185 ] {
1186 assert!(
1187 fields.iter().any(|candidate| candidate == field),
1188 "EntitySchemaDescription must keep `{field}` field key",
1189 );
1190 }
1191 }
1192
1193 #[test]
1194 fn entity_field_description_candid_shape_is_stable() {
1195 let fields = expect_record_fields(EntityFieldDescription::ty());
1196
1197 for field in ["name", "slot", "kind", "primary_key", "queryable", "origin"] {
1198 assert!(
1199 fields.iter().any(|candidate| candidate == field),
1200 "EntityFieldDescription must keep `{field}` field key",
1201 );
1202 }
1203
1204 assert!(
1205 matches!(
1206 expect_record_field_type(EntityFieldDescription::ty(), "slot").as_ref(),
1207 TypeInner::Nat16
1208 ),
1209 "EntityFieldDescription slot must remain plain nat16 for CLI/canister compatibility",
1210 );
1211 }
1212
1213 #[test]
1214 fn entity_index_description_candid_shape_is_stable() {
1215 let fields = expect_record_fields(EntityIndexDescription::ty());
1216
1217 for field in ["name", "unique", "fields", "origin"] {
1218 assert!(
1219 fields.iter().any(|candidate| candidate == field),
1220 "EntityIndexDescription must keep `{field}` field key",
1221 );
1222 }
1223 }
1224
1225 #[test]
1226 fn entity_relation_description_candid_shape_is_stable() {
1227 let fields = expect_record_fields(EntityRelationDescription::ty());
1228
1229 for field in [
1230 "field",
1231 "target_path",
1232 "target_entity_name",
1233 "target_store_path",
1234 "strength",
1235 "cardinality",
1236 ] {
1237 assert!(
1238 fields.iter().any(|candidate| candidate == field),
1239 "EntityRelationDescription must keep `{field}` field key",
1240 );
1241 }
1242 }
1243
1244 #[test]
1245 fn relation_enum_variant_labels_are_stable() {
1246 let mut strength_labels = expect_variant_labels(EntityRelationStrength::ty());
1247 strength_labels.sort_unstable();
1248 assert_eq!(
1249 strength_labels,
1250 vec!["Strong".to_string(), "Weak".to_string()]
1251 );
1252
1253 let mut cardinality_labels = expect_variant_labels(EntityRelationCardinality::ty());
1254 cardinality_labels.sort_unstable();
1255 assert_eq!(
1256 cardinality_labels,
1257 vec!["List".to_string(), "Set".to_string(), "Single".to_string()],
1258 );
1259 }
1260
1261 #[test]
1262 fn describe_fixture_constructors_stay_usable() {
1263 let payload = EntitySchemaDescription::new(
1264 "entities::User".to_string(),
1265 "User".to_string(),
1266 "id".to_string(),
1267 vec![EntityFieldDescription::new(
1268 "id".to_string(),
1269 Some(0),
1270 "ulid".to_string(),
1271 false,
1272 true,
1273 true,
1274 "generated".to_string(),
1275 )],
1276 vec![EntityIndexDescription::new(
1277 "idx_email".to_string(),
1278 true,
1279 vec!["email".to_string()],
1280 "generated".to_string(),
1281 )],
1282 vec![EntityRelationDescription::new(
1283 "account_id".to_string(),
1284 "entities::Account".to_string(),
1285 "Account".to_string(),
1286 "accounts".to_string(),
1287 EntityRelationStrength::Strong,
1288 EntityRelationCardinality::Single,
1289 )],
1290 );
1291
1292 assert_eq!(payload.entity_name(), "User");
1293 assert_eq!(payload.primary_key(), "id");
1294 assert_eq!(payload.primary_key_fields(), ["id".to_string()].as_slice());
1295 assert_eq!(payload.fields().len(), 1);
1296 assert_eq!(payload.indexes().len(), 1);
1297 assert_eq!(payload.relations().len(), 1);
1298 }
1299
1300 #[test]
1301 fn describe_entity_model_marks_all_composite_primary_key_fields() {
1302 let described = describe_entity_model(&DESCRIBE_COMPOSITE_PK_MODEL);
1303 let primary_key_fields = described
1304 .fields()
1305 .iter()
1306 .filter(|field| field.primary_key())
1307 .map(EntityFieldDescription::name)
1308 .collect::<Vec<_>>();
1309
1310 assert_eq!(described.primary_key(), "tenant_id, local_id");
1311 assert_eq!(
1312 described.primary_key_fields(),
1313 ["tenant_id".to_string(), "local_id".to_string()].as_slice(),
1314 );
1315 assert_eq!(primary_key_fields, ["tenant_id", "local_id"]);
1316 }
1317
1318 #[test]
1319 fn schema_describe_relations_match_relation_descriptors() {
1320 let descriptors =
1321 relation_descriptors_for_model_iter(&DESCRIBE_RELATION_MODEL).collect::<Vec<_>>();
1322 let described = describe_entity_model(&DESCRIBE_RELATION_MODEL);
1323 let relations = described.relations();
1324
1325 assert_eq!(descriptors.len(), relations.len());
1326
1327 for (descriptor, relation) in descriptors.iter().zip(relations) {
1328 assert_eq!(relation.field(), descriptor.field_name());
1329 assert_eq!(relation.target_path(), descriptor.target_path());
1330 assert_eq!(
1331 relation.target_entity_name(),
1332 descriptor.target_entity_name()
1333 );
1334 assert_eq!(relation.target_store_path(), descriptor.target_store_path());
1335 assert_eq!(
1336 relation.strength(),
1337 match descriptor.strength() {
1338 RelationStrength::Strong => EntityRelationStrength::Strong,
1339 RelationStrength::Weak => EntityRelationStrength::Weak,
1340 }
1341 );
1342 assert_eq!(
1343 relation.cardinality(),
1344 match descriptor.cardinality() {
1345 RelationDescriptorCardinality::Single => EntityRelationCardinality::Single,
1346 RelationDescriptorCardinality::List => EntityRelationCardinality::List,
1347 RelationDescriptorCardinality::Set => EntityRelationCardinality::Set,
1348 }
1349 );
1350 }
1351 }
1352
1353 #[test]
1354 fn schema_describe_includes_text_max_len_contract() {
1355 static FIELDS: [FieldModel; 2] = [
1356 FieldModel::generated("id", FieldKind::Ulid),
1357 FieldModel::generated("name", FieldKind::Text { max_len: Some(16) }),
1358 ];
1359 static INDEXES: [&crate::model::index::IndexModel; 0] = [];
1360 static MODEL: EntityModel = EntityModel::generated(
1361 "entities::BoundedName",
1362 "BoundedName",
1363 &FIELDS[0],
1364 0,
1365 &FIELDS,
1366 &INDEXES,
1367 );
1368
1369 let described = describe_entity_model(&MODEL);
1370 let name_field = described
1371 .fields()
1372 .iter()
1373 .find(|field| field.name() == "name")
1374 .expect("bounded text field should be described");
1375
1376 assert_eq!(name_field.kind(), "text(max_len=16)");
1377 }
1378
1379 #[test]
1380 fn schema_describe_includes_generated_database_default_metadata() {
1381 static DEFAULT_PAYLOAD: &[u8] = &[0xFF, 0x01, 7, 0, 0, 0, 0, 0, 0, 0];
1382 static FIELDS: [FieldModel; 2] = [
1383 FieldModel::generated("id", FieldKind::Ulid),
1384 FieldModel::generated_with_storage_decode_nullability_write_policies_database_default_and_nested_fields(
1385 "score",
1386 FieldKind::Nat,
1387 FieldStorageDecode::ByKind,
1388 false,
1389 None,
1390 None,
1391 FieldDatabaseDefault::EncodedSlotPayload(DEFAULT_PAYLOAD),
1392 &[],
1393 ),
1394 ];
1395 static INDEXES: [&crate::model::index::IndexModel; 0] = [];
1396 static MODEL: EntityModel = EntityModel::generated(
1397 "entities::DefaultedScore",
1398 "DefaultedScore",
1399 &FIELDS[0],
1400 0,
1401 &FIELDS,
1402 &INDEXES,
1403 );
1404
1405 let described = describe_entity_model(&MODEL);
1406 let score_field = described
1407 .fields()
1408 .iter()
1409 .find(|field| field.name() == "score")
1410 .expect("database-defaulted score field should be described");
1411
1412 assert_eq!(
1413 score_field.kind(),
1414 "nat default=slot_payload(bytes=10, sha256=37746b8fe16bb6b4)"
1415 );
1416 }
1417
1418 #[test]
1419 fn schema_describe_uses_accepted_top_level_field_metadata() {
1420 let id_slot = SchemaFieldSlot::new(0);
1421 let payload_slot = SchemaFieldSlot::new(7);
1422 let stale_payload_field_slot = SchemaFieldSlot::new(3);
1425 let snapshot = AcceptedSchemaSnapshot::new(PersistedSchemaSnapshot::new(
1426 SchemaVersion::initial(),
1427 "entities::BlobEvent".to_string(),
1428 "BlobEvent".to_string(),
1429 FieldId::new(1),
1430 SchemaRowLayout::new(
1431 SchemaVersion::initial(),
1432 vec![(FieldId::new(1), id_slot), (FieldId::new(2), payload_slot)],
1433 ),
1434 vec![
1435 PersistedFieldSnapshot::new(
1436 FieldId::new(1),
1437 "id".to_string(),
1438 id_slot,
1439 PersistedFieldKind::Ulid,
1440 Vec::new(),
1441 false,
1442 SchemaFieldDefault::None,
1443 FieldStorageDecode::ByKind,
1444 LeafCodec::StructuralFallback,
1445 ),
1446 PersistedFieldSnapshot::new(
1447 FieldId::new(2),
1448 "payload".to_string(),
1449 stale_payload_field_slot,
1450 PersistedFieldKind::Blob { max_len: None },
1451 Vec::new(),
1452 false,
1453 SchemaFieldDefault::SlotPayload(vec![0x10, 0x20, 0x30]),
1454 FieldStorageDecode::ByKind,
1455 LeafCodec::StructuralFallback,
1456 ),
1457 ],
1458 ));
1459
1460 let described = describe_entity_fields_with_persisted_schema(&snapshot)
1461 .into_iter()
1462 .map(|field| {
1463 (
1464 field.name().to_string(),
1465 field.slot(),
1466 field.kind().to_string(),
1467 )
1468 })
1469 .collect::<Vec<_>>();
1470
1471 assert_eq!(
1472 described,
1473 vec![
1474 ("id".to_string(), Some(0), "ulid".to_string()),
1475 (
1476 "payload".to_string(),
1477 Some(7),
1478 "blob(unbounded) default=slot_payload(bytes=3, sha256=8e1336ab78ebe687)"
1479 .to_string()
1480 ),
1481 ],
1482 );
1483 }
1484
1485 #[test]
1486 fn schema_describe_uses_accepted_nested_leaf_metadata() {
1487 let snapshot = AcceptedSchemaSnapshot::new(PersistedSchemaSnapshot::new(
1488 SchemaVersion::initial(),
1489 "entities::AcceptedProfile".to_string(),
1490 "AcceptedProfile".to_string(),
1491 FieldId::new(1),
1492 SchemaRowLayout::new(
1493 SchemaVersion::initial(),
1494 vec![
1495 (FieldId::new(1), SchemaFieldSlot::new(0)),
1496 (FieldId::new(2), SchemaFieldSlot::new(1)),
1497 ],
1498 ),
1499 vec![
1500 PersistedFieldSnapshot::new(
1501 FieldId::new(1),
1502 "id".to_string(),
1503 SchemaFieldSlot::new(0),
1504 PersistedFieldKind::Ulid,
1505 Vec::new(),
1506 false,
1507 SchemaFieldDefault::None,
1508 FieldStorageDecode::ByKind,
1509 LeafCodec::StructuralFallback,
1510 ),
1511 PersistedFieldSnapshot::new(
1512 FieldId::new(2),
1513 "profile".to_string(),
1514 SchemaFieldSlot::new(1),
1515 PersistedFieldKind::Structured { queryable: true },
1516 vec![PersistedNestedLeafSnapshot::new(
1517 vec!["rank".to_string()],
1518 PersistedFieldKind::Blob { max_len: None },
1519 false,
1520 FieldStorageDecode::ByKind,
1521 LeafCodec::Scalar(ScalarCodec::Blob),
1522 )],
1523 false,
1524 SchemaFieldDefault::None,
1525 FieldStorageDecode::Value,
1526 LeafCodec::StructuralFallback,
1527 ),
1528 ],
1529 ));
1530
1531 let described = describe_entity_fields_with_persisted_schema(&snapshot);
1532 let rank = described
1533 .iter()
1534 .find(|field| field.name() == "└─ rank")
1535 .expect("accepted nested leaf should be described");
1536
1537 assert_eq!(rank.slot(), None);
1538 assert_eq!(rank.kind(), "blob(unbounded)");
1539 assert!(rank.queryable());
1540 }
1541
1542 #[test]
1543 fn schema_describe_expands_generated_structured_field_leaves() {
1544 static NESTED_FIELDS: [FieldModel; 3] = [
1545 FieldModel::generated("name", FieldKind::Text { max_len: None }),
1546 FieldModel::generated("level", FieldKind::Nat),
1547 FieldModel::generated("pid", FieldKind::Principal),
1548 ];
1549 static FIELDS: [FieldModel; 2] = [
1550 FieldModel::generated("id", FieldKind::Ulid),
1551 FieldModel::generated_with_storage_decode_nullability_write_policies_and_nested_fields(
1552 "mentor",
1553 FieldKind::Structured { queryable: false },
1554 FieldStorageDecode::Value,
1555 false,
1556 None,
1557 None,
1558 &NESTED_FIELDS,
1559 ),
1560 ];
1561 static INDEXES: [&crate::model::index::IndexModel; 0] = [];
1562 static MODEL: EntityModel = EntityModel::generated(
1563 "entities::Character",
1564 "Character",
1565 &FIELDS[0],
1566 0,
1567 &FIELDS,
1568 &INDEXES,
1569 );
1570
1571 let described = describe_entity_model(&MODEL);
1572 let described_fields = described
1573 .fields()
1574 .iter()
1575 .map(|field| (field.name(), field.slot(), field.kind(), field.queryable()))
1576 .collect::<Vec<_>>();
1577
1578 assert_eq!(
1579 described_fields,
1580 vec![
1581 ("id", Some(0), "ulid", true),
1582 ("mentor", Some(1), "structured", false),
1583 ("├─ name", None, "text(unbounded)", true),
1584 ("├─ level", None, "nat", true),
1585 ("└─ pid", None, "principal", true),
1586 ],
1587 );
1588 }
1589}