1use crate::{
7 db::schema::CompositeCodec,
8 db::{
9 data::decode_admitted_value_from_accepted_field_contract,
10 schema::{
11 AcceptedCheckExprV1, AcceptedConstraintKind, AcceptedFieldKind,
12 AcceptedFieldPersistenceContract, AcceptedIdentityInspection,
13 AcceptedInsertOmissionPolicy, AcceptedRowLayoutRuntimeContract, AcceptedRuleOperation,
14 AcceptedRuleTarget, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
15 ConstraintActivationKind, ConstraintActivationSnapshot, ConstraintActivationState,
16 ConstraintOrigin, ConstraintValidationJob, FieldId, FieldInsertGeneration,
17 PersistedIndexKeyItemSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
18 PersistedNestedLeafSnapshot, PersistedRelationEdgeSnapshot,
19 PersistedRelationSourceSnapshot, PersistedSchemaSnapshot, SchemaHistoricalFill,
20 composite_catalog::{AcceptedCompositeElement, AcceptedCompositeShape},
21 identity_kind_maximum, output_value_from_runtime, query_field_is_queryable,
22 render_accepted_check_expr_sql,
23 runtime::AcceptedRowLayoutRuntimeField,
24 },
25 },
26 error::InternalError,
27 value::{OutputValue, render_output_value_text},
28};
29use std::fmt::Write;
30
31use candid::CandidType;
32use serde::Deserialize;
33use sha2::{Digest, Sha256};
34
35const ENTITY_FIELD_DESCRIPTION_NO_SLOT: u16 = u16::MAX;
36const MAX_SCHEMA_VALUE_RENDER_CHARS: usize = 128;
37const MAX_SQL_COLUMN_EXTRA_FLAGS: usize = 3;
38const MAX_SQL_COMPACT_COLUMN_ROWS: usize =
39 icydb_schema::MAX_FRAGMENT_FIELDS * (1 + icydb_schema::MAX_FRAGMENT_FIELDS);
40
41#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
43pub enum SqlColumnKey {
44 Primary,
46 Unique,
48 Multiple,
50 None,
52}
53
54#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
56pub enum SqlColumnDefault {
57 Auto,
59 Null,
61 Literal {
63 text: String,
65 },
66 Required,
68 NotApplicable,
70}
71
72impl SqlColumnDefault {
73 #[must_use]
75 pub const fn literal_text(&self) -> Option<&str> {
76 match self {
77 Self::Literal { text } => Some(text.as_str()),
78 Self::Auto | Self::Null | Self::Required | Self::NotApplicable => None,
79 }
80 }
81}
82
83#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
85pub enum SqlColumnExtra {
86 Identity,
88 Generated,
90 Relation,
92}
93
94#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
96pub struct SqlColumnSummary {
97 name: String,
98 field_type: String,
99 nullable: bool,
100 key: SqlColumnKey,
101 default: SqlColumnDefault,
102 extra: Vec<SqlColumnExtra>,
103}
104
105impl SqlColumnSummary {
106 fn new(
107 name: String,
108 field_type: String,
109 nullable: bool,
110 key: SqlColumnKey,
111 default: SqlColumnDefault,
112 extra: Vec<SqlColumnExtra>,
113 ) -> Result<Self, InternalError> {
114 if extra.len() > MAX_SQL_COLUMN_EXTRA_FLAGS
115 || default
116 .literal_text()
117 .is_some_and(|text| text.len() > MAX_SCHEMA_VALUE_RENDER_CHARS)
118 {
119 return Err(InternalError::store_invariant());
120 }
121 Ok(Self {
122 name,
123 field_type,
124 nullable,
125 key,
126 default,
127 extra,
128 })
129 }
130
131 #[must_use]
133 pub const fn name(&self) -> &str {
134 self.name.as_str()
135 }
136
137 #[must_use]
139 pub const fn field_type(&self) -> &str {
140 self.field_type.as_str()
141 }
142
143 #[must_use]
145 pub const fn nullable(&self) -> bool {
146 self.nullable
147 }
148
149 #[must_use]
151 pub const fn key(&self) -> SqlColumnKey {
152 self.key
153 }
154
155 #[must_use]
157 pub const fn default(&self) -> &SqlColumnDefault {
158 &self.default
159 }
160
161 #[must_use]
163 pub const fn extra(&self) -> &[SqlColumnExtra] {
164 self.extra.as_slice()
165 }
166}
167
168#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
170pub enum SqlDescribeOutput {
171 Compact {
173 entity: String,
175 columns: Vec<SqlColumnSummary>,
177 },
178 Verbose {
180 description: EntitySchemaDescription,
182 },
183}
184
185#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
187pub enum SqlShowColumnsOutput {
188 Compact {
190 entity: String,
192 columns: Vec<SqlColumnSummary>,
194 },
195 Verbose {
197 entity: String,
199 columns: Vec<EntityFieldDescription>,
201 },
202}
203
204#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
206pub struct SqlShowRelationsOutput {
207 entity: String,
208 relations: Vec<EntityRelationDescription>,
209}
210
211impl SqlShowRelationsOutput {
212 pub(in crate::db) fn new(
214 entity: String,
215 relations: Vec<EntityRelationDescription>,
216 ) -> Result<Self, InternalError> {
217 if relations.len() > icydb_schema::MAX_FRAGMENT_RELATIONS {
218 return Err(InternalError::store_invariant());
219 }
220 Ok(Self { entity, relations })
221 }
222
223 #[must_use]
225 pub const fn entity(&self) -> &str {
226 self.entity.as_str()
227 }
228
229 #[must_use]
231 pub const fn relations(&self) -> &[EntityRelationDescription] {
232 self.relations.as_slice()
233 }
234}
235
236#[cfg_attr(
237 doc,
238 doc = "EntitySchemaDescription\n\nStable describe payload for one entity model."
239)]
240#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
241pub struct EntitySchemaDescription {
242 pub(crate) entity_path: String,
243 pub(crate) entity_name: String,
244 pub(crate) entity_tag: u64,
245 pub(crate) accepted_schema_fingerprint_method: u8,
246 pub(crate) accepted_schema_fingerprint: [u8; 16],
247 pub(crate) primary_key: String,
248 pub(crate) primary_key_fields: Vec<String>,
249 pub(crate) identity: Option<Box<EntityIdentityDescription>>,
250 pub(crate) fields: Vec<EntityFieldDescription>,
251 pub(crate) indexes: Vec<EntityIndexDescription>,
252 pub(crate) relations: Vec<EntityRelationDescription>,
253 pub(crate) constraints: Vec<EntityConstraintDescription>,
254 pub(crate) row_layout_current: u32,
255 pub(crate) row_layout_history_floor: u32,
256}
257
258impl EntitySchemaDescription {
259 #[expect(
261 clippy::too_many_arguments,
262 reason = "schema description construction keeps identity, collections, and layout explicit"
263 )]
264 #[must_use]
265 pub const fn new(
266 entity_path: String,
267 entity_name: String,
268 entity_tag: u64,
269 accepted_schema_fingerprint_method: u8,
270 accepted_schema_fingerprint: [u8; 16],
271 primary_key: String,
272 primary_key_fields: Vec<String>,
273 fields: Vec<EntityFieldDescription>,
274 indexes: Vec<EntityIndexDescription>,
275 relations: Vec<EntityRelationDescription>,
276 constraints: Vec<EntityConstraintDescription>,
277 row_layout_current: u32,
278 row_layout_history_floor: u32,
279 ) -> Self {
280 Self {
281 entity_path,
282 entity_name,
283 entity_tag,
284 accepted_schema_fingerprint_method,
285 accepted_schema_fingerprint,
286 primary_key,
287 primary_key_fields,
288 identity: None,
289 fields,
290 indexes,
291 relations,
292 constraints,
293 row_layout_current,
294 row_layout_history_floor,
295 }
296 }
297
298 #[must_use]
300 pub const fn entity_path(&self) -> &str {
301 self.entity_path.as_str()
302 }
303
304 #[must_use]
306 pub const fn entity_name(&self) -> &str {
307 self.entity_name.as_str()
308 }
309
310 #[must_use]
312 pub const fn entity_tag(&self) -> u64 {
313 self.entity_tag
314 }
315
316 #[must_use]
318 pub const fn accepted_schema_fingerprint_method(&self) -> u8 {
319 self.accepted_schema_fingerprint_method
320 }
321
322 #[must_use]
324 pub const fn accepted_schema_fingerprint(&self) -> [u8; 16] {
325 self.accepted_schema_fingerprint
326 }
327
328 #[must_use]
330 pub const fn primary_key(&self) -> &str {
331 self.primary_key.as_str()
332 }
333
334 #[must_use]
336 pub const fn primary_key_fields(&self) -> &[String] {
337 self.primary_key_fields.as_slice()
338 }
339
340 #[must_use]
342 pub fn identity(&self) -> Option<&EntityIdentityDescription> {
343 self.identity.as_deref()
344 }
345
346 #[must_use]
348 pub const fn fields(&self) -> &[EntityFieldDescription] {
349 self.fields.as_slice()
350 }
351
352 #[must_use]
354 pub const fn indexes(&self) -> &[EntityIndexDescription] {
355 self.indexes.as_slice()
356 }
357
358 #[must_use]
360 pub const fn relations(&self) -> &[EntityRelationDescription] {
361 self.relations.as_slice()
362 }
363
364 #[must_use]
366 pub const fn constraints(&self) -> &[EntityConstraintDescription] {
367 self.constraints.as_slice()
368 }
369
370 #[must_use]
372 pub const fn row_layout_current(&self) -> u32 {
373 self.row_layout_current
374 }
375
376 #[must_use]
378 pub const fn row_layout_history_floor(&self) -> u32 {
379 self.row_layout_history_floor
380 }
381
382 fn with_identity(mut self, identity: Option<EntityIdentityDescription>) -> Self {
383 self.identity = identity.map(Box::new);
384 self
385 }
386}
387
388#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
391pub struct EntityIdentityDescription {
392 field: String,
393 generator: String,
394 accepted_kind: String,
395 minimum: u128,
396 maximum: u128,
397 high_water: u128,
398 remaining: u128,
399 exhausted: bool,
400}
401
402impl EntityIdentityDescription {
403 pub(in crate::db) fn new(
404 field: String,
405 accepted_kind: String,
406 maximum: u128,
407 high_water: u128,
408 ) -> Result<Self, InternalError> {
409 let remaining = maximum
410 .checked_sub(high_water)
411 .ok_or_else(InternalError::identity_state_corruption)?;
412 Ok(Self {
413 field,
414 generator: "Identity::next".to_string(),
415 accepted_kind,
416 minimum: 1,
417 maximum,
418 high_water,
419 remaining,
420 exhausted: high_water == maximum,
421 })
422 }
423
424 #[must_use]
426 pub const fn field(&self) -> &str {
427 self.field.as_str()
428 }
429
430 #[must_use]
432 pub const fn generator(&self) -> &str {
433 self.generator.as_str()
434 }
435
436 #[must_use]
438 pub const fn accepted_kind(&self) -> &str {
439 self.accepted_kind.as_str()
440 }
441
442 #[must_use]
444 pub const fn minimum(&self) -> u128 {
445 self.minimum
446 }
447
448 #[must_use]
450 pub const fn maximum(&self) -> u128 {
451 self.maximum
452 }
453
454 #[must_use]
456 pub const fn high_water(&self) -> u128 {
457 self.high_water
458 }
459
460 #[must_use]
462 pub const fn remaining(&self) -> u128 {
463 self.remaining
464 }
465
466 #[must_use]
468 pub const fn exhausted(&self) -> bool {
469 self.exhausted
470 }
471}
472
473pub(in crate::db) fn describe_accepted_identity(
474 identity: &AcceptedIdentityInspection,
475 high_water: u128,
476) -> Result<EntityIdentityDescription, InternalError> {
477 let accepted_kind = describe_kind_name(identity.accepted_kind())
478 .ok_or_else(InternalError::identity_state_corruption)?;
479 let maximum = identity_kind_maximum(identity.accepted_kind())
480 .ok_or_else(InternalError::identity_state_corruption)?;
481 EntityIdentityDescription::new(
482 identity.field_name().to_string(),
483 accepted_kind.to_string(),
484 maximum,
485 high_water,
486 )
487}
488
489#[cfg_attr(
490 doc,
491 doc = "EntityConstraintDescription\n\nOne accepted structural constraint entry in a describe payload."
492)]
493#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
494pub struct EntityConstraintDescription {
495 pub(crate) id: u32,
496 pub(crate) name: String,
497 pub(crate) kind: String,
498 pub(crate) origin: String,
499 pub(crate) validation_state: String,
500 pub(crate) validation_progress: Option<ConstraintValidationProgressDescription>,
501 pub(crate) field_id: Option<u32>,
502 pub(crate) index_id: Option<u32>,
503 pub(crate) relation_id: Option<u32>,
504 pub(crate) fields: Vec<String>,
505 pub(crate) index: Option<String>,
506 pub(crate) predicate_sql: Option<String>,
507 pub(crate) relation: Option<String>,
508 pub(crate) target_entity: Option<String>,
509 pub(crate) action: Option<String>,
510 pub(crate) semantics: String,
511 pub(crate) check_sql: Option<String>,
512}
513
514#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
516pub struct ConstraintValidationProgressDescription {
517 phase: String,
518 rows_scanned: u64,
519 findings_seen: u64,
520 restarts: u64,
521}
522
523impl ConstraintValidationProgressDescription {
524 fn from_job(job: &ConstraintValidationJob) -> Self {
525 Self {
526 phase: job.phase().as_str().to_string(),
527 rows_scanned: job.rows_scanned(),
528 findings_seen: job.findings_seen(),
529 restarts: job.restarts(),
530 }
531 }
532
533 #[must_use]
535 pub const fn phase(&self) -> &str {
536 self.phase.as_str()
537 }
538
539 #[must_use]
541 pub const fn rows_scanned(&self) -> u64 {
542 self.rows_scanned
543 }
544
545 #[must_use]
547 pub const fn findings_seen(&self) -> u64 {
548 self.findings_seen
549 }
550
551 #[must_use]
553 pub const fn restarts(&self) -> u64 {
554 self.restarts
555 }
556}
557
558impl EntityConstraintDescription {
559 #[must_use]
561 pub const fn id(&self) -> u32 {
562 self.id
563 }
564
565 #[must_use]
567 pub const fn name(&self) -> &str {
568 self.name.as_str()
569 }
570
571 #[must_use]
573 pub const fn kind(&self) -> &str {
574 self.kind.as_str()
575 }
576
577 #[must_use]
579 pub const fn origin(&self) -> &str {
580 self.origin.as_str()
581 }
582
583 #[must_use]
585 pub const fn validation_state(&self) -> &str {
586 self.validation_state.as_str()
587 }
588
589 #[must_use]
591 pub const fn validation_progress(&self) -> Option<&ConstraintValidationProgressDescription> {
592 self.validation_progress.as_ref()
593 }
594
595 #[must_use]
597 pub const fn field_id(&self) -> Option<u32> {
598 self.field_id
599 }
600
601 #[must_use]
603 pub const fn index_id(&self) -> Option<u32> {
604 self.index_id
605 }
606
607 #[must_use]
609 pub const fn relation_id(&self) -> Option<u32> {
610 self.relation_id
611 }
612
613 #[must_use]
615 pub const fn fields(&self) -> &[String] {
616 self.fields.as_slice()
617 }
618
619 #[must_use]
621 pub fn index(&self) -> Option<&str> {
622 self.index.as_deref()
623 }
624
625 #[must_use]
628 pub fn predicate_sql(&self) -> Option<&str> {
629 self.predicate_sql.as_deref()
630 }
631
632 #[must_use]
634 pub fn relation(&self) -> Option<&str> {
635 self.relation.as_deref()
636 }
637
638 #[must_use]
640 pub fn target_entity(&self) -> Option<&str> {
641 self.target_entity.as_deref()
642 }
643
644 #[must_use]
646 pub fn action(&self) -> Option<&str> {
647 self.action.as_deref()
648 }
649
650 #[must_use]
652 pub const fn semantics(&self) -> &str {
653 self.semantics.as_str()
654 }
655
656 #[must_use]
658 pub fn check_sql(&self) -> Option<&str> {
659 self.check_sql.as_deref()
660 }
661}
662
663#[cfg_attr(
664 doc,
665 doc = "EntityFieldDescription\n\nOne field entry in a describe payload."
666)]
667#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
668pub struct EntityFieldDescription {
669 pub(crate) name: String,
670 pub(crate) slot: u16,
671 pub(crate) kind: String,
672 pub(crate) nullable: bool,
673 pub(crate) primary_key: bool,
674 pub(crate) queryable: bool,
675 pub(crate) origin: String,
676 pub(crate) insert_omission: Option<String>,
677 pub(crate) insert_default: Option<String>,
678 pub(crate) insert_default_bytes: Option<u32>,
679 pub(crate) insert_default_hash: Option<String>,
680 pub(crate) introduced_in_layout: Option<u32>,
681 pub(crate) historical_fill: Option<String>,
682 pub(crate) historical_fill_bytes: Option<u32>,
683 pub(crate) historical_fill_hash: Option<String>,
684}
685
686struct EntityFieldTemporalFacts {
694 insert_omission: Option<String>,
695 insert_default: Option<String>,
696 insert_default_bytes: Option<u32>,
697 insert_default_hash: Option<String>,
698 introduced_in_layout: Option<u32>,
699 historical_fill: Option<String>,
700 historical_fill_bytes: Option<u32>,
701 historical_fill_hash: Option<String>,
702}
703
704impl EntityFieldTemporalFacts {
705 const fn nested() -> Self {
706 Self {
707 insert_omission: None,
708 insert_default: None,
709 insert_default_bytes: None,
710 insert_default_hash: None,
711 introduced_in_layout: None,
712 historical_fill: None,
713 historical_fill_bytes: None,
714 historical_fill_hash: None,
715 }
716 }
717}
718
719impl EntityFieldDescription {
720 #[expect(
722 clippy::too_many_arguments,
723 reason = "schema description construction keeps every temporal field fact explicit"
724 )]
725 #[must_use]
726 pub fn new(
727 name: String,
728 slot: Option<u16>,
729 kind: String,
730 nullable: bool,
731 primary_key: bool,
732 queryable: bool,
733 origin: String,
734 insert_omission: Option<String>,
735 insert_default: Option<String>,
736 insert_default_bytes: Option<u32>,
737 insert_default_hash: Option<String>,
738 introduced_in_layout: Option<u32>,
739 historical_fill: Option<String>,
740 historical_fill_bytes: Option<u32>,
741 historical_fill_hash: Option<String>,
742 ) -> Self {
743 Self::new_with_temporal_facts(
744 name,
745 slot,
746 primary_key,
747 DescribeFieldMetadata::new(kind, nullable, queryable, origin),
748 EntityFieldTemporalFacts {
749 insert_omission,
750 insert_default,
751 insert_default_bytes,
752 insert_default_hash,
753 introduced_in_layout,
754 historical_fill,
755 historical_fill_bytes,
756 historical_fill_hash,
757 },
758 )
759 }
760
761 fn new_with_temporal_facts(
762 name: String,
763 slot: Option<u16>,
764 primary_key: bool,
765 metadata: DescribeFieldMetadata,
766 temporal: EntityFieldTemporalFacts,
767 ) -> Self {
768 let slot = match slot {
769 Some(slot) => slot,
770 None => ENTITY_FIELD_DESCRIPTION_NO_SLOT,
771 };
772
773 Self {
774 name,
775 slot,
776 kind: metadata.kind,
777 nullable: metadata.nullable,
778 primary_key,
779 queryable: metadata.queryable,
780 origin: metadata.origin,
781 insert_omission: temporal.insert_omission,
782 insert_default: temporal.insert_default,
783 insert_default_bytes: temporal.insert_default_bytes,
784 insert_default_hash: temporal.insert_default_hash,
785 introduced_in_layout: temporal.introduced_in_layout,
786 historical_fill: temporal.historical_fill,
787 historical_fill_bytes: temporal.historical_fill_bytes,
788 historical_fill_hash: temporal.historical_fill_hash,
789 }
790 }
791
792 #[must_use]
794 pub const fn name(&self) -> &str {
795 self.name.as_str()
796 }
797
798 #[must_use]
800 pub const fn slot(&self) -> Option<u16> {
801 if self.slot == ENTITY_FIELD_DESCRIPTION_NO_SLOT {
802 None
803 } else {
804 Some(self.slot)
805 }
806 }
807
808 #[must_use]
810 pub const fn kind(&self) -> &str {
811 self.kind.as_str()
812 }
813
814 #[must_use]
816 pub const fn nullable(&self) -> bool {
817 self.nullable
818 }
819
820 #[must_use]
822 pub const fn primary_key(&self) -> bool {
823 self.primary_key
824 }
825
826 #[must_use]
828 pub const fn queryable(&self) -> bool {
829 self.queryable
830 }
831
832 #[must_use]
834 pub const fn origin(&self) -> &str {
835 self.origin.as_str()
836 }
837
838 #[must_use]
840 pub fn insert_omission(&self) -> Option<&str> {
841 self.insert_omission.as_deref()
842 }
843
844 #[must_use]
846 pub fn insert_default(&self) -> Option<&str> {
847 self.insert_default.as_deref()
848 }
849
850 #[must_use]
852 pub const fn insert_default_bytes(&self) -> Option<u32> {
853 self.insert_default_bytes
854 }
855
856 #[must_use]
858 pub fn insert_default_hash(&self) -> Option<&str> {
859 self.insert_default_hash.as_deref()
860 }
861
862 #[must_use]
864 pub const fn introduced_in_layout(&self) -> Option<u32> {
865 self.introduced_in_layout
866 }
867
868 #[must_use]
870 pub fn historical_fill(&self) -> Option<&str> {
871 self.historical_fill.as_deref()
872 }
873
874 #[must_use]
876 pub const fn historical_fill_bytes(&self) -> Option<u32> {
877 self.historical_fill_bytes
878 }
879
880 #[must_use]
882 pub fn historical_fill_hash(&self) -> Option<&str> {
883 self.historical_fill_hash.as_deref()
884 }
885}
886
887#[cfg_attr(
888 doc,
889 doc = "EntityIndexDescription\n\nOne index entry in a describe payload."
890)]
891#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
892pub struct EntityIndexDescription {
893 pub(crate) name: String,
894 pub(crate) unique: bool,
895 pub(crate) fields: Vec<String>,
896 pub(crate) origin: String,
897}
898
899impl EntityIndexDescription {
900 #[must_use]
902 pub const fn new(name: String, unique: bool, fields: Vec<String>, origin: String) -> Self {
903 Self {
904 name,
905 unique,
906 fields,
907 origin,
908 }
909 }
910
911 #[must_use]
913 pub const fn name(&self) -> &str {
914 self.name.as_str()
915 }
916
917 #[must_use]
919 pub const fn unique(&self) -> bool {
920 self.unique
921 }
922
923 #[must_use]
925 pub const fn fields(&self) -> &[String] {
926 self.fields.as_slice()
927 }
928
929 #[must_use]
931 pub const fn origin(&self) -> &str {
932 self.origin.as_str()
933 }
934}
935
936#[cfg_attr(
937 doc,
938 doc = "EntityRelationDescription\n\nOne relation entry in a describe payload."
939)]
940#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
941pub struct EntityRelationDescription {
942 pub(crate) field: String,
943 pub(crate) target_path: String,
944 pub(crate) target_entity_name: String,
945 pub(crate) target_store_path: String,
946 pub(crate) cardinality: EntityRelationCardinality,
947}
948
949impl EntityRelationDescription {
950 #[must_use]
952 pub const fn new(
953 field: String,
954 target_path: String,
955 target_entity_name: String,
956 target_store_path: String,
957 cardinality: EntityRelationCardinality,
958 ) -> Self {
959 Self {
960 field,
961 target_path,
962 target_entity_name,
963 target_store_path,
964 cardinality,
965 }
966 }
967
968 #[must_use]
970 pub const fn field(&self) -> &str {
971 self.field.as_str()
972 }
973
974 #[must_use]
976 pub const fn target_path(&self) -> &str {
977 self.target_path.as_str()
978 }
979
980 #[must_use]
982 pub const fn target_entity_name(&self) -> &str {
983 self.target_entity_name.as_str()
984 }
985
986 #[must_use]
988 pub const fn target_store_path(&self) -> &str {
989 self.target_store_path.as_str()
990 }
991
992 #[must_use]
994 pub const fn cardinality(&self) -> EntityRelationCardinality {
995 self.cardinality
996 }
997}
998
999#[cfg_attr(
1000 doc,
1001 doc = "EntityRelationCardinality\n\nDescribe relation cardinality."
1002)]
1003#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
1004pub enum EntityRelationCardinality {
1005 Single,
1006 List,
1007 Set,
1008}
1009
1010pub(in crate::db) struct AcceptedEntityDescriptionMetadata {
1012 identity: Option<EntityIdentityDescription>,
1013 entity_tag: u64,
1014 accepted_schema_fingerprint_method: u8,
1015 accepted_schema_fingerprint: [u8; 16],
1016}
1017
1018impl AcceptedEntityDescriptionMetadata {
1019 pub(in crate::db) const fn new(
1021 identity: Option<EntityIdentityDescription>,
1022 entity_tag: u64,
1023 accepted_schema_fingerprint_method: u8,
1024 accepted_schema_fingerprint: [u8; 16],
1025 ) -> Self {
1026 Self {
1027 identity,
1028 entity_tag,
1029 accepted_schema_fingerprint_method,
1030 accepted_schema_fingerprint,
1031 }
1032 }
1033}
1034
1035pub(in crate::db) fn describe_accepted_entity_with_persisted_schema(
1037 schema: &AcceptedSchemaSnapshot,
1038 value_catalog: &AcceptedValueCatalogHandle,
1039 validation_jobs: &[ConstraintValidationJob],
1040 metadata: AcceptedEntityDescriptionMetadata,
1041 resolve_relation_target: impl Fn(&str) -> Result<(String, String), InternalError>,
1042) -> Result<EntitySchemaDescription, InternalError> {
1043 describe_entity_with_persisted_schema(
1044 schema,
1045 value_catalog,
1046 validation_jobs,
1047 metadata,
1048 &resolve_relation_target,
1049 )
1050}
1051
1052fn describe_entity_with_persisted_schema(
1053 schema: &AcceptedSchemaSnapshot,
1054 value_catalog: &AcceptedValueCatalogHandle,
1055 validation_jobs: &[ConstraintValidationJob],
1056 metadata: AcceptedEntityDescriptionMetadata,
1057 resolve_relation_target: &impl Fn(&str) -> Result<(String, String), InternalError>,
1058) -> Result<EntitySchemaDescription, InternalError> {
1059 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
1060 let fields = describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)?;
1061 let primary_key_fields = schema.primary_key_field_names();
1062 if primary_key_fields.is_empty() {
1063 return Err(InternalError::store_invariant());
1064 }
1065 let primary_key_fields = primary_key_fields
1066 .into_iter()
1067 .map(str::to_string)
1068 .collect::<Vec<_>>();
1069 let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
1070
1071 Ok(describe_entity_model_from_description_rows(
1072 schema.entity_path(),
1073 schema.entity_name(),
1074 metadata.entity_tag,
1075 metadata.accepted_schema_fingerprint_method,
1076 metadata.accepted_schema_fingerprint,
1077 primary_key.as_str(),
1078 primary_key_fields,
1079 fields,
1080 describe_entity_indexes_with_persisted_schema(schema),
1081 describe_entity_relations_with_persisted_schema(schema, resolve_relation_target)?,
1082 describe_entity_constraints_with_persisted_schema(schema, value_catalog, validation_jobs)?,
1083 row_layout.current_layout_version().get(),
1084 row_layout.history_floor().get(),
1085 )
1086 .with_identity(metadata.identity))
1087}
1088
1089#[expect(
1094 clippy::too_many_arguments,
1095 reason = "one final schema DTO assembly keeps every already-owned section explicit"
1096)]
1097fn describe_entity_model_from_description_rows(
1098 entity_path: &str,
1099 entity_name: &str,
1100 entity_tag: u64,
1101 accepted_schema_fingerprint_method: u8,
1102 accepted_schema_fingerprint: [u8; 16],
1103 primary_key: &str,
1104 primary_key_fields: Vec<String>,
1105 fields: Vec<EntityFieldDescription>,
1106 indexes: Vec<EntityIndexDescription>,
1107 relations: Vec<EntityRelationDescription>,
1108 constraints: Vec<EntityConstraintDescription>,
1109 row_layout_current: u32,
1110 row_layout_history_floor: u32,
1111) -> EntitySchemaDescription {
1112 EntitySchemaDescription::new(
1113 entity_path.to_string(),
1114 entity_name.to_string(),
1115 entity_tag,
1116 accepted_schema_fingerprint_method,
1117 accepted_schema_fingerprint,
1118 primary_key.to_string(),
1119 primary_key_fields,
1120 fields,
1121 indexes,
1122 relations,
1123 constraints,
1124 row_layout_current,
1125 row_layout_history_floor,
1126 )
1127}
1128
1129fn describe_entity_constraints_with_persisted_schema(
1130 schema: &AcceptedSchemaSnapshot,
1131 value_catalog: &AcceptedValueCatalogHandle,
1132 validation_jobs: &[ConstraintValidationJob],
1133) -> Result<Vec<EntityConstraintDescription>, InternalError> {
1134 let snapshot = schema.persisted_snapshot();
1135 let mut descriptions = snapshot
1136 .constraints()
1137 .iter()
1138 .map(|constraint| describe_accepted_constraint(snapshot, value_catalog, constraint))
1139 .collect::<Result<Vec<_>, InternalError>>()?;
1140 descriptions.extend(
1141 snapshot
1142 .constraint_activations()
1143 .iter()
1144 .map(|activation| {
1145 let job = validation_jobs
1146 .iter()
1147 .find(|job| job.constraint_id() == activation.id());
1148 describe_constraint_activation(snapshot, value_catalog, activation, job)
1149 })
1150 .collect::<Result<Vec<_>, InternalError>>()?,
1151 );
1152 if validation_jobs.iter().any(|job| {
1153 !snapshot
1154 .constraint_activations()
1155 .iter()
1156 .any(|activation| activation.id() == job.constraint_id())
1157 }) {
1158 return Err(InternalError::store_invariant());
1159 }
1160 icydb_schema::compact_sort_unstable_by(&mut descriptions, |left, right| {
1161 (left.id(), left.validation_state() != "validated")
1162 .cmp(&(right.id(), right.validation_state() != "validated"))
1163 });
1164 Ok(descriptions)
1165}
1166
1167fn describe_accepted_constraint(
1168 snapshot: &PersistedSchemaSnapshot,
1169 value_catalog: &AcceptedValueCatalogHandle,
1170 constraint: &crate::db::schema::AcceptedConstraintSnapshot,
1171) -> Result<EntityConstraintDescription, InternalError> {
1172 let mut description = accepted_constraint_description(
1173 constraint.id().get(),
1174 constraint.name(),
1175 constraint.origin(),
1176 );
1177 match constraint.kind() {
1178 AcceptedConstraintKind::PrimaryKey => {
1179 description.kind = "primary_key".to_string();
1180 description.fields = snapshot
1181 .primary_key_field_ids()
1182 .iter()
1183 .map(|field_id| accepted_field_name(snapshot, *field_id))
1184 .collect::<Result<Vec<_>, _>>()?;
1185 description.semantics = "primary_key_v1".to_string();
1186 }
1187 AcceptedConstraintKind::NotNull { field_id } => {
1188 apply_not_null_description(&mut description, snapshot, *field_id)?;
1189 }
1190 AcceptedConstraintKind::Unique { index_id } => {
1191 let index = snapshot
1192 .indexes()
1193 .iter()
1194 .find(|index| index.schema_id() == *index_id)
1195 .ok_or_else(InternalError::store_invariant)?;
1196 apply_unique_index_description(&mut description, index);
1197 }
1198 AcceptedConstraintKind::Relation { relation_id } => {
1199 let relation = snapshot
1200 .relations()
1201 .iter()
1202 .find(|relation| relation.id() == *relation_id)
1203 .ok_or_else(InternalError::store_invariant)?;
1204 apply_relation_description(&mut description, snapshot, relation)?;
1205 }
1206 AcceptedConstraintKind::Check { expression } => {
1207 apply_check_description(&mut description, snapshot, value_catalog, expression)?;
1208 }
1209 AcceptedConstraintKind::TargetedRule { target, operation } => {
1210 apply_targeted_rule_description(&mut description, snapshot, target, operation)?;
1211 }
1212 }
1213 Ok(description)
1214}
1215
1216fn describe_constraint_activation(
1217 snapshot: &PersistedSchemaSnapshot,
1218 value_catalog: &AcceptedValueCatalogHandle,
1219 activation: &ConstraintActivationSnapshot,
1220 validation_job: Option<&ConstraintValidationJob>,
1221) -> Result<EntityConstraintDescription, InternalError> {
1222 let mut description = accepted_constraint_description(
1223 activation.id().get(),
1224 activation.name(),
1225 activation.origin(),
1226 );
1227 match activation.state() {
1228 ConstraintActivationState::EnforcingNewWrites if validation_job.is_none() => {
1229 description.validation_state = "enforcing_new_writes".to_string();
1230 }
1231 ConstraintActivationState::Validating => {
1232 let job = validation_job.ok_or_else(InternalError::store_invariant)?;
1233 job.validate(Some(activation))?;
1234 description.validation_state = "validating".to_string();
1235 description.validation_progress =
1236 Some(ConstraintValidationProgressDescription::from_job(job));
1237 }
1238 ConstraintActivationState::EnforcingNewWrites => {
1239 return Err(InternalError::store_invariant());
1240 }
1241 }
1242 match activation.kind() {
1243 ConstraintActivationKind::NotNull { field_id } => {
1244 apply_not_null_description(&mut description, snapshot, *field_id)?;
1245 }
1246 ConstraintActivationKind::Unique { index_id } => {
1247 let index = snapshot
1248 .candidate_indexes()
1249 .iter()
1250 .find(|index| index.schema_id() == *index_id)
1251 .ok_or_else(InternalError::store_invariant)?;
1252 apply_unique_index_description(&mut description, index);
1253 }
1254 ConstraintActivationKind::Relation { relation_id } => {
1255 let relation = snapshot
1256 .candidate_relations()
1257 .iter()
1258 .find(|relation| relation.id() == *relation_id)
1259 .ok_or_else(InternalError::store_invariant)?;
1260 apply_relation_description(&mut description, snapshot, relation)?;
1261 }
1262 ConstraintActivationKind::Check { expression } => {
1263 apply_check_description(&mut description, snapshot, value_catalog, expression)?;
1264 }
1265 ConstraintActivationKind::TargetedRule { target, operation } => {
1266 apply_targeted_rule_description(&mut description, snapshot, target, operation)?;
1267 }
1268 }
1269 Ok(description)
1270}
1271
1272fn accepted_constraint_description(
1273 id: u32,
1274 name: &str,
1275 origin: ConstraintOrigin,
1276) -> EntityConstraintDescription {
1277 EntityConstraintDescription {
1278 id,
1279 name: name.to_string(),
1280 kind: String::new(),
1281 origin: accepted_constraint_origin_label(origin).to_string(),
1282 validation_state: "validated".to_string(),
1283 validation_progress: None,
1284 field_id: None,
1285 index_id: None,
1286 relation_id: None,
1287 fields: Vec::new(),
1288 index: None,
1289 predicate_sql: None,
1290 relation: None,
1291 target_entity: None,
1292 action: None,
1293 semantics: String::new(),
1294 check_sql: None,
1295 }
1296}
1297
1298fn apply_not_null_description(
1301 description: &mut EntityConstraintDescription,
1302 snapshot: &PersistedSchemaSnapshot,
1303 field_id: FieldId,
1304) -> Result<(), InternalError> {
1305 description.kind = "not_null".to_string();
1306 description.field_id = Some(field_id.get());
1307 description.fields = vec![accepted_field_name(snapshot, field_id)?];
1308 description.semantics = "not_null_v1".to_string();
1309 Ok(())
1310}
1311
1312fn apply_relation_description(
1313 description: &mut EntityConstraintDescription,
1314 snapshot: &PersistedSchemaSnapshot,
1315 relation: &PersistedRelationEdgeSnapshot,
1316) -> Result<(), InternalError> {
1317 description.kind = "relation".to_string();
1318 description.relation_id = Some(relation.id().get());
1319 description.fields = relation
1320 .source()
1321 .root_field_ids()
1322 .iter()
1323 .map(|field_id| accepted_field_name(snapshot, *field_id))
1324 .collect::<Result<Vec<_>, _>>()?;
1325 description.relation = Some(relation.name().to_string());
1326 description.target_entity = Some(relation.target_path().to_string());
1327 description.action = Some("restrict".to_string());
1328 description.semantics = "relation_pk_restrict_v1".to_string();
1329 Ok(())
1330}
1331
1332fn apply_check_description(
1333 description: &mut EntityConstraintDescription,
1334 snapshot: &PersistedSchemaSnapshot,
1335 value_catalog: &AcceptedValueCatalogHandle,
1336 expression: &AcceptedCheckExprV1,
1337) -> Result<(), InternalError> {
1338 description.kind = "check".to_string();
1339 description.fields = expression
1340 .dependencies()
1341 .into_iter()
1342 .map(|field_id| accepted_field_name(snapshot, field_id))
1343 .collect::<Result<Vec<_>, _>>()?;
1344 description.semantics = "check_expr_v1".to_string();
1345 description.check_sql = Some(render_accepted_check_expr_sql(
1346 expression,
1347 snapshot,
1348 value_catalog,
1349 )?);
1350 Ok(())
1351}
1352
1353fn apply_targeted_rule_description(
1354 description: &mut EntityConstraintDescription,
1355 snapshot: &PersistedSchemaSnapshot,
1356 target: &AcceptedRuleTarget,
1357 operation: &AcceptedRuleOperation,
1358) -> Result<(), InternalError> {
1359 description.kind = "targeted_rule".to_string();
1360 description.field_id = Some(target.root_field_id().get());
1361 description.fields = vec![accepted_field_name(snapshot, target.root_field_id())?];
1362 description.semantics = match operation {
1363 AcceptedRuleOperation::LengthRangeInclusive { .. } => "targeted_length_range_v1",
1364 AcceptedRuleOperation::MultipleOf { .. } => "targeted_multiple_of_v1",
1365 AcceptedRuleOperation::NumericMaximumInclusive { .. } => "targeted_numeric_maximum_v1",
1366 AcceptedRuleOperation::NumericMinimumInclusive { .. } => "targeted_numeric_minimum_v1",
1367 AcceptedRuleOperation::NumericRangeInclusive { .. } => "targeted_numeric_range_v1",
1368 }
1369 .to_string();
1370 Ok(())
1371}
1372
1373fn apply_unique_index_description(
1374 description: &mut EntityConstraintDescription,
1375 index: &PersistedIndexSnapshot,
1376) {
1377 description.kind = "unique".to_string();
1378 description.index_id = Some(index.schema_id().get());
1379 description.fields = describe_persisted_index_fields(index.key());
1380 description.index = Some(index.name().to_string());
1381 description.predicate_sql = index.predicate_sql().map(str::to_string);
1382 description.semantics = if index.predicate_sql().is_some() {
1383 "partial_unique_index_v1"
1384 } else {
1385 "unique_index_v1"
1386 }
1387 .to_string();
1388}
1389
1390const fn accepted_constraint_origin_label(origin: ConstraintOrigin) -> &'static str {
1391 match origin {
1392 ConstraintOrigin::Generated => "generated",
1393 ConstraintOrigin::SqlDdl => "sql_ddl",
1394 }
1395}
1396
1397fn accepted_field_name(
1398 snapshot: &crate::db::schema::PersistedSchemaSnapshot,
1399 field_id: FieldId,
1400) -> Result<String, InternalError> {
1401 snapshot
1402 .fields()
1403 .iter()
1404 .find(|field| field.id() == field_id)
1405 .map(|field| field.name().to_string())
1406 .ok_or_else(InternalError::store_invariant)
1407}
1408
1409fn render_primary_key_fields(fields: &[String]) -> String {
1410 fields.join(", ")
1411}
1412
1413fn describe_entity_indexes_with_persisted_schema(
1414 schema: &AcceptedSchemaSnapshot,
1415) -> Vec<EntityIndexDescription> {
1416 schema
1417 .persisted_snapshot()
1418 .indexes()
1419 .iter()
1420 .map(|index| {
1421 EntityIndexDescription::new(
1422 index.name().to_string(),
1423 index.unique(),
1424 describe_persisted_index_fields(index.key()),
1425 if index.generated() {
1426 "generated".to_string()
1427 } else {
1428 "ddl".to_string()
1429 },
1430 )
1431 })
1432 .collect()
1433}
1434
1435fn describe_persisted_index_fields(key: &PersistedIndexKeySnapshot) -> Vec<String> {
1436 match key {
1437 PersistedIndexKeySnapshot::FieldPath(paths) => paths
1438 .iter()
1439 .map(|field_path| field_path.path().join("."))
1440 .collect(),
1441 PersistedIndexKeySnapshot::Items(items) => items
1442 .iter()
1443 .map(|item| match item {
1444 PersistedIndexKeyItemSnapshot::FieldPath(field_path) => field_path.path().join("."),
1445 PersistedIndexKeyItemSnapshot::Expression(expression) => {
1446 expression.canonical_text().to_string()
1447 }
1448 })
1449 .collect(),
1450 }
1451}
1452
1453pub(in crate::db) fn describe_compact_columns_with_persisted_schema(
1455 schema: &AcceptedSchemaSnapshot,
1456 value_catalog: &AcceptedValueCatalogHandle,
1457) -> Result<Vec<SqlColumnSummary>, InternalError> {
1458 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
1459 let snapshot = schema.persisted_snapshot();
1460 if snapshot.fields().len() != row_layout.fields().len()
1461 || snapshot.fields().len() > icydb_schema::MAX_FRAGMENT_FIELDS
1462 {
1463 return Err(InternalError::store_invariant());
1464 }
1465
1466 let capacity = compact_column_capacity(snapshot.fields())?;
1467 let mut accepted_fields = snapshot
1468 .fields()
1469 .iter()
1470 .zip(row_layout.fields())
1471 .collect::<Vec<_>>();
1472 icydb_schema::compact_sort_unstable_by(&mut accepted_fields, |left, right| {
1473 left.0.id().cmp(&right.0.id())
1474 });
1475 let mut columns = Vec::with_capacity(capacity);
1476 for (field, runtime_field) in accepted_fields {
1477 let matching_identity = field.id() == runtime_field.field_id();
1478 let matching_name = field.name() == runtime_field.name();
1479 if !matching_identity || !matching_name {
1480 return Err(InternalError::store_invariant());
1481 }
1482
1483 let generated = accepted_write_policy_generates(runtime_field);
1484 let relation = snapshot
1485 .relations()
1486 .iter()
1487 .any(|relation| relation.source().uses_root_field(field.id()));
1488 let extra = compact_column_extras(
1489 runtime_field.write_policy().insert_generation()
1490 == Some(FieldInsertGeneration::Identity),
1491 generated,
1492 relation,
1493 );
1494
1495 columns.push(SqlColumnSummary::new(
1496 field.name().to_string(),
1497 summarize_persisted_field_kind(field.kind(), value_catalog)?,
1498 field.nullable(),
1499 compact_column_key(snapshot, field.name()),
1500 compact_column_default(runtime_field, value_catalog)?,
1501 extra,
1502 )?);
1503
1504 let mut nested = field.nested_leaves().iter().collect::<Vec<_>>();
1505 icydb_schema::compact_sort_unstable_by(&mut nested, |left, right| {
1506 left.path().cmp(right.path())
1507 });
1508 for leaf in nested {
1509 let mut canonical_path = Vec::with_capacity(leaf.path().len().saturating_add(1));
1510 canonical_path.push(field.name());
1511 canonical_path.extend(leaf.path().iter().map(String::as_str));
1512 let canonical_name = canonical_path.join(".");
1513 columns.push(SqlColumnSummary::new(
1514 canonical_name.clone(),
1515 summarize_persisted_field_kind(leaf.kind(), value_catalog)?,
1516 nested_path_nullable(field.nullable(), field.nested_leaves(), leaf.path()),
1517 compact_column_key(snapshot, canonical_name.as_str()),
1518 SqlColumnDefault::NotApplicable,
1519 compact_column_extras(false, generated, false),
1520 )?);
1521 }
1522 }
1523
1524 if columns.len() != capacity {
1525 return Err(InternalError::store_invariant());
1526 }
1527 Ok(columns)
1528}
1529
1530fn compact_column_capacity(
1531 fields: &[crate::db::schema::PersistedFieldSnapshot],
1532) -> Result<usize, InternalError> {
1533 compact_column_capacity_from_counts(
1534 fields.len(),
1535 fields.iter().map(|field| field.nested_leaves().len()),
1536 )
1537}
1538
1539fn compact_column_capacity_from_counts(
1540 field_count: usize,
1541 nested_counts: impl IntoIterator<Item = usize>,
1542) -> Result<usize, InternalError> {
1543 if field_count > icydb_schema::MAX_FRAGMENT_FIELDS {
1544 return Err(InternalError::store_invariant());
1545 }
1546 let mut seen_fields = 0usize;
1547 let mut total = field_count;
1548 for nested_count in nested_counts {
1549 seen_fields = seen_fields
1550 .checked_add(1)
1551 .ok_or_else(InternalError::store_invariant)?;
1552 if nested_count > icydb_schema::MAX_FRAGMENT_FIELDS {
1553 return Err(InternalError::store_invariant());
1554 }
1555 total = total
1556 .checked_add(nested_count)
1557 .ok_or_else(InternalError::store_invariant)?;
1558 }
1559 if seen_fields != field_count {
1560 return Err(InternalError::store_invariant());
1561 }
1562 if total > MAX_SQL_COMPACT_COLUMN_ROWS {
1563 return Err(InternalError::store_invariant());
1564 }
1565 Ok(total)
1566}
1567
1568const fn accepted_write_policy_generates(field: &AcceptedRowLayoutRuntimeField<'_>) -> bool {
1569 let policy = field.write_policy();
1570 policy.insert_generation().is_some() || policy.write_management().is_some()
1571}
1572
1573fn compact_column_extras(identity: bool, generated: bool, relation: bool) -> Vec<SqlColumnExtra> {
1574 let mut extra = Vec::with_capacity(MAX_SQL_COLUMN_EXTRA_FLAGS);
1575 if identity {
1576 extra.push(SqlColumnExtra::Identity);
1577 }
1578 if generated {
1579 extra.push(SqlColumnExtra::Generated);
1580 }
1581 if relation {
1582 extra.push(SqlColumnExtra::Relation);
1583 }
1584 extra
1585}
1586
1587fn compact_column_default(
1588 field: &AcceptedRowLayoutRuntimeField<'_>,
1589 value_catalog: &AcceptedValueCatalogHandle,
1590) -> Result<SqlColumnDefault, InternalError> {
1591 if accepted_write_policy_generates(field) {
1592 return Ok(SqlColumnDefault::Auto);
1593 }
1594 match field.insert_omission_policy() {
1595 AcceptedInsertOmissionPolicy::NullIfMissing => Ok(SqlColumnDefault::Null),
1596 AcceptedInsertOmissionPolicy::DefaultIfMissing => {
1597 let payload = field
1598 .insert_default()
1599 .slot_payload()
1600 .ok_or_else(InternalError::store_invariant)?;
1601 let rendered = accepted_payload_facts(field, value_catalog, payload)?;
1602 Ok(SqlColumnDefault::Literal {
1603 text: rendered.value,
1604 })
1605 }
1606 AcceptedInsertOmissionPolicy::Required => Ok(SqlColumnDefault::Required),
1607 }
1608}
1609
1610fn nested_path_nullable(
1611 top_level_nullable: bool,
1612 leaves: &[PersistedNestedLeafSnapshot],
1613 path: &[String],
1614) -> bool {
1615 top_level_nullable
1616 || leaves.iter().any(|candidate| {
1617 candidate.path().len() <= path.len()
1618 && path.starts_with(candidate.path())
1619 && candidate.nullable()
1620 })
1621}
1622
1623fn compact_column_key(snapshot: &PersistedSchemaSnapshot, path: &str) -> SqlColumnKey {
1624 let top_level_field = snapshot.fields().iter().find(|field| field.name() == path);
1625 let primary =
1626 top_level_field.is_some_and(|field| snapshot.primary_key_field_ids().contains(&field.id()));
1627 let memberships = snapshot.indexes().iter().filter_map(|index| {
1628 let key_items = match index.key() {
1629 PersistedIndexKeySnapshot::FieldPath(paths) => paths.len(),
1630 PersistedIndexKeySnapshot::Items(items) => items.len(),
1631 };
1632 let exact_path_member = match index.key() {
1633 PersistedIndexKeySnapshot::FieldPath(paths) => {
1634 paths.iter().any(|item| item.path().join(".") == path)
1635 }
1636 PersistedIndexKeySnapshot::Items(items) => items.iter().any(|item| {
1637 matches!(
1638 item,
1639 PersistedIndexKeyItemSnapshot::FieldPath(field_path)
1640 if field_path.path().join(".") == path
1641 )
1642 }),
1643 };
1644 if !exact_path_member {
1645 return None;
1646 }
1647 Some((index.unique(), key_items))
1648 });
1649 classify_compact_column_key(primary, memberships)
1650}
1651
1652fn classify_compact_column_key(
1653 primary: bool,
1654 memberships: impl IntoIterator<Item = (bool, usize)>,
1655) -> SqlColumnKey {
1656 if primary {
1657 return SqlColumnKey::Primary;
1658 }
1659 let mut multiple = false;
1660 for (unique, key_items) in memberships {
1661 if unique && key_items == 1 {
1662 return SqlColumnKey::Unique;
1663 }
1664 multiple = true;
1665 }
1666 if multiple {
1667 SqlColumnKey::Multiple
1668 } else {
1669 SqlColumnKey::None
1670 }
1671}
1672
1673#[cfg_attr(
1674 doc,
1675 doc = "Build field descriptors using accepted persisted schema slot metadata."
1676)]
1677#[cfg(any(test, feature = "sql"))]
1678pub(in crate::db) fn describe_entity_fields_with_persisted_schema(
1679 schema: &AcceptedSchemaSnapshot,
1680 value_catalog: &AcceptedValueCatalogHandle,
1681) -> Result<Vec<EntityFieldDescription>, InternalError> {
1682 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
1683 describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)
1684}
1685
1686fn describe_entity_fields_with_runtime_contract(
1687 schema: &AcceptedSchemaSnapshot,
1688 row_layout: &AcceptedRowLayoutRuntimeContract<'_>,
1689 value_catalog: &AcceptedValueCatalogHandle,
1690) -> Result<Vec<EntityFieldDescription>, InternalError> {
1691 let snapshot = schema.persisted_snapshot();
1692 if snapshot.fields().len() != row_layout.fields().len() {
1693 return Err(InternalError::store_invariant());
1694 }
1695 let mut fields = Vec::with_capacity(snapshot.fields().len());
1696
1697 for (field, runtime_field) in snapshot.fields().iter().zip(row_layout.fields()) {
1700 if field.id() != runtime_field.field_id() {
1701 return Err(InternalError::store_invariant());
1702 }
1703 let primary_key = snapshot.primary_key_field_ids().contains(&field.id());
1704 let slot = Some(runtime_field.slot().get());
1705 let metadata = DescribeFieldMetadata::new(
1706 summarize_persisted_field_kind(field.kind(), value_catalog)?,
1707 field.nullable(),
1708 query_field_is_queryable(field.kind(), value_catalog.composite_catalog()),
1709 field_origin_label(field.generated()),
1710 );
1711 let temporal = accepted_field_temporal_facts(runtime_field, value_catalog)?;
1712
1713 push_described_field_row(
1714 &mut fields,
1715 field.name(),
1716 slot,
1717 primary_key,
1718 None,
1719 metadata,
1720 temporal,
1721 );
1722
1723 if !field.nested_leaves().is_empty() {
1724 describe_persisted_nested_leaves(
1725 &mut fields,
1726 field.nested_leaves(),
1727 field_origin_label(field.generated()),
1728 value_catalog,
1729 )?;
1730 }
1731 }
1732
1733 Ok(fields)
1734}
1735
1736struct DescribeFieldMetadata {
1743 kind: String,
1744 nullable: bool,
1745 queryable: bool,
1746 origin: String,
1747}
1748
1749impl DescribeFieldMetadata {
1750 const fn new(kind: String, nullable: bool, queryable: bool, origin: String) -> Self {
1752 Self {
1753 kind,
1754 nullable,
1755 queryable,
1756 origin,
1757 }
1758 }
1759}
1760
1761fn push_described_field_row(
1764 fields: &mut Vec<EntityFieldDescription>,
1765 name: &str,
1766 slot: Option<u16>,
1767 primary_key: bool,
1768 tree_prefix: Option<&'static str>,
1769 metadata: DescribeFieldMetadata,
1770 temporal: EntityFieldTemporalFacts,
1771) {
1772 let display_name = if let Some(prefix) = tree_prefix {
1775 format!("{prefix}{name}")
1776 } else {
1777 name.to_string()
1778 };
1779
1780 fields.push(EntityFieldDescription::new_with_temporal_facts(
1781 display_name,
1782 slot,
1783 primary_key,
1784 metadata,
1785 temporal,
1786 ));
1787}
1788
1789fn describe_persisted_nested_leaves(
1792 fields: &mut Vec<EntityFieldDescription>,
1793 nested_leaves: &[PersistedNestedLeafSnapshot],
1794 origin: String,
1795 value_catalog: &AcceptedValueCatalogHandle,
1796) -> Result<(), InternalError> {
1797 for (index, leaf) in nested_leaves.iter().enumerate() {
1798 let prefix = if index + 1 == nested_leaves.len() {
1799 "└─ "
1800 } else {
1801 "├─ "
1802 };
1803 let name = leaf.path().last().map_or("", String::as_str);
1804 let metadata = DescribeFieldMetadata::new(
1805 summarize_persisted_field_kind(leaf.kind(), value_catalog)?,
1806 leaf.nullable(),
1807 query_field_is_queryable(leaf.kind(), value_catalog.composite_catalog()),
1808 origin.clone(),
1809 );
1810
1811 push_described_field_row(
1812 fields,
1813 name,
1814 None,
1815 false,
1816 Some(prefix),
1817 metadata,
1818 EntityFieldTemporalFacts::nested(),
1819 );
1820 }
1821
1822 Ok(())
1823}
1824
1825fn field_origin_label(generated: bool) -> String {
1826 if generated {
1827 "generated".to_string()
1828 } else {
1829 "ddl".to_string()
1830 }
1831}
1832
1833pub(in crate::db) fn describe_entity_relations_with_persisted_schema(
1834 schema: &AcceptedSchemaSnapshot,
1835 resolve_target: &impl Fn(&str) -> Result<(String, String), InternalError>,
1836) -> Result<Vec<EntityRelationDescription>, InternalError> {
1837 let snapshot = schema.persisted_snapshot();
1838 if snapshot.relations().len() > icydb_schema::MAX_FRAGMENT_RELATIONS {
1839 return Err(InternalError::store_invariant());
1840 }
1841 snapshot
1842 .relations()
1843 .iter()
1844 .map(|relation| {
1845 let local_fields = relation
1846 .source()
1847 .root_field_ids()
1848 .iter()
1849 .map(|field_id| accepted_field_name(snapshot, *field_id))
1850 .collect::<Result<Vec<_>, _>>()?;
1851 let (target_entity_name, target_store_path) = resolve_target(relation.target_path())?;
1852
1853 Ok(EntityRelationDescription::new(
1854 render_primary_key_fields(local_fields.as_slice()),
1855 relation.target_path().to_string(),
1856 target_entity_name,
1857 target_store_path,
1858 persisted_relation_cardinality(snapshot, relation)?,
1859 ))
1860 })
1861 .collect()
1862}
1863
1864fn persisted_relation_cardinality(
1865 snapshot: &PersistedSchemaSnapshot,
1866 relation: &PersistedRelationEdgeSnapshot,
1867) -> Result<EntityRelationCardinality, InternalError> {
1868 let PersistedRelationSourceSnapshot::Direct { field_ids } = relation.source() else {
1869 return Ok(EntityRelationCardinality::Single);
1870 };
1871 let [field_id] = field_ids.as_slice() else {
1872 return Ok(EntityRelationCardinality::Single);
1873 };
1874 let field = snapshot
1875 .fields()
1876 .iter()
1877 .find(|field| field.id() == *field_id)
1878 .ok_or_else(InternalError::store_invariant)?;
1879
1880 Ok(match field.kind() {
1881 AcceptedFieldKind::List(_) => EntityRelationCardinality::List,
1882 AcceptedFieldKind::Set(_) => EntityRelationCardinality::Set,
1883 _ => EntityRelationCardinality::Single,
1884 })
1885}
1886
1887fn write_accepted_composite_shape_summary(
1888 out: &mut String,
1889 shape: &AcceptedCompositeShape,
1890 value_catalog: &AcceptedValueCatalogHandle,
1891) -> Result<(), InternalError> {
1892 match shape {
1893 AcceptedCompositeShape::Record(fields) => {
1894 out.push_str("record{");
1895 for (index, field) in fields.iter().enumerate() {
1896 if index > 0 {
1897 out.push_str(", ");
1898 }
1899 out.push_str(field.name());
1900 out.push(':');
1901 write_accepted_composite_element_summary(out, field.contract(), value_catalog)?;
1902 }
1903 out.push('}');
1904 }
1905 AcceptedCompositeShape::Tuple(elements) => {
1906 out.push_str("tuple<");
1907 for (index, element) in elements.iter().enumerate() {
1908 if index > 0 {
1909 out.push_str(", ");
1910 }
1911 write_accepted_composite_element_summary(out, element, value_catalog)?;
1912 }
1913 out.push('>');
1914 }
1915 AcceptedCompositeShape::Newtype(inner) => {
1916 out.push_str("newtype<");
1917 write_accepted_composite_element_summary(out, inner, value_catalog)?;
1918 out.push('>');
1919 }
1920 }
1921
1922 Ok(())
1923}
1924
1925fn write_accepted_composite_element_summary(
1926 out: &mut String,
1927 element: &AcceptedCompositeElement,
1928 value_catalog: &AcceptedValueCatalogHandle,
1929) -> Result<(), InternalError> {
1930 write_persisted_field_kind_summary(out, element.kind(), value_catalog)?;
1931 write_composite_nullability_summary(out, element.nullable());
1932 Ok(())
1933}
1934
1935fn write_composite_codec_summary(out: &mut String, codec: CompositeCodec) {
1936 match codec {
1937 CompositeCodec::StructuralV1 => out.push_str("structural_v1"),
1938 }
1939}
1940
1941fn write_composite_nullability_summary(out: &mut String, nullable: bool) {
1942 if nullable {
1943 out.push('?');
1944 }
1945}
1946
1947fn write_length_bounded_field_kind_summary(
1951 out: &mut String,
1952 kind_name: &str,
1953 max_len: Option<u32>,
1954) {
1955 out.push_str(kind_name);
1956 if let Some(max_len) = max_len {
1957 out.push_str("(max_len=");
1958 out.push_str(&max_len.to_string());
1959 out.push(')');
1960 } else {
1961 out.push_str("(unbounded)");
1962 }
1963}
1964
1965fn write_byte_bounded_field_kind_summary(out: &mut String, kind_name: &str, max_bytes: u32) {
1966 out.push_str(kind_name);
1967 out.push_str("(max_bytes=");
1968 out.push_str(&max_bytes.to_string());
1969 out.push(')');
1970}
1971
1972struct RenderedTemporalPayload {
1980 value: String,
1981 bytes: u32,
1982 hash: String,
1983}
1984
1985fn accepted_field_temporal_facts(
1986 field: &AcceptedRowLayoutRuntimeField<'_>,
1987 value_catalog: &AcceptedValueCatalogHandle,
1988) -> Result<EntityFieldTemporalFacts, InternalError> {
1989 let write_policy = field.write_policy();
1990 let insert_omission = if write_policy.insert_generation().is_some() {
1991 "generated"
1992 } else if write_policy.write_management().is_some() {
1993 "managed"
1994 } else {
1995 match field.insert_omission_policy() {
1996 AcceptedInsertOmissionPolicy::NullIfMissing => "null",
1997 AcceptedInsertOmissionPolicy::DefaultIfMissing => "default",
1998 AcceptedInsertOmissionPolicy::Required => "required",
1999 }
2000 };
2001 let insert_default = field
2002 .insert_default()
2003 .slot_payload()
2004 .map(|payload| accepted_payload_facts(field, value_catalog, payload))
2005 .transpose()?;
2006 let (insert_default, insert_default_bytes, insert_default_hash) = match insert_default {
2007 Some(payload) => (Some(payload.value), Some(payload.bytes), Some(payload.hash)),
2008 None => (None, None, None),
2009 };
2010 let (historical_fill, historical_fill_bytes, historical_fill_hash) =
2011 match field.historical_fill() {
2012 SchemaHistoricalFill::Reject => (Some("reject".to_string()), None, None),
2013 SchemaHistoricalFill::Null => (Some("null".to_string()), None, None),
2014 SchemaHistoricalFill::SlotPayload(payload) => {
2015 let rendered = accepted_payload_facts(field, value_catalog, payload.as_slice())?;
2016 (
2017 Some(rendered.value),
2018 Some(rendered.bytes),
2019 Some(rendered.hash),
2020 )
2021 }
2022 };
2023
2024 Ok(EntityFieldTemporalFacts {
2025 insert_omission: Some(insert_omission.to_string()),
2026 insert_default,
2027 insert_default_bytes,
2028 insert_default_hash,
2029 introduced_in_layout: Some(field.introduced_in_layout().get()),
2030 historical_fill,
2031 historical_fill_bytes,
2032 historical_fill_hash,
2033 })
2034}
2035
2036fn accepted_payload_facts(
2037 field: &AcceptedRowLayoutRuntimeField<'_>,
2038 value_catalog: &AcceptedValueCatalogHandle,
2039 payload: &[u8],
2040) -> Result<RenderedTemporalPayload, InternalError> {
2041 let persistence = AcceptedFieldPersistenceContract::new(value_catalog, field.decode_contract())
2042 .map_err(|_| InternalError::store_invariant())?;
2043 let admitted = decode_admitted_value_from_accepted_field_contract(persistence, payload)?;
2044 let output = output_value_from_runtime(value_catalog.enum_catalog(), admitted.into_value())
2045 .map_err(|_| InternalError::store_invariant())?;
2046 let hash = short_default_payload_fingerprint(payload);
2047 let rendered = bounded_schema_value_rendering(&output, payload, hash.as_str());
2048 let bytes = u32::try_from(payload.len()).map_err(|_| InternalError::store_invariant())?;
2049
2050 Ok(RenderedTemporalPayload {
2051 value: rendered,
2052 bytes,
2053 hash,
2054 })
2055}
2056
2057fn bounded_schema_value_rendering(value: &OutputValue, payload: &[u8], hash: &str) -> String {
2058 let rendered = match value.as_public() {
2059 crate::value::PublicValue::Text(value) => format!("'{}'", value.escape_default()),
2060 _ => render_output_value_text(value),
2061 };
2062 if rendered.len() <= MAX_SCHEMA_VALUE_RENDER_CHARS {
2063 return rendered;
2064 }
2065
2066 format!(
2067 "{}(bytes={}, sha256={})",
2068 output_value_kind_label(value),
2069 payload.len(),
2070 hash,
2071 )
2072}
2073
2074const fn output_value_kind_label(value: &OutputValue) -> &'static str {
2075 match value.as_public() {
2076 crate::value::PublicValue::Account(_) => "account",
2077 crate::value::PublicValue::Blob(_) => "blob",
2078 crate::value::PublicValue::Bool(_) => "bool",
2079 crate::value::PublicValue::Date(_) => "date",
2080 crate::value::PublicValue::Decimal(_) => "decimal",
2081 crate::value::PublicValue::Duration(_) => "duration",
2082 crate::value::PublicValue::Enum(_) => "enum",
2083 crate::value::PublicValue::Float32(_) => "float32",
2084 crate::value::PublicValue::Float64(_) => "float64",
2085 crate::value::PublicValue::Int64(_) => "int64",
2086 crate::value::PublicValue::Int128(_) => "int128",
2087 crate::value::PublicValue::IntBig(_) => "int_big",
2088 crate::value::PublicValue::List(_) => "list",
2089 crate::value::PublicValue::Map(_) => "map",
2090 crate::value::PublicValue::Null => "null",
2091 crate::value::PublicValue::Principal(_) => "principal",
2092 crate::value::PublicValue::Subaccount(_) => "subaccount",
2093 crate::value::PublicValue::Text(_) => "text",
2094 crate::value::PublicValue::Timestamp(_) => "timestamp",
2095 crate::value::PublicValue::Nat64(_) => "nat64",
2096 crate::value::PublicValue::Nat128(_) => "nat128",
2097 crate::value::PublicValue::NatBig(_) => "nat_big",
2098 crate::value::PublicValue::Ulid(_) => "ulid",
2099 crate::value::PublicValue::Unit => "unit",
2100 crate::value::PublicValue::U256(_) => "u256",
2101 }
2102}
2103
2104fn short_default_payload_fingerprint(payload: &[u8]) -> String {
2105 let digest = Sha256::digest(payload);
2106 let mut out = String::with_capacity(16);
2107 for byte in &digest[..8] {
2108 let _ = write!(out, "{byte:02x}");
2109 }
2110 out
2111}
2112
2113#[cfg_attr(
2114 doc,
2115 doc = "Render one stable field-kind label from accepted persisted schema metadata."
2116)]
2117fn summarize_persisted_field_kind(
2118 kind: &AcceptedFieldKind,
2119 value_catalog: &AcceptedValueCatalogHandle,
2120) -> Result<String, InternalError> {
2121 let mut out = String::new();
2122 write_persisted_field_kind_summary(&mut out, kind, value_catalog)?;
2123
2124 Ok(out)
2125}
2126
2127fn write_persisted_field_kind_summary(
2130 out: &mut String,
2131 kind: &AcceptedFieldKind,
2132 value_catalog: &AcceptedValueCatalogHandle,
2133) -> Result<(), InternalError> {
2134 if let Some(name) = describe_kind_name(kind) {
2135 out.push_str(name);
2136 return Ok(());
2137 }
2138
2139 match kind {
2140 AcceptedFieldKind::Blob { max_len } => {
2141 write_length_bounded_field_kind_summary(out, "blob", *max_len);
2142 }
2143 AcceptedFieldKind::Decimal { scale } => {
2144 let _ = write!(out, "decimal(scale={scale})");
2145 }
2146 AcceptedFieldKind::IntBig { max_bytes } => {
2147 write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
2148 }
2149 AcceptedFieldKind::Enum { type_id } => {
2150 let definition = value_catalog
2151 .enum_catalog()
2152 .enum_type(*type_id)
2153 .ok_or_else(InternalError::store_invariant)?;
2154 out.push_str("enum(");
2155 out.push_str(definition.path());
2156 out.push(')');
2157 }
2158 AcceptedFieldKind::Text { max_len } => {
2159 write_length_bounded_field_kind_summary(out, "text", *max_len);
2160 }
2161 AcceptedFieldKind::Relation {
2162 target_entity_name,
2163 key_kind,
2164 ..
2165 } => {
2166 out.push_str("relation(target=");
2167 out.push_str(target_entity_name);
2168 out.push_str(", key=");
2169 write_persisted_field_kind_summary(out, key_kind, value_catalog)?;
2170 out.push(')');
2171 }
2172 AcceptedFieldKind::List(inner) => {
2173 out.push_str("list<");
2174 write_persisted_field_kind_summary(out, inner, value_catalog)?;
2175 out.push('>');
2176 }
2177 AcceptedFieldKind::Set(inner) => {
2178 out.push_str("set<");
2179 write_persisted_field_kind_summary(out, inner, value_catalog)?;
2180 out.push('>');
2181 }
2182 AcceptedFieldKind::Map { key, value } => {
2183 out.push_str("map<");
2184 write_persisted_field_kind_summary(out, key, value_catalog)?;
2185 out.push_str(", ");
2186 write_persisted_field_kind_summary(out, value, value_catalog)?;
2187 out.push('>');
2188 }
2189 AcceptedFieldKind::Composite { type_id } => {
2190 let composite_catalog = value_catalog.composite_catalog();
2191 let definition = composite_catalog
2192 .composite_type(*type_id)
2193 .ok_or_else(InternalError::store_invariant)?;
2194 out.push_str("composite(path=");
2195 out.push_str(definition.path());
2196 out.push_str(", codec=");
2197 write_composite_codec_summary(out, definition.codec());
2198 out.push_str(", shape=");
2199 write_accepted_composite_shape_summary(out, definition.shape(), value_catalog)?;
2200 out.push(')');
2201 }
2202 AcceptedFieldKind::Account
2203 | AcceptedFieldKind::Bool
2204 | AcceptedFieldKind::Date
2205 | AcceptedFieldKind::Duration
2206 | AcceptedFieldKind::Float32
2207 | AcceptedFieldKind::Float64
2208 | AcceptedFieldKind::Int8
2209 | AcceptedFieldKind::Int16
2210 | AcceptedFieldKind::Int32
2211 | AcceptedFieldKind::Int64
2212 | AcceptedFieldKind::Int128
2213 | AcceptedFieldKind::Principal
2214 | AcceptedFieldKind::Subaccount
2215 | AcceptedFieldKind::Timestamp
2216 | AcceptedFieldKind::Nat8
2217 | AcceptedFieldKind::Nat16
2218 | AcceptedFieldKind::Nat32
2219 | AcceptedFieldKind::Nat64
2220 | AcceptedFieldKind::Nat128
2221 | AcceptedFieldKind::Ulid
2222 | AcceptedFieldKind::Unit
2223 | AcceptedFieldKind::U256 => return Err(InternalError::store_invariant()),
2224 AcceptedFieldKind::NatBig { max_bytes } => {
2225 write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
2226 }
2227 }
2228
2229 Ok(())
2230}
2231
2232const fn describe_kind_name(kind: &AcceptedFieldKind) -> Option<&'static str> {
2233 Some(match kind {
2234 AcceptedFieldKind::Account => "account",
2235 AcceptedFieldKind::Bool => "bool",
2236 AcceptedFieldKind::Date => "date",
2237 AcceptedFieldKind::Duration => "duration",
2238 AcceptedFieldKind::Float32 => "float32",
2239 AcceptedFieldKind::Float64 => "float64",
2240 AcceptedFieldKind::Int8 => "int8",
2241 AcceptedFieldKind::Int16 => "int16",
2242 AcceptedFieldKind::Int32 => "int32",
2243 AcceptedFieldKind::Int64 => "int64",
2244 AcceptedFieldKind::Int128 => "int128",
2245 AcceptedFieldKind::Principal => "principal",
2246 AcceptedFieldKind::Subaccount => "subaccount",
2247 AcceptedFieldKind::Timestamp => "timestamp",
2248 AcceptedFieldKind::Nat8 => "nat8",
2249 AcceptedFieldKind::Nat16 => "nat16",
2250 AcceptedFieldKind::Nat32 => "nat32",
2251 AcceptedFieldKind::Nat64 => "nat64",
2252 AcceptedFieldKind::Nat128 => "nat128",
2253 AcceptedFieldKind::Ulid => "ulid",
2254 AcceptedFieldKind::Unit => "unit",
2255 AcceptedFieldKind::U256 => "u256",
2256 AcceptedFieldKind::Blob { .. }
2257 | AcceptedFieldKind::Decimal { .. }
2258 | AcceptedFieldKind::Enum { .. }
2259 | AcceptedFieldKind::IntBig { .. }
2260 | AcceptedFieldKind::NatBig { .. }
2261 | AcceptedFieldKind::Text { .. }
2262 | AcceptedFieldKind::Relation { .. }
2263 | AcceptedFieldKind::List(_)
2264 | AcceptedFieldKind::Set(_)
2265 | AcceptedFieldKind::Map { .. }
2266 | AcceptedFieldKind::Composite { .. } => return None,
2267 })
2268}
2269
2270#[cfg(test)]
2275mod tests {
2276 use std::collections::BTreeMap;
2277
2278 use super::{
2279 EntityFieldDescription, EntityIdentityDescription, EntityRelationCardinality,
2280 EntityRelationDescription, MAX_SCHEMA_VALUE_RENDER_CHARS, SqlColumnDefault, SqlColumnExtra,
2281 SqlColumnKey, SqlColumnSummary, SqlDescribeOutput, SqlShowRelationsOutput,
2282 classify_compact_column_key, compact_column_capacity_from_counts, compact_column_extras,
2283 describe_accepted_constraint, describe_compact_columns_with_persisted_schema,
2284 describe_constraint_activation, describe_entity_fields_with_persisted_schema,
2285 nested_path_nullable,
2286 };
2287 use crate::db::schema::{
2288 AcceptedCheckExprV1, AcceptedCheckValueExprV1, AcceptedCompositeCatalog,
2289 AcceptedConstraintCatalog, AcceptedConstraintKind, AcceptedConstraintSnapshot,
2290 AcceptedFieldKind, AcceptedNamedTypeIdentity, AcceptedRuleOperation, AcceptedRuleTarget,
2291 AcceptedSchemaFingerprint, AcceptedSchemaRevision, AcceptedSchemaSnapshot,
2292 AcceptedValueCatalogHandle, CompositeFieldId, CompositeTypeId, ConstraintActivationKind,
2293 ConstraintActivationSnapshot, ConstraintActivationState, ConstraintId, ConstraintOrigin,
2294 ConstraintValidationJob, FieldId, FieldStorageDecode, LeafCodec, MAX_SCHEMA_SNAPSHOT_BYTES,
2295 PersistedFieldSnapshot, PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot,
2296 PersistedIndexSnapshot, PersistedNestedLeafSnapshot, PersistedRelationEdgeSnapshot,
2297 PersistedSchemaSnapshot, RelationId, ScalarCodec, SchemaFieldSlot, SchemaIndexId,
2298 SchemaInsertDefault, SchemaRowLayout, SchemaVersion,
2299 composite_catalog::{
2300 AcceptedCompositeElement, AcceptedCompositeField, AcceptedCompositeShape,
2301 decode_accepted_composite_catalog, encode_accepted_composite_catalog,
2302 },
2303 decode_persisted_schema_snapshot, empty_accepted_enum_catalog_for_tests,
2304 encode_persisted_schema_snapshot,
2305 };
2306
2307 use candid::Encode;
2308
2309 const REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS: usize = icydb_schema::MAX_FRAGMENT_FIELDS;
2310 const REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES: usize = 95;
2311 const IC_QUERY_REPLY_BYTES: usize = 3 * 1024 * 1024;
2312
2313 fn constraint_description_fixture() -> (PersistedSchemaSnapshot, AcceptedValueCatalogHandle) {
2314 let snapshot = PersistedSchemaSnapshot::new_with_indexes(
2315 SchemaVersion::initial(),
2316 "tests::Account".to_string(),
2317 "Account".to_string(),
2318 FieldId::new(1),
2319 SchemaRowLayout::initial(vec![
2320 (FieldId::new(1), SchemaFieldSlot::new(0)),
2321 (FieldId::new(2), SchemaFieldSlot::new(1)),
2322 ]),
2323 vec![
2324 PersistedFieldSnapshot::new_initial(
2325 FieldId::new(1),
2326 "id".to_string(),
2327 SchemaFieldSlot::new(0),
2328 AcceptedFieldKind::Ulid,
2329 Vec::new(),
2330 false,
2331 SchemaInsertDefault::None,
2332 FieldStorageDecode::ByKind,
2333 LeafCodec::Scalar(ScalarCodec::Ulid),
2334 ),
2335 PersistedFieldSnapshot::new_initial(
2336 FieldId::new(2),
2337 "email".to_string(),
2338 SchemaFieldSlot::new(1),
2339 AcceptedFieldKind::Text { max_len: None },
2340 Vec::new(),
2341 true,
2342 SchemaInsertDefault::None,
2343 FieldStorageDecode::ByKind,
2344 LeafCodec::Scalar(ScalarCodec::Text),
2345 ),
2346 ],
2347 vec![PersistedIndexSnapshot::new(
2348 SchemaIndexId::new(1).expect("test index identity should be non-zero"),
2349 1,
2350 "account_email".to_string(),
2351 "tests::Account::account_email".to_string(),
2352 true,
2353 PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
2354 FieldId::new(2),
2355 SchemaFieldSlot::new(1),
2356 vec!["email".to_string()],
2357 AcceptedFieldKind::Text { max_len: None },
2358 true,
2359 )]),
2360 Some("email IS NOT NULL".to_string()),
2361 )],
2362 );
2363 let catalog = AcceptedConstraintCatalog::initial(
2364 snapshot.fields(),
2365 snapshot.indexes(),
2366 snapshot.relations(),
2367 )
2368 .expect("fixture constraints should build");
2369 let snapshot = snapshot.with_constraint_catalog(catalog);
2370 let value_catalog = AcceptedValueCatalogHandle::new_for_tests(
2371 empty_accepted_enum_catalog_for_tests(),
2372 AcceptedCompositeCatalog::empty(),
2373 AcceptedSchemaRevision::INITIAL,
2374 );
2375 (snapshot, value_catalog)
2376 }
2377
2378 #[test]
2379 fn filtered_unique_constraint_description_exposes_partial_backing_contract() {
2380 let (snapshot, value_catalog) = constraint_description_fixture();
2381 let constraint = snapshot
2382 .constraints()
2383 .iter()
2384 .find(|constraint| constraint.name() == "account_email")
2385 .expect("unique constraint should exist");
2386
2387 let description = describe_accepted_constraint(&snapshot, &value_catalog, constraint)
2388 .expect("accepted unique constraint should describe");
2389 assert_eq!(description.index_id(), Some(1));
2390 assert_eq!(description.index(), Some("account_email"));
2391 assert_eq!(description.predicate_sql(), Some("email IS NOT NULL"));
2392 assert_eq!(description.semantics(), "partial_unique_index_v1");
2393 }
2394
2395 #[test]
2396 fn row_local_constraint_descriptions_preserve_meaning_across_activation() {
2397 let (snapshot, values) = constraint_description_fixture();
2398 let field_id = FieldId::new(2);
2399 let expression = Box::new(AcceptedCheckExprV1::IsNotNull(
2400 AcceptedCheckValueExprV1::Field(field_id),
2401 ));
2402 let target = AcceptedRuleTarget::new(
2403 field_id,
2404 AcceptedNamedTypeIdentity::Composite(CompositeTypeId::new(1).unwrap()),
2405 );
2406 let operation = Box::new(AcceptedRuleOperation::LengthRangeInclusive { min: 1, max: 10 });
2407 for (accepted, activating, semantics) in [
2408 (
2409 AcceptedConstraintKind::NotNull { field_id },
2410 ConstraintActivationKind::NotNull { field_id },
2411 "not_null_v1",
2412 ),
2413 (
2414 AcceptedConstraintKind::Check {
2415 expression: expression.clone(),
2416 },
2417 ConstraintActivationKind::Check { expression },
2418 "check_expr_v1",
2419 ),
2420 (
2421 AcceptedConstraintKind::TargetedRule {
2422 target,
2423 operation: operation.clone(),
2424 },
2425 ConstraintActivationKind::TargetedRule { target, operation },
2426 "targeted_length_range_v1",
2427 ),
2428 ] {
2429 assert_constraint_description_lifecycle(
2430 &snapshot, &values, accepted, activating, semantics,
2431 );
2432 }
2433 }
2434
2435 fn assert_constraint_description_lifecycle(
2436 snapshot: &PersistedSchemaSnapshot,
2437 values: &AcceptedValueCatalogHandle,
2438 accepted: AcceptedConstraintKind,
2439 activating: ConstraintActivationKind,
2440 semantics: &str,
2441 ) {
2442 let id = ConstraintId::new(10).unwrap();
2443 let constraint = AcceptedConstraintSnapshot::new(
2444 id,
2445 "rule".into(),
2446 ConstraintOrigin::Generated,
2447 accepted,
2448 );
2449 let mut expected = describe_accepted_constraint(snapshot, values, &constraint).unwrap();
2450 assert_eq!(expected.semantics(), semantics);
2451 assert_eq!(expected.fields(), &["email"]);
2452 assert_eq!(expected.validation_state(), "validated");
2453 for state in [
2454 ConstraintActivationState::EnforcingNewWrites,
2455 ConstraintActivationState::Validating,
2456 ] {
2457 let activation = ConstraintActivationSnapshot::new(
2458 id,
2459 "rule".into(),
2460 ConstraintOrigin::Generated,
2461 activating.clone(),
2462 state,
2463 AcceptedSchemaFingerprint::new([1; 32]),
2464 1,
2465 );
2466 let job = (state == ConstraintActivationState::Validating).then(|| {
2467 ConstraintValidationJob::start(
2468 crate::types::EntityTag::new(1),
2469 snapshot.entity_path().into(),
2470 &activation,
2471 None,
2472 )
2473 .unwrap()
2474 });
2475 if job.is_some() {
2476 assert!(
2477 describe_constraint_activation(snapshot, values, &activation, None).is_err()
2478 );
2479 }
2480 let description =
2481 describe_constraint_activation(snapshot, values, &activation, job.as_ref())
2482 .unwrap();
2483 assert_eq!(
2484 description.validation_state(),
2485 if job.is_some() {
2486 "validating"
2487 } else {
2488 "enforcing_new_writes"
2489 }
2490 );
2491 assert_eq!(description.validation_progress().is_some(), job.is_some());
2492 expected
2493 .validation_state
2494 .clone_from(&description.validation_state);
2495 expected
2496 .validation_progress
2497 .clone_from(&description.validation_progress);
2498 assert_eq!(description, expected);
2499 }
2500 }
2501
2502 #[test]
2503 fn relation_descriptions_use_the_owner_for_their_lifecycle_state() {
2504 let (snapshot, values) = constraint_description_fixture();
2505 let relation_id = RelationId::new(1).unwrap();
2506 let accepted = PersistedRelationEdgeSnapshot::new_direct(
2507 relation_id,
2508 "accepted_relation".into(),
2509 "AcceptedTarget".into(),
2510 vec![FieldId::new(1)],
2511 );
2512 let candidate = PersistedRelationEdgeSnapshot::new_direct(
2513 relation_id,
2514 "candidate_relation".into(),
2515 "CandidateTarget".into(),
2516 vec![FieldId::new(2)],
2517 );
2518 let snapshot = snapshot
2519 .with_relations(vec![accepted])
2520 .with_constraint_candidates(Vec::new(), vec![candidate]);
2521 let id = ConstraintId::new(10).unwrap();
2522 let constraint = AcceptedConstraintSnapshot::new(
2523 id,
2524 "relation".into(),
2525 ConstraintOrigin::SqlDdl,
2526 AcceptedConstraintKind::Relation { relation_id },
2527 );
2528 let activation = ConstraintActivationSnapshot::new(
2529 id,
2530 "relation".into(),
2531 ConstraintOrigin::SqlDdl,
2532 ConstraintActivationKind::Relation { relation_id },
2533 ConstraintActivationState::EnforcingNewWrites,
2534 AcceptedSchemaFingerprint::new([1; 32]),
2535 1,
2536 );
2537 let accepted = describe_accepted_constraint(&snapshot, &values, &constraint).unwrap();
2538 let candidate =
2539 describe_constraint_activation(&snapshot, &values, &activation, None).unwrap();
2540 assert_eq!(accepted.relation(), Some("accepted_relation"));
2541 assert_eq!(accepted.target_entity(), Some("AcceptedTarget"));
2542 assert_eq!(accepted.fields(), &["id"]);
2543 assert_eq!(candidate.relation(), Some("candidate_relation"));
2544 assert_eq!(candidate.target_entity(), Some("CandidateTarget"));
2545 assert_eq!(candidate.fields(), &["email"]);
2546 assert_eq!(candidate.action(), Some("restrict"));
2547 assert_eq!(candidate.semantics(), "relation_pk_restrict_v1");
2548 }
2549
2550 #[test]
2551 fn identity_description_reports_exact_remaining_capacity_and_exhaustion() {
2552 let available =
2553 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 254)
2554 .expect("in-domain Identity description should build");
2555 assert_eq!(available.minimum(), 1);
2556 assert_eq!(available.maximum(), 255);
2557 assert_eq!(available.high_water(), 254);
2558 assert_eq!(available.remaining(), 1);
2559 assert!(!available.exhausted());
2560
2561 let exhausted =
2562 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 255)
2563 .expect("exact-domain exhaustion should remain describable");
2564 assert_eq!(exhausted.remaining(), 0);
2565 assert!(exhausted.exhausted());
2566
2567 assert!(
2568 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 256).is_err(),
2569 "state beyond the accepted domain must not be described",
2570 );
2571 }
2572
2573 #[test]
2574 fn compact_key_contract_distinguishes_single_unique_from_compound_membership() {
2575 assert_eq!(
2576 classify_compact_column_key(true, [(true, 1), (false, 2)]),
2577 SqlColumnKey::Primary
2578 );
2579 assert_eq!(
2580 classify_compact_column_key(false, [(true, 2)]),
2581 SqlColumnKey::Multiple,
2582 "compound unique membership must not imply independent uniqueness",
2583 );
2584 assert_eq!(
2585 classify_compact_column_key(false, [(false, 1), (true, 1)]),
2586 SqlColumnKey::Unique,
2587 "single-field unique membership has precedence over non-unique membership",
2588 );
2589 assert_eq!(
2590 classify_compact_column_key(false, std::iter::empty()),
2591 SqlColumnKey::None
2592 );
2593 }
2594
2595 #[test]
2596 fn compact_extra_contract_is_closed_and_deterministically_ordered() {
2597 assert_eq!(
2598 compact_column_extras(true, true, true),
2599 vec![
2600 SqlColumnExtra::Identity,
2601 SqlColumnExtra::Generated,
2602 SqlColumnExtra::Relation,
2603 ]
2604 );
2605 assert_eq!(
2606 compact_column_extras(false, true, false),
2607 vec![SqlColumnExtra::Generated]
2608 );
2609 assert!(compact_column_extras(false, false, false).is_empty());
2610 }
2611
2612 #[test]
2613 fn compact_projection_bounds_accept_maximum_and_reject_max_plus_one() {
2614 assert_eq!(
2615 compact_column_capacity_from_counts(
2616 icydb_schema::MAX_FRAGMENT_FIELDS,
2617 std::iter::repeat_n(
2618 icydb_schema::MAX_FRAGMENT_FIELDS,
2619 icydb_schema::MAX_FRAGMENT_FIELDS,
2620 ),
2621 )
2622 .expect("accepted maximum should remain projectable"),
2623 super::MAX_SQL_COMPACT_COLUMN_ROWS,
2624 );
2625 assert!(
2626 compact_column_capacity_from_counts(
2627 icydb_schema::MAX_FRAGMENT_FIELDS + 1,
2628 std::iter::repeat_n(0, icydb_schema::MAX_FRAGMENT_FIELDS + 1),
2629 )
2630 .is_err()
2631 );
2632 assert!(
2633 compact_column_capacity_from_counts(1, [icydb_schema::MAX_FRAGMENT_FIELDS + 1],)
2634 .is_err()
2635 );
2636
2637 let valid = SqlColumnSummary::new(
2638 "value".to_string(),
2639 "text".to_string(),
2640 false,
2641 SqlColumnKey::None,
2642 SqlColumnDefault::Literal {
2643 text: "x".repeat(MAX_SCHEMA_VALUE_RENDER_CHARS),
2644 },
2645 vec![
2646 SqlColumnExtra::Identity,
2647 SqlColumnExtra::Generated,
2648 SqlColumnExtra::Relation,
2649 ],
2650 );
2651 let valid = valid.expect("the complete admitted compact row should remain valid");
2652 assert!(
2653 SqlColumnSummary::new(
2654 "value".to_string(),
2655 "text".to_string(),
2656 false,
2657 SqlColumnKey::None,
2658 SqlColumnDefault::Literal {
2659 text: "x".repeat(MAX_SCHEMA_VALUE_RENDER_CHARS + 1),
2660 },
2661 Vec::new(),
2662 )
2663 .is_err()
2664 );
2665 assert!(
2666 SqlColumnSummary::new(
2667 "value".to_string(),
2668 "text".to_string(),
2669 false,
2670 SqlColumnKey::None,
2671 SqlColumnDefault::Required,
2672 vec![SqlColumnExtra::Generated; 4],
2673 )
2674 .is_err()
2675 );
2676
2677 let maximum = SqlDescribeOutput::Compact {
2678 entity: "AcceptedMaximum".to_string(),
2679 columns: vec![valid; super::MAX_SQL_COMPACT_COLUMN_ROWS],
2680 };
2681 let first = Encode!(&maximum).expect("accepted maximum should encode to bounded Candid");
2682 let second = Encode!(&maximum).expect("accepted maximum should encode deterministically");
2683 assert_eq!(first, second);
2684 assert_eq!(first.len(), 9_737_853);
2685
2686 let relation = EntityRelationDescription::new(
2687 "owner_id".to_string(),
2688 "entities::Owner".to_string(),
2689 "Owner".to_string(),
2690 "stores::Owner".to_string(),
2691 EntityRelationCardinality::Single,
2692 );
2693 assert!(
2694 SqlShowRelationsOutput::new(
2695 "Entry".to_string(),
2696 vec![relation.clone(); icydb_schema::MAX_FRAGMENT_RELATIONS],
2697 )
2698 .is_ok()
2699 );
2700 assert!(
2701 SqlShowRelationsOutput::new(
2702 "Entry".to_string(),
2703 vec![relation; icydb_schema::MAX_FRAGMENT_RELATIONS + 1],
2704 )
2705 .is_err()
2706 );
2707 }
2708
2709 #[test]
2710 fn reachable_accepted_compact_projection_exceeds_the_public_query_reply_limit() {
2711 let accepted = reachable_compact_reply_schema();
2712 let value_catalog = reachable_compact_reply_value_catalog();
2713 let columns = describe_compact_columns_with_persisted_schema(&accepted, &value_catalog)
2714 .expect("reachable accepted schema should project compact columns");
2715
2716 assert_eq!(
2717 columns.len(),
2718 1 + REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES
2719 * (1 + REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS),
2720 );
2721 let output = SqlDescribeOutput::Compact {
2722 entity: accepted.entity_name().to_string(),
2723 columns,
2724 };
2725 let encoded_output =
2726 Encode!(&output).expect("reachable accepted compact output should encode");
2727 assert_eq!(encoded_output.len(), 3_693_116);
2728 assert!(
2729 encoded_output.len() > IC_QUERY_REPLY_BYTES,
2730 "a valid accepted schema must exercise the generated endpoint reply guard",
2731 );
2732 }
2733
2734 #[test]
2735 fn field_descriptions_preserve_root_and_nested_queryability() {
2736 let accepted = reachable_compact_reply_schema();
2737 let value_catalog = reachable_compact_reply_value_catalog();
2738 let fields = describe_entity_fields_with_persisted_schema(&accepted, &value_catalog)
2739 .expect("accepted root and leaf metadata should describe");
2740
2741 assert!(fields[0].queryable());
2742 let (groups, remainder) =
2743 fields[1..].as_chunks::<{ 1 + REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS }>();
2744 assert!(remainder.is_empty());
2745 assert_eq!(groups.len(), REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES);
2746 for group in groups {
2747 assert!(!group[0].queryable(), "record roots remain non-queryable");
2748 assert!(group[1..].iter().all(EntityFieldDescription::queryable));
2749 }
2750 }
2751
2752 #[test]
2753 fn nested_nullability_includes_nullable_ancestors() {
2754 let leaves = vec![
2755 PersistedNestedLeafSnapshot::new(
2756 vec!["address".to_string()],
2757 AcceptedFieldKind::Unit,
2758 true,
2759 ),
2760 PersistedNestedLeafSnapshot::new(
2761 vec!["address".to_string(), "city".to_string()],
2762 AcceptedFieldKind::Unit,
2763 false,
2764 ),
2765 ];
2766 assert!(nested_path_nullable(
2767 false,
2768 leaves.as_slice(),
2769 &["address".to_string(), "city".to_string()],
2770 ));
2771 assert!(nested_path_nullable(
2772 true,
2773 leaves.as_slice(),
2774 &["other".to_string()],
2775 ));
2776 assert!(!nested_path_nullable(
2777 false,
2778 leaves.as_slice(),
2779 &["other".to_string()],
2780 ));
2781 }
2782
2783 fn compact_reply_leaf_name(index: usize) -> String {
2784 let first = u8::try_from(index / 26).expect("bounded leaf prefix fits u8") + b'a';
2785 let second = u8::try_from(index % 26).expect("bounded leaf suffix fits u8") + b'a';
2786 String::from_utf8(vec![first, second]).expect("ASCII leaf name should be UTF-8")
2787 }
2788
2789 fn compact_reply_top_level_name(index: usize) -> String {
2790 let prefix = format!("field_{index:03}_");
2791 format!("{prefix}{}", "x".repeat(128 - prefix.len()))
2792 }
2793
2794 fn reachable_compact_reply_schema() -> AcceptedSchemaSnapshot {
2795 let composite_type_id = CompositeTypeId::new(1).expect("one is non-zero");
2796 let nested_leaves = (0..REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS)
2797 .map(|index| {
2798 PersistedNestedLeafSnapshot::new(
2799 vec![compact_reply_leaf_name(index)],
2800 AcceptedFieldKind::Unit,
2801 false,
2802 )
2803 })
2804 .collect::<Vec<_>>();
2805 let mut fields = vec![PersistedFieldSnapshot::new_initial(
2806 FieldId::new(1),
2807 "id".to_string(),
2808 SchemaFieldSlot::new(0),
2809 AcceptedFieldKind::Nat64,
2810 Vec::new(),
2811 false,
2812 SchemaInsertDefault::None,
2813 FieldStorageDecode::ByKind,
2814 LeafCodec::Scalar(ScalarCodec::Nat64),
2815 )];
2816 let mut layout = vec![(FieldId::new(1), SchemaFieldSlot::new(0))];
2817 for index in 0..REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES {
2818 let raw_id = u32::try_from(index)
2819 .expect("bounded top-level index fits u32")
2820 .checked_add(2)
2821 .expect("bounded top-level identity has a successor");
2822 let raw_slot = u16::try_from(index)
2823 .expect("bounded top-level index fits u16")
2824 .checked_add(1)
2825 .expect("bounded top-level slot has a successor");
2826 let id = FieldId::new(raw_id);
2827 let slot = SchemaFieldSlot::new(raw_slot);
2828 fields.push(PersistedFieldSnapshot::new_initial(
2829 id,
2830 compact_reply_top_level_name(index),
2831 slot,
2832 AcceptedFieldKind::Composite {
2833 type_id: composite_type_id,
2834 },
2835 nested_leaves.clone(),
2836 false,
2837 SchemaInsertDefault::None,
2838 FieldStorageDecode::ByKind,
2839 LeafCodec::Structural,
2840 ));
2841 layout.push((id, slot));
2842 }
2843 let persisted = PersistedSchemaSnapshot::new(
2844 SchemaVersion::initial(),
2845 "tests::ReachableCompactReply".to_string(),
2846 "ReachableCompactReply".to_string(),
2847 FieldId::new(1),
2848 SchemaRowLayout::initial(layout),
2849 fields,
2850 );
2851 let encoded = encode_persisted_schema_snapshot(&persisted)
2852 .expect("reachable compact-reply schema should fit its persisted payload limit");
2853 assert_eq!(encoded.len(), 310_865);
2854 assert!(encoded.len() <= MAX_SCHEMA_SNAPSHOT_BYTES as usize);
2855 AcceptedSchemaSnapshot::try_new(
2856 decode_persisted_schema_snapshot(encoded.as_slice())
2857 .expect("persisted compact-reply schema should decode"),
2858 )
2859 .expect("decoded compact-reply schema should satisfy accepted integrity")
2860 }
2861
2862 fn reachable_compact_reply_value_catalog() -> AcceptedValueCatalogHandle {
2863 let enum_catalog = empty_accepted_enum_catalog_for_tests();
2864 let composite_type_id = CompositeTypeId::new(1).expect("one is non-zero");
2865 let composite_fields = (0..REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS)
2866 .map(|index| {
2867 let raw_id = u32::try_from(index)
2868 .expect("bounded composite index fits u32")
2869 .checked_add(1)
2870 .expect("bounded composite identity has a successor");
2871 AcceptedCompositeField::new(
2872 CompositeFieldId::new(raw_id).expect("composite field identity is non-zero"),
2873 compact_reply_leaf_name(index),
2874 AcceptedCompositeElement::new(AcceptedFieldKind::Unit, false),
2875 )
2876 })
2877 .collect::<Vec<_>>();
2878 let composite_catalog = AcceptedCompositeCatalog::from_initial_definitions(
2879 BTreeMap::from([(
2880 composite_type_id,
2881 (
2882 "tests::CompactReplyRecord".to_string(),
2883 AcceptedCompositeShape::Record(composite_fields),
2884 ),
2885 )]),
2886 &enum_catalog,
2887 )
2888 .expect("bounded reusable record composite should admit");
2889 let encoded = encode_accepted_composite_catalog(&composite_catalog, &enum_catalog)
2890 .expect("reachable composite authority should fit its persisted payload limit");
2891 assert!(encoded.len() <= MAX_SCHEMA_SNAPSHOT_BYTES as usize);
2892 let composite_catalog = decode_accepted_composite_catalog(&encoded, &enum_catalog)
2893 .expect("persisted composite authority should decode");
2894 AcceptedValueCatalogHandle::new_for_tests(
2895 enum_catalog,
2896 composite_catalog,
2897 AcceptedSchemaRevision::INITIAL,
2898 )
2899 }
2900}