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
1129pub(in crate::db) fn describe_entity_constraints_with_persisted_schema(
1131 schema: &AcceptedSchemaSnapshot,
1132 value_catalog: &AcceptedValueCatalogHandle,
1133 validation_jobs: &[ConstraintValidationJob],
1134) -> Result<Vec<EntityConstraintDescription>, InternalError> {
1135 let snapshot = schema.persisted_snapshot();
1136 let mut descriptions = snapshot
1137 .constraints()
1138 .iter()
1139 .map(|constraint| describe_accepted_constraint(snapshot, value_catalog, constraint))
1140 .collect::<Result<Vec<_>, InternalError>>()?;
1141 descriptions.extend(
1142 snapshot
1143 .constraint_activations()
1144 .iter()
1145 .map(|activation| {
1146 let job = validation_jobs
1147 .iter()
1148 .find(|job| job.constraint_id() == activation.id());
1149 describe_constraint_activation(snapshot, value_catalog, activation, job)
1150 })
1151 .collect::<Result<Vec<_>, InternalError>>()?,
1152 );
1153 if validation_jobs.iter().any(|job| {
1154 !snapshot
1155 .constraint_activations()
1156 .iter()
1157 .any(|activation| activation.id() == job.constraint_id())
1158 }) {
1159 return Err(InternalError::store_invariant());
1160 }
1161 icydb_schema::compact_sort_unstable_by(&mut descriptions, |left, right| {
1162 (left.id(), left.validation_state() != "validated")
1163 .cmp(&(right.id(), right.validation_state() != "validated"))
1164 });
1165 Ok(descriptions)
1166}
1167
1168fn describe_accepted_constraint(
1169 snapshot: &PersistedSchemaSnapshot,
1170 value_catalog: &AcceptedValueCatalogHandle,
1171 constraint: &crate::db::schema::AcceptedConstraintSnapshot,
1172) -> Result<EntityConstraintDescription, InternalError> {
1173 let mut description = accepted_constraint_description(
1174 constraint.id().get(),
1175 constraint.name(),
1176 constraint.origin(),
1177 );
1178 match constraint.kind() {
1179 AcceptedConstraintKind::PrimaryKey => {
1180 description.kind = "primary_key".to_string();
1181 description.fields = snapshot
1182 .primary_key_field_ids()
1183 .iter()
1184 .map(|field_id| accepted_field_name(snapshot, *field_id))
1185 .collect::<Result<Vec<_>, _>>()?;
1186 description.semantics = "primary_key_v1".to_string();
1187 }
1188 AcceptedConstraintKind::NotNull { field_id } => {
1189 apply_not_null_description(&mut description, snapshot, *field_id)?;
1190 }
1191 AcceptedConstraintKind::Unique { index_id } => {
1192 let index = snapshot
1193 .indexes()
1194 .iter()
1195 .find(|index| index.schema_id() == *index_id)
1196 .ok_or_else(InternalError::store_invariant)?;
1197 apply_unique_index_description(&mut description, index, snapshot, value_catalog)?;
1198 }
1199 AcceptedConstraintKind::Relation { relation_id } => {
1200 let relation = snapshot
1201 .relations()
1202 .iter()
1203 .find(|relation| relation.id() == *relation_id)
1204 .ok_or_else(InternalError::store_invariant)?;
1205 apply_relation_description(&mut description, snapshot, relation)?;
1206 }
1207 AcceptedConstraintKind::Check { expression } => {
1208 apply_check_description(&mut description, snapshot, value_catalog, expression)?;
1209 }
1210 AcceptedConstraintKind::TargetedRule { target, operation } => {
1211 apply_targeted_rule_description(&mut description, snapshot, target, operation)?;
1212 }
1213 }
1214 Ok(description)
1215}
1216
1217fn describe_constraint_activation(
1218 snapshot: &PersistedSchemaSnapshot,
1219 value_catalog: &AcceptedValueCatalogHandle,
1220 activation: &ConstraintActivationSnapshot,
1221 validation_job: Option<&ConstraintValidationJob>,
1222) -> Result<EntityConstraintDescription, InternalError> {
1223 let mut description = accepted_constraint_description(
1224 activation.id().get(),
1225 activation.name(),
1226 activation.origin(),
1227 );
1228 match activation.state() {
1229 ConstraintActivationState::EnforcingNewWrites if validation_job.is_none() => {
1230 description.validation_state = "enforcing_new_writes".to_string();
1231 }
1232 ConstraintActivationState::Validating => {
1233 let job = validation_job.ok_or_else(InternalError::store_invariant)?;
1234 job.validate(Some(activation))?;
1235 description.validation_state = "validating".to_string();
1236 description.validation_progress =
1237 Some(ConstraintValidationProgressDescription::from_job(job));
1238 }
1239 ConstraintActivationState::EnforcingNewWrites => {
1240 return Err(InternalError::store_invariant());
1241 }
1242 }
1243 match activation.kind() {
1244 ConstraintActivationKind::NotNull { field_id } => {
1245 apply_not_null_description(&mut description, snapshot, *field_id)?;
1246 }
1247 ConstraintActivationKind::Unique { index_id } => {
1248 let index = snapshot
1249 .candidate_indexes()
1250 .iter()
1251 .find(|index| index.schema_id() == *index_id)
1252 .ok_or_else(InternalError::store_invariant)?;
1253 apply_unique_index_description(&mut description, index, snapshot, value_catalog)?;
1254 }
1255 ConstraintActivationKind::Relation { relation_id } => {
1256 let relation = snapshot
1257 .candidate_relations()
1258 .iter()
1259 .find(|relation| relation.id() == *relation_id)
1260 .ok_or_else(InternalError::store_invariant)?;
1261 apply_relation_description(&mut description, snapshot, relation)?;
1262 }
1263 ConstraintActivationKind::Check { expression } => {
1264 apply_check_description(&mut description, snapshot, value_catalog, expression)?;
1265 }
1266 ConstraintActivationKind::TargetedRule { target, operation } => {
1267 apply_targeted_rule_description(&mut description, snapshot, target, operation)?;
1268 }
1269 }
1270 Ok(description)
1271}
1272
1273fn accepted_constraint_description(
1274 id: u32,
1275 name: &str,
1276 origin: ConstraintOrigin,
1277) -> EntityConstraintDescription {
1278 EntityConstraintDescription {
1279 id,
1280 name: name.to_string(),
1281 kind: String::new(),
1282 origin: accepted_constraint_origin_label(origin).to_string(),
1283 validation_state: "validated".to_string(),
1284 validation_progress: None,
1285 field_id: None,
1286 index_id: None,
1287 relation_id: None,
1288 fields: Vec::new(),
1289 index: None,
1290 predicate_sql: None,
1291 relation: None,
1292 target_entity: None,
1293 action: None,
1294 semantics: String::new(),
1295 check_sql: None,
1296 }
1297}
1298
1299fn apply_not_null_description(
1302 description: &mut EntityConstraintDescription,
1303 snapshot: &PersistedSchemaSnapshot,
1304 field_id: FieldId,
1305) -> Result<(), InternalError> {
1306 description.kind = "not_null".to_string();
1307 description.field_id = Some(field_id.get());
1308 description.fields = vec![accepted_field_name(snapshot, field_id)?];
1309 description.semantics = "not_null_v1".to_string();
1310 Ok(())
1311}
1312
1313fn apply_relation_description(
1314 description: &mut EntityConstraintDescription,
1315 snapshot: &PersistedSchemaSnapshot,
1316 relation: &PersistedRelationEdgeSnapshot,
1317) -> Result<(), InternalError> {
1318 description.kind = "relation".to_string();
1319 description.relation_id = Some(relation.id().get());
1320 description.fields = relation
1321 .source()
1322 .root_field_ids()
1323 .iter()
1324 .map(|field_id| accepted_field_name(snapshot, *field_id))
1325 .collect::<Result<Vec<_>, _>>()?;
1326 description.relation = Some(relation.name().to_string());
1327 description.target_entity = Some(relation.target_path().to_string());
1328 description.action = Some("restrict".to_string());
1329 description.semantics = "relation_pk_restrict_v1".to_string();
1330 Ok(())
1331}
1332
1333fn apply_check_description(
1334 description: &mut EntityConstraintDescription,
1335 snapshot: &PersistedSchemaSnapshot,
1336 value_catalog: &AcceptedValueCatalogHandle,
1337 expression: &AcceptedCheckExprV1,
1338) -> Result<(), InternalError> {
1339 description.kind = "check".to_string();
1340 description.fields = expression
1341 .dependencies()
1342 .into_iter()
1343 .map(|field_id| accepted_field_name(snapshot, field_id))
1344 .collect::<Result<Vec<_>, _>>()?;
1345 description.semantics = "check_expr_v1".to_string();
1346 description.check_sql = Some(render_accepted_check_expr_sql(
1347 expression,
1348 snapshot,
1349 value_catalog,
1350 )?);
1351 Ok(())
1352}
1353
1354fn apply_targeted_rule_description(
1355 description: &mut EntityConstraintDescription,
1356 snapshot: &PersistedSchemaSnapshot,
1357 target: &AcceptedRuleTarget,
1358 operation: &AcceptedRuleOperation,
1359) -> Result<(), InternalError> {
1360 description.kind = "targeted_rule".to_string();
1361 description.field_id = Some(target.root_field_id().get());
1362 description.fields = vec![accepted_field_name(snapshot, target.root_field_id())?];
1363 description.semantics = match operation {
1364 AcceptedRuleOperation::LengthRangeInclusive { .. } => "targeted_length_range_v1",
1365 AcceptedRuleOperation::MultipleOf { .. } => "targeted_multiple_of_v1",
1366 AcceptedRuleOperation::NumericMaximumInclusive { .. } => "targeted_numeric_maximum_v1",
1367 AcceptedRuleOperation::NumericMinimumInclusive { .. } => "targeted_numeric_minimum_v1",
1368 AcceptedRuleOperation::NumericRangeInclusive { .. } => "targeted_numeric_range_v1",
1369 }
1370 .to_string();
1371 Ok(())
1372}
1373
1374fn apply_unique_index_description(
1375 description: &mut EntityConstraintDescription,
1376 index: &PersistedIndexSnapshot,
1377 snapshot: &PersistedSchemaSnapshot,
1378 value_catalog: &AcceptedValueCatalogHandle,
1379) -> Result<(), InternalError> {
1380 description.kind = "unique".to_string();
1381 description.index_id = Some(index.schema_id().get());
1382 description.fields = describe_persisted_index_fields(index.key());
1383 description.index = Some(index.name().to_string());
1384 description.predicate_sql = index
1385 .predicate()
1386 .map(|predicate| predicate.render_sql(snapshot.fields(), value_catalog))
1387 .transpose()?;
1388 description.semantics = if index.predicate().is_some() {
1389 "partial_unique_index_v1"
1390 } else {
1391 "unique_index_v1"
1392 }
1393 .to_string();
1394 Ok(())
1395}
1396
1397const fn accepted_constraint_origin_label(origin: ConstraintOrigin) -> &'static str {
1398 match origin {
1399 ConstraintOrigin::Generated => "generated",
1400 ConstraintOrigin::SqlDdl => "sql_ddl",
1401 }
1402}
1403
1404fn accepted_field_name(
1405 snapshot: &crate::db::schema::PersistedSchemaSnapshot,
1406 field_id: FieldId,
1407) -> Result<String, InternalError> {
1408 snapshot
1409 .fields()
1410 .iter()
1411 .find(|field| field.id() == field_id)
1412 .map(|field| field.name().to_string())
1413 .ok_or_else(InternalError::store_invariant)
1414}
1415
1416fn render_primary_key_fields(fields: &[String]) -> String {
1417 fields.join(", ")
1418}
1419
1420fn describe_entity_indexes_with_persisted_schema(
1421 schema: &AcceptedSchemaSnapshot,
1422) -> Vec<EntityIndexDescription> {
1423 schema
1424 .persisted_snapshot()
1425 .indexes()
1426 .iter()
1427 .map(|index| {
1428 EntityIndexDescription::new(
1429 index.name().to_string(),
1430 index.unique(),
1431 describe_persisted_index_fields(index.key()),
1432 if index.generated() {
1433 "generated".to_string()
1434 } else {
1435 "ddl".to_string()
1436 },
1437 )
1438 })
1439 .collect()
1440}
1441
1442fn describe_persisted_index_fields(key: &PersistedIndexKeySnapshot) -> Vec<String> {
1443 key.items()
1444 .iter()
1445 .map(|item| match item {
1446 PersistedIndexKeyItemSnapshot::FieldPath(path) => path.path().join("."),
1447 PersistedIndexKeyItemSnapshot::Expression(expression) => {
1448 expression.canonical_text().to_string()
1449 }
1450 })
1451 .collect()
1452}
1453
1454pub(in crate::db) fn describe_compact_columns_with_persisted_schema(
1456 schema: &AcceptedSchemaSnapshot,
1457 value_catalog: &AcceptedValueCatalogHandle,
1458) -> Result<Vec<SqlColumnSummary>, InternalError> {
1459 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
1460 let snapshot = schema.persisted_snapshot();
1461 if snapshot.fields().len() != row_layout.fields().len()
1462 || snapshot.fields().len() > icydb_schema::MAX_FRAGMENT_FIELDS
1463 {
1464 return Err(InternalError::store_invariant());
1465 }
1466
1467 let capacity = compact_column_capacity(snapshot.fields())?;
1468 let mut accepted_fields = snapshot
1469 .fields()
1470 .iter()
1471 .zip(row_layout.fields())
1472 .collect::<Vec<_>>();
1473 icydb_schema::compact_sort_unstable_by(&mut accepted_fields, |left, right| {
1474 left.0.id().cmp(&right.0.id())
1475 });
1476 let mut columns = Vec::with_capacity(capacity);
1477 for (field, runtime_field) in accepted_fields {
1478 let matching_identity = field.id() == runtime_field.field_id();
1479 let matching_name = field.name() == runtime_field.name();
1480 if !matching_identity || !matching_name {
1481 return Err(InternalError::store_invariant());
1482 }
1483
1484 let generated = accepted_write_policy_generates(runtime_field);
1485 let relation = snapshot
1486 .relations()
1487 .iter()
1488 .any(|relation| relation.source().uses_root_field(field.id()));
1489 let extra = compact_column_extras(
1490 runtime_field.write_policy().insert_generation()
1491 == Some(FieldInsertGeneration::Identity),
1492 generated,
1493 relation,
1494 );
1495
1496 columns.push(SqlColumnSummary::new(
1497 field.name().to_string(),
1498 summarize_persisted_field_kind(field.kind(), value_catalog)?,
1499 field.nullable(),
1500 compact_column_key(snapshot, field.name()),
1501 compact_column_default(runtime_field, value_catalog)?,
1502 extra,
1503 )?);
1504
1505 let mut nested = field.nested_leaves().iter().collect::<Vec<_>>();
1506 icydb_schema::compact_sort_unstable_by(&mut nested, |left, right| {
1507 left.path().cmp(right.path())
1508 });
1509 for leaf in nested {
1510 let mut canonical_path = Vec::with_capacity(leaf.path().len().saturating_add(1));
1511 canonical_path.push(field.name());
1512 canonical_path.extend(leaf.path().iter().map(String::as_str));
1513 let canonical_name = canonical_path.join(".");
1514 columns.push(SqlColumnSummary::new(
1515 canonical_name.clone(),
1516 summarize_persisted_field_kind(leaf.kind(), value_catalog)?,
1517 nested_path_nullable(field.nullable(), field.nested_leaves(), leaf.path()),
1518 compact_column_key(snapshot, canonical_name.as_str()),
1519 SqlColumnDefault::NotApplicable,
1520 compact_column_extras(false, generated, false),
1521 )?);
1522 }
1523 }
1524
1525 if columns.len() != capacity {
1526 return Err(InternalError::store_invariant());
1527 }
1528 Ok(columns)
1529}
1530
1531fn compact_column_capacity(
1532 fields: &[crate::db::schema::PersistedFieldSnapshot],
1533) -> Result<usize, InternalError> {
1534 compact_column_capacity_from_counts(
1535 fields.len(),
1536 fields.iter().map(|field| field.nested_leaves().len()),
1537 )
1538}
1539
1540fn compact_column_capacity_from_counts(
1541 field_count: usize,
1542 nested_counts: impl IntoIterator<Item = usize>,
1543) -> Result<usize, InternalError> {
1544 if field_count > icydb_schema::MAX_FRAGMENT_FIELDS {
1545 return Err(InternalError::store_invariant());
1546 }
1547 let mut seen_fields = 0usize;
1548 let mut total = field_count;
1549 for nested_count in nested_counts {
1550 seen_fields = seen_fields
1551 .checked_add(1)
1552 .ok_or_else(InternalError::store_invariant)?;
1553 if nested_count > icydb_schema::MAX_FRAGMENT_FIELDS {
1554 return Err(InternalError::store_invariant());
1555 }
1556 total = total
1557 .checked_add(nested_count)
1558 .ok_or_else(InternalError::store_invariant)?;
1559 }
1560 if seen_fields != field_count {
1561 return Err(InternalError::store_invariant());
1562 }
1563 if total > MAX_SQL_COMPACT_COLUMN_ROWS {
1564 return Err(InternalError::store_invariant());
1565 }
1566 Ok(total)
1567}
1568
1569const fn accepted_write_policy_generates(field: &AcceptedRowLayoutRuntimeField<'_>) -> bool {
1570 let policy = field.write_policy();
1571 policy.insert_generation().is_some() || policy.write_management().is_some()
1572}
1573
1574fn compact_column_extras(identity: bool, generated: bool, relation: bool) -> Vec<SqlColumnExtra> {
1575 let mut extra = Vec::with_capacity(MAX_SQL_COLUMN_EXTRA_FLAGS);
1576 if identity {
1577 extra.push(SqlColumnExtra::Identity);
1578 }
1579 if generated {
1580 extra.push(SqlColumnExtra::Generated);
1581 }
1582 if relation {
1583 extra.push(SqlColumnExtra::Relation);
1584 }
1585 extra
1586}
1587
1588fn compact_column_default(
1589 field: &AcceptedRowLayoutRuntimeField<'_>,
1590 value_catalog: &AcceptedValueCatalogHandle,
1591) -> Result<SqlColumnDefault, InternalError> {
1592 if accepted_write_policy_generates(field) {
1593 return Ok(SqlColumnDefault::Auto);
1594 }
1595 match field.insert_omission_policy() {
1596 AcceptedInsertOmissionPolicy::NullIfMissing => Ok(SqlColumnDefault::Null),
1597 AcceptedInsertOmissionPolicy::DefaultIfMissing => {
1598 let payload = field
1599 .insert_default()
1600 .slot_payload()
1601 .ok_or_else(InternalError::store_invariant)?;
1602 let rendered = accepted_payload_facts(field, value_catalog, payload)?;
1603 Ok(SqlColumnDefault::Literal {
1604 text: rendered.value,
1605 })
1606 }
1607 AcceptedInsertOmissionPolicy::Required => Ok(SqlColumnDefault::Required),
1608 }
1609}
1610
1611fn nested_path_nullable(
1612 top_level_nullable: bool,
1613 leaves: &[PersistedNestedLeafSnapshot],
1614 path: &[String],
1615) -> bool {
1616 top_level_nullable
1617 || leaves.iter().any(|candidate| {
1618 candidate.path().len() <= path.len()
1619 && path.starts_with(candidate.path())
1620 && candidate.nullable()
1621 })
1622}
1623
1624fn compact_column_key(snapshot: &PersistedSchemaSnapshot, path: &str) -> SqlColumnKey {
1625 let top_level_field = snapshot.fields().iter().find(|field| field.name() == path);
1626 let primary =
1627 top_level_field.is_some_and(|field| snapshot.primary_key_field_ids().contains(&field.id()));
1628 let memberships = snapshot.indexes().iter().filter_map(|index| {
1629 let key_items = index.key().component_count();
1630 let exact_path_member = index
1631 .key()
1632 .field_paths()
1633 .any(|item| item.path().join(".") == path);
1634 if !exact_path_member {
1635 return None;
1636 }
1637 Some((index.unique(), key_items))
1638 });
1639 classify_compact_column_key(primary, memberships)
1640}
1641
1642fn classify_compact_column_key(
1643 primary: bool,
1644 memberships: impl IntoIterator<Item = (bool, usize)>,
1645) -> SqlColumnKey {
1646 if primary {
1647 return SqlColumnKey::Primary;
1648 }
1649 let mut multiple = false;
1650 for (unique, key_items) in memberships {
1651 if unique && key_items == 1 {
1652 return SqlColumnKey::Unique;
1653 }
1654 multiple = true;
1655 }
1656 if multiple {
1657 SqlColumnKey::Multiple
1658 } else {
1659 SqlColumnKey::None
1660 }
1661}
1662
1663#[cfg_attr(
1664 doc,
1665 doc = "Build field descriptors using accepted persisted schema slot metadata."
1666)]
1667#[cfg(any(test, feature = "sql"))]
1668pub(in crate::db) fn describe_entity_fields_with_persisted_schema(
1669 schema: &AcceptedSchemaSnapshot,
1670 value_catalog: &AcceptedValueCatalogHandle,
1671) -> Result<Vec<EntityFieldDescription>, InternalError> {
1672 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
1673 describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)
1674}
1675
1676fn describe_entity_fields_with_runtime_contract(
1677 schema: &AcceptedSchemaSnapshot,
1678 row_layout: &AcceptedRowLayoutRuntimeContract<'_>,
1679 value_catalog: &AcceptedValueCatalogHandle,
1680) -> Result<Vec<EntityFieldDescription>, InternalError> {
1681 let snapshot = schema.persisted_snapshot();
1682 if snapshot.fields().len() != row_layout.fields().len() {
1683 return Err(InternalError::store_invariant());
1684 }
1685 let mut fields = Vec::with_capacity(snapshot.fields().len());
1686
1687 for (field, runtime_field) in snapshot.fields().iter().zip(row_layout.fields()) {
1690 if field.id() != runtime_field.field_id() {
1691 return Err(InternalError::store_invariant());
1692 }
1693 let primary_key = snapshot.primary_key_field_ids().contains(&field.id());
1694 let slot = Some(runtime_field.slot().get());
1695 let metadata = DescribeFieldMetadata::new(
1696 summarize_persisted_field_kind(field.kind(), value_catalog)?,
1697 field.nullable(),
1698 query_field_is_queryable(field.kind(), value_catalog.composite_catalog()),
1699 field_origin_label(field.generated()),
1700 );
1701 let temporal = accepted_field_temporal_facts(runtime_field, value_catalog)?;
1702
1703 push_described_field_row(
1704 &mut fields,
1705 field.name(),
1706 slot,
1707 primary_key,
1708 None,
1709 metadata,
1710 temporal,
1711 );
1712
1713 if !field.nested_leaves().is_empty() {
1714 describe_persisted_nested_leaves(
1715 &mut fields,
1716 field.nested_leaves(),
1717 field_origin_label(field.generated()),
1718 value_catalog,
1719 )?;
1720 }
1721 }
1722
1723 Ok(fields)
1724}
1725
1726struct DescribeFieldMetadata {
1733 kind: String,
1734 nullable: bool,
1735 queryable: bool,
1736 origin: String,
1737}
1738
1739impl DescribeFieldMetadata {
1740 const fn new(kind: String, nullable: bool, queryable: bool, origin: String) -> Self {
1742 Self {
1743 kind,
1744 nullable,
1745 queryable,
1746 origin,
1747 }
1748 }
1749}
1750
1751fn push_described_field_row(
1754 fields: &mut Vec<EntityFieldDescription>,
1755 name: &str,
1756 slot: Option<u16>,
1757 primary_key: bool,
1758 tree_prefix: Option<&'static str>,
1759 metadata: DescribeFieldMetadata,
1760 temporal: EntityFieldTemporalFacts,
1761) {
1762 let display_name = if let Some(prefix) = tree_prefix {
1765 format!("{prefix}{name}")
1766 } else {
1767 name.to_string()
1768 };
1769
1770 fields.push(EntityFieldDescription::new_with_temporal_facts(
1771 display_name,
1772 slot,
1773 primary_key,
1774 metadata,
1775 temporal,
1776 ));
1777}
1778
1779fn describe_persisted_nested_leaves(
1782 fields: &mut Vec<EntityFieldDescription>,
1783 nested_leaves: &[PersistedNestedLeafSnapshot],
1784 origin: String,
1785 value_catalog: &AcceptedValueCatalogHandle,
1786) -> Result<(), InternalError> {
1787 for (index, leaf) in nested_leaves.iter().enumerate() {
1788 let prefix = if index + 1 == nested_leaves.len() {
1789 "└─ "
1790 } else {
1791 "├─ "
1792 };
1793 let name = leaf.path().last().map_or("", String::as_str);
1794 let metadata = DescribeFieldMetadata::new(
1795 summarize_persisted_field_kind(leaf.kind(), value_catalog)?,
1796 leaf.nullable(),
1797 query_field_is_queryable(leaf.kind(), value_catalog.composite_catalog()),
1798 origin.clone(),
1799 );
1800
1801 push_described_field_row(
1802 fields,
1803 name,
1804 None,
1805 false,
1806 Some(prefix),
1807 metadata,
1808 EntityFieldTemporalFacts::nested(),
1809 );
1810 }
1811
1812 Ok(())
1813}
1814
1815fn field_origin_label(generated: bool) -> String {
1816 if generated {
1817 "generated".to_string()
1818 } else {
1819 "ddl".to_string()
1820 }
1821}
1822
1823pub(in crate::db) fn describe_entity_relations_with_persisted_schema(
1824 schema: &AcceptedSchemaSnapshot,
1825 resolve_target: &impl Fn(&str) -> Result<(String, String), InternalError>,
1826) -> Result<Vec<EntityRelationDescription>, InternalError> {
1827 let snapshot = schema.persisted_snapshot();
1828 if snapshot.relations().len() > icydb_schema::MAX_FRAGMENT_RELATIONS {
1829 return Err(InternalError::store_invariant());
1830 }
1831 snapshot
1832 .relations()
1833 .iter()
1834 .map(|relation| {
1835 let local_fields = relation
1836 .source()
1837 .root_field_ids()
1838 .iter()
1839 .map(|field_id| accepted_field_name(snapshot, *field_id))
1840 .collect::<Result<Vec<_>, _>>()?;
1841 let (target_entity_name, target_store_path) = resolve_target(relation.target_path())?;
1842
1843 Ok(EntityRelationDescription::new(
1844 render_primary_key_fields(local_fields.as_slice()),
1845 relation.target_path().to_string(),
1846 target_entity_name,
1847 target_store_path,
1848 persisted_relation_cardinality(snapshot, relation)?,
1849 ))
1850 })
1851 .collect()
1852}
1853
1854fn persisted_relation_cardinality(
1855 snapshot: &PersistedSchemaSnapshot,
1856 relation: &PersistedRelationEdgeSnapshot,
1857) -> Result<EntityRelationCardinality, InternalError> {
1858 let PersistedRelationSourceSnapshot::Direct { field_ids } = relation.source() else {
1859 return Ok(EntityRelationCardinality::Single);
1860 };
1861 let [field_id] = field_ids.as_slice() else {
1862 return Ok(EntityRelationCardinality::Single);
1863 };
1864 let field = snapshot
1865 .fields()
1866 .iter()
1867 .find(|field| field.id() == *field_id)
1868 .ok_or_else(InternalError::store_invariant)?;
1869
1870 Ok(match field.kind() {
1871 AcceptedFieldKind::List(_) => EntityRelationCardinality::List,
1872 AcceptedFieldKind::Set(_) => EntityRelationCardinality::Set,
1873 _ => EntityRelationCardinality::Single,
1874 })
1875}
1876
1877fn write_accepted_composite_shape_summary(
1878 out: &mut String,
1879 shape: &AcceptedCompositeShape,
1880 value_catalog: &AcceptedValueCatalogHandle,
1881) -> Result<(), InternalError> {
1882 match shape {
1883 AcceptedCompositeShape::Record(fields) => {
1884 out.push_str("record{");
1885 for (index, field) in fields.iter().enumerate() {
1886 if index > 0 {
1887 out.push_str(", ");
1888 }
1889 out.push_str(field.name());
1890 out.push(':');
1891 write_accepted_composite_element_summary(out, field.contract(), value_catalog)?;
1892 }
1893 out.push('}');
1894 }
1895 AcceptedCompositeShape::Tuple(elements) => {
1896 out.push_str("tuple<");
1897 for (index, element) in elements.iter().enumerate() {
1898 if index > 0 {
1899 out.push_str(", ");
1900 }
1901 write_accepted_composite_element_summary(out, element, value_catalog)?;
1902 }
1903 out.push('>');
1904 }
1905 AcceptedCompositeShape::Newtype(inner) => {
1906 out.push_str("newtype<");
1907 write_accepted_composite_element_summary(out, inner, value_catalog)?;
1908 out.push('>');
1909 }
1910 }
1911
1912 Ok(())
1913}
1914
1915fn write_accepted_composite_element_summary(
1916 out: &mut String,
1917 element: &AcceptedCompositeElement,
1918 value_catalog: &AcceptedValueCatalogHandle,
1919) -> Result<(), InternalError> {
1920 write_persisted_field_kind_summary(out, element.kind(), value_catalog)?;
1921 write_composite_nullability_summary(out, element.nullable());
1922 Ok(())
1923}
1924
1925fn write_composite_codec_summary(out: &mut String, codec: CompositeCodec) {
1926 match codec {
1927 CompositeCodec::StructuralV1 => out.push_str("structural_v1"),
1928 }
1929}
1930
1931fn write_composite_nullability_summary(out: &mut String, nullable: bool) {
1932 if nullable {
1933 out.push('?');
1934 }
1935}
1936
1937fn write_length_bounded_field_kind_summary(
1941 out: &mut String,
1942 kind_name: &str,
1943 max_len: Option<u32>,
1944) {
1945 out.push_str(kind_name);
1946 if let Some(max_len) = max_len {
1947 out.push_str("(max_len=");
1948 out.push_str(&max_len.to_string());
1949 out.push(')');
1950 } else {
1951 out.push_str("(unbounded)");
1952 }
1953}
1954
1955fn write_byte_bounded_field_kind_summary(out: &mut String, kind_name: &str, max_bytes: u32) {
1956 out.push_str(kind_name);
1957 out.push_str("(max_bytes=");
1958 out.push_str(&max_bytes.to_string());
1959 out.push(')');
1960}
1961
1962struct RenderedTemporalPayload {
1970 value: String,
1971 bytes: u32,
1972 hash: String,
1973}
1974
1975fn accepted_field_temporal_facts(
1976 field: &AcceptedRowLayoutRuntimeField<'_>,
1977 value_catalog: &AcceptedValueCatalogHandle,
1978) -> Result<EntityFieldTemporalFacts, InternalError> {
1979 let write_policy = field.write_policy();
1980 let insert_omission = if write_policy.insert_generation().is_some() {
1981 "generated"
1982 } else if write_policy.write_management().is_some() {
1983 "managed"
1984 } else {
1985 match field.insert_omission_policy() {
1986 AcceptedInsertOmissionPolicy::NullIfMissing => "null",
1987 AcceptedInsertOmissionPolicy::DefaultIfMissing => "default",
1988 AcceptedInsertOmissionPolicy::Required => "required",
1989 }
1990 };
1991 let insert_default = field
1992 .insert_default()
1993 .slot_payload()
1994 .map(|payload| accepted_payload_facts(field, value_catalog, payload))
1995 .transpose()?;
1996 let (insert_default, insert_default_bytes, insert_default_hash) = match insert_default {
1997 Some(payload) => (Some(payload.value), Some(payload.bytes), Some(payload.hash)),
1998 None => (None, None, None),
1999 };
2000 let (historical_fill, historical_fill_bytes, historical_fill_hash) =
2001 match field.historical_fill() {
2002 SchemaHistoricalFill::Reject => (Some("reject".to_string()), None, None),
2003 SchemaHistoricalFill::Null => (Some("null".to_string()), None, None),
2004 SchemaHistoricalFill::SlotPayload(payload) => {
2005 let rendered = accepted_payload_facts(field, value_catalog, payload.as_slice())?;
2006 (
2007 Some(rendered.value),
2008 Some(rendered.bytes),
2009 Some(rendered.hash),
2010 )
2011 }
2012 };
2013
2014 Ok(EntityFieldTemporalFacts {
2015 insert_omission: Some(insert_omission.to_string()),
2016 insert_default,
2017 insert_default_bytes,
2018 insert_default_hash,
2019 introduced_in_layout: Some(field.introduced_in_layout().get()),
2020 historical_fill,
2021 historical_fill_bytes,
2022 historical_fill_hash,
2023 })
2024}
2025
2026fn accepted_payload_facts(
2027 field: &AcceptedRowLayoutRuntimeField<'_>,
2028 value_catalog: &AcceptedValueCatalogHandle,
2029 payload: &[u8],
2030) -> Result<RenderedTemporalPayload, InternalError> {
2031 let persistence = AcceptedFieldPersistenceContract::new(value_catalog, field.decode_contract())
2032 .map_err(|_| InternalError::store_invariant())?;
2033 let admitted = decode_admitted_value_from_accepted_field_contract(persistence, payload)?;
2034 let output = output_value_from_runtime(value_catalog.enum_catalog(), admitted.into_value())
2035 .map_err(|_| InternalError::store_invariant())?;
2036 let hash = short_default_payload_fingerprint(payload);
2037 let rendered = bounded_schema_value_rendering(&output, payload, hash.as_str());
2038 let bytes = u32::try_from(payload.len()).map_err(|_| InternalError::store_invariant())?;
2039
2040 Ok(RenderedTemporalPayload {
2041 value: rendered,
2042 bytes,
2043 hash,
2044 })
2045}
2046
2047fn bounded_schema_value_rendering(value: &OutputValue, payload: &[u8], hash: &str) -> String {
2048 let rendered = match value.as_public() {
2049 crate::value::PublicValue::Text(value) => format!("'{}'", value.escape_default()),
2050 _ => render_output_value_text(value),
2051 };
2052 if rendered.len() <= MAX_SCHEMA_VALUE_RENDER_CHARS {
2053 return rendered;
2054 }
2055
2056 format!(
2057 "{}(bytes={}, sha256={})",
2058 output_value_kind_label(value),
2059 payload.len(),
2060 hash,
2061 )
2062}
2063
2064const fn output_value_kind_label(value: &OutputValue) -> &'static str {
2065 match value.as_public() {
2066 crate::value::PublicValue::Account(_) => "account",
2067 crate::value::PublicValue::Blob(_) => "blob",
2068 crate::value::PublicValue::Bool(_) => "bool",
2069 crate::value::PublicValue::Date(_) => "date",
2070 crate::value::PublicValue::Decimal(_) => "decimal",
2071 crate::value::PublicValue::Duration(_) => "duration",
2072 crate::value::PublicValue::Enum(_) => "enum",
2073 crate::value::PublicValue::Float32(_) => "float32",
2074 crate::value::PublicValue::Float64(_) => "float64",
2075 crate::value::PublicValue::Int64(_) => "int64",
2076 crate::value::PublicValue::Int128(_) => "int128",
2077 crate::value::PublicValue::IntBig(_) => "int_big",
2078 crate::value::PublicValue::List(_) => "list",
2079 crate::value::PublicValue::Map(_) => "map",
2080 crate::value::PublicValue::Null => "null",
2081 crate::value::PublicValue::Principal(_) => "principal",
2082 crate::value::PublicValue::Subaccount(_) => "subaccount",
2083 crate::value::PublicValue::Text(_) => "text",
2084 crate::value::PublicValue::Timestamp(_) => "timestamp",
2085 crate::value::PublicValue::Nat64(_) => "nat64",
2086 crate::value::PublicValue::Nat128(_) => "nat128",
2087 crate::value::PublicValue::NatBig(_) => "nat_big",
2088 crate::value::PublicValue::Ulid(_) => "ulid",
2089 crate::value::PublicValue::Unit => "unit",
2090 crate::value::PublicValue::U256(_) => "u256",
2091 }
2092}
2093
2094fn short_default_payload_fingerprint(payload: &[u8]) -> String {
2095 let digest = Sha256::digest(payload);
2096 let mut out = String::with_capacity(16);
2097 for byte in &digest[..8] {
2098 let _ = write!(out, "{byte:02x}");
2099 }
2100 out
2101}
2102
2103#[cfg_attr(
2104 doc,
2105 doc = "Render one stable field-kind label from accepted persisted schema metadata."
2106)]
2107fn summarize_persisted_field_kind(
2108 kind: &AcceptedFieldKind,
2109 value_catalog: &AcceptedValueCatalogHandle,
2110) -> Result<String, InternalError> {
2111 let mut out = String::new();
2112 write_persisted_field_kind_summary(&mut out, kind, value_catalog)?;
2113
2114 Ok(out)
2115}
2116
2117fn write_persisted_field_kind_summary(
2120 out: &mut String,
2121 kind: &AcceptedFieldKind,
2122 value_catalog: &AcceptedValueCatalogHandle,
2123) -> Result<(), InternalError> {
2124 if let Some(name) = describe_kind_name(kind) {
2125 out.push_str(name);
2126 return Ok(());
2127 }
2128
2129 match kind {
2130 AcceptedFieldKind::Blob { max_len } => {
2131 write_length_bounded_field_kind_summary(out, "blob", *max_len);
2132 }
2133 AcceptedFieldKind::Decimal { scale } => {
2134 let _ = write!(out, "decimal(scale={scale})");
2135 }
2136 AcceptedFieldKind::IntBig { max_bytes } => {
2137 write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
2138 }
2139 AcceptedFieldKind::Enum { type_id } => {
2140 let definition = value_catalog
2141 .enum_catalog()
2142 .enum_type(*type_id)
2143 .ok_or_else(InternalError::store_invariant)?;
2144 out.push_str("enum(");
2145 out.push_str(definition.path());
2146 out.push(')');
2147 }
2148 AcceptedFieldKind::Text { max_len } => {
2149 write_length_bounded_field_kind_summary(out, "text", *max_len);
2150 }
2151 AcceptedFieldKind::Relation {
2152 target_entity_name,
2153 key_kind,
2154 ..
2155 } => {
2156 out.push_str("relation(target=");
2157 out.push_str(target_entity_name);
2158 out.push_str(", key=");
2159 write_persisted_field_kind_summary(out, key_kind, value_catalog)?;
2160 out.push(')');
2161 }
2162 AcceptedFieldKind::List(inner) => {
2163 out.push_str("list<");
2164 write_persisted_field_kind_summary(out, inner, value_catalog)?;
2165 out.push('>');
2166 }
2167 AcceptedFieldKind::Set(inner) => {
2168 out.push_str("set<");
2169 write_persisted_field_kind_summary(out, inner, value_catalog)?;
2170 out.push('>');
2171 }
2172 AcceptedFieldKind::Map { key, value } => {
2173 out.push_str("map<");
2174 write_persisted_field_kind_summary(out, key, value_catalog)?;
2175 out.push_str(", ");
2176 write_persisted_field_kind_summary(out, value, value_catalog)?;
2177 out.push('>');
2178 }
2179 AcceptedFieldKind::Composite { type_id } => {
2180 let composite_catalog = value_catalog.composite_catalog();
2181 let definition = composite_catalog
2182 .composite_type(*type_id)
2183 .ok_or_else(InternalError::store_invariant)?;
2184 out.push_str("composite(path=");
2185 out.push_str(definition.path());
2186 out.push_str(", codec=");
2187 write_composite_codec_summary(out, definition.codec());
2188 out.push_str(", shape=");
2189 write_accepted_composite_shape_summary(out, definition.shape(), value_catalog)?;
2190 out.push(')');
2191 }
2192 AcceptedFieldKind::Account
2193 | AcceptedFieldKind::Bool
2194 | AcceptedFieldKind::Date
2195 | AcceptedFieldKind::Duration
2196 | AcceptedFieldKind::Float32
2197 | AcceptedFieldKind::Float64
2198 | AcceptedFieldKind::Int8
2199 | AcceptedFieldKind::Int16
2200 | AcceptedFieldKind::Int32
2201 | AcceptedFieldKind::Int64
2202 | AcceptedFieldKind::Int128
2203 | AcceptedFieldKind::Principal
2204 | AcceptedFieldKind::Subaccount
2205 | AcceptedFieldKind::Timestamp
2206 | AcceptedFieldKind::Nat8
2207 | AcceptedFieldKind::Nat16
2208 | AcceptedFieldKind::Nat32
2209 | AcceptedFieldKind::Nat64
2210 | AcceptedFieldKind::Nat128
2211 | AcceptedFieldKind::Ulid
2212 | AcceptedFieldKind::Unit
2213 | AcceptedFieldKind::U256 => return Err(InternalError::store_invariant()),
2214 AcceptedFieldKind::NatBig { max_bytes } => {
2215 write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
2216 }
2217 }
2218
2219 Ok(())
2220}
2221
2222const fn describe_kind_name(kind: &AcceptedFieldKind) -> Option<&'static str> {
2223 Some(match kind {
2224 AcceptedFieldKind::Account => "account",
2225 AcceptedFieldKind::Bool => "bool",
2226 AcceptedFieldKind::Date => "date",
2227 AcceptedFieldKind::Duration => "duration",
2228 AcceptedFieldKind::Float32 => "float32",
2229 AcceptedFieldKind::Float64 => "float64",
2230 AcceptedFieldKind::Int8 => "int8",
2231 AcceptedFieldKind::Int16 => "int16",
2232 AcceptedFieldKind::Int32 => "int32",
2233 AcceptedFieldKind::Int64 => "int64",
2234 AcceptedFieldKind::Int128 => "int128",
2235 AcceptedFieldKind::Principal => "principal",
2236 AcceptedFieldKind::Subaccount => "subaccount",
2237 AcceptedFieldKind::Timestamp => "timestamp",
2238 AcceptedFieldKind::Nat8 => "nat8",
2239 AcceptedFieldKind::Nat16 => "nat16",
2240 AcceptedFieldKind::Nat32 => "nat32",
2241 AcceptedFieldKind::Nat64 => "nat64",
2242 AcceptedFieldKind::Nat128 => "nat128",
2243 AcceptedFieldKind::Ulid => "ulid",
2244 AcceptedFieldKind::Unit => "unit",
2245 AcceptedFieldKind::U256 => "u256",
2246 AcceptedFieldKind::Blob { .. }
2247 | AcceptedFieldKind::Decimal { .. }
2248 | AcceptedFieldKind::Enum { .. }
2249 | AcceptedFieldKind::IntBig { .. }
2250 | AcceptedFieldKind::NatBig { .. }
2251 | AcceptedFieldKind::Text { .. }
2252 | AcceptedFieldKind::Relation { .. }
2253 | AcceptedFieldKind::List(_)
2254 | AcceptedFieldKind::Set(_)
2255 | AcceptedFieldKind::Map { .. }
2256 | AcceptedFieldKind::Composite { .. } => return None,
2257 })
2258}
2259
2260#[cfg(test)]
2265mod tests {
2266 use std::collections::BTreeMap;
2267
2268 use super::{
2269 EntityFieldDescription, EntityIdentityDescription, EntityRelationCardinality,
2270 EntityRelationDescription, MAX_SCHEMA_VALUE_RENDER_CHARS, SqlColumnDefault, SqlColumnExtra,
2271 SqlColumnKey, SqlColumnSummary, SqlDescribeOutput, SqlShowRelationsOutput,
2272 classify_compact_column_key, compact_column_capacity_from_counts, compact_column_extras,
2273 describe_accepted_constraint, describe_compact_columns_with_persisted_schema,
2274 describe_constraint_activation, describe_entity_constraints_with_persisted_schema,
2275 describe_entity_fields_with_persisted_schema, nested_path_nullable,
2276 };
2277 use crate::db::schema::{
2278 AcceptedCheckExprV1, AcceptedCheckValueExprV1, AcceptedCompositeCatalog,
2279 AcceptedConstraintCatalog, AcceptedConstraintKind, AcceptedConstraintSnapshot,
2280 AcceptedFieldKind, AcceptedNamedTypeIdentity, AcceptedRuleOperation, AcceptedRuleTarget,
2281 AcceptedSchemaFingerprint, AcceptedSchemaRevision, AcceptedSchemaSnapshot,
2282 AcceptedValueCatalogHandle, CompositeFieldId, CompositeTypeId, ConstraintActivationKind,
2283 ConstraintActivationSnapshot, ConstraintActivationState, ConstraintId, ConstraintOrigin,
2284 ConstraintValidationJob, FieldId, FieldStorageDecode, LeafCodec, MAX_SCHEMA_SNAPSHOT_BYTES,
2285 PersistedFieldSnapshot, PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot,
2286 PersistedIndexSnapshot, PersistedNestedLeafSnapshot, PersistedRelationEdgeSnapshot,
2287 PersistedSchemaSnapshot, RelationId, ScalarCodec, SchemaFieldSlot, SchemaIndexId,
2288 SchemaInsertDefault, SchemaRowLayout, SchemaVersion,
2289 composite_catalog::{
2290 AcceptedCompositeElement, AcceptedCompositeField, AcceptedCompositeShape,
2291 decode_accepted_composite_catalog, encode_accepted_composite_catalog,
2292 },
2293 decode_persisted_schema_snapshot, empty_accepted_enum_catalog_for_tests,
2294 encode_persisted_schema_snapshot,
2295 };
2296
2297 use candid::Encode;
2298
2299 const REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS: usize = icydb_schema::MAX_FRAGMENT_FIELDS;
2300 const REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES: usize = 95;
2301 const IC_QUERY_REPLY_BYTES: usize = 3 * 1024 * 1024;
2302
2303 fn constraint_description_fixture() -> (PersistedSchemaSnapshot, AcceptedValueCatalogHandle) {
2304 let snapshot = PersistedSchemaSnapshot::new_with_indexes(
2305 SchemaVersion::initial(),
2306 "tests::Account".to_string(),
2307 "Account".to_string(),
2308 FieldId::new(1),
2309 SchemaRowLayout::initial(vec![
2310 (FieldId::new(1), SchemaFieldSlot::new(0)),
2311 (FieldId::new(2), SchemaFieldSlot::new(1)),
2312 ]),
2313 vec![
2314 PersistedFieldSnapshot::new_initial(
2315 FieldId::new(1),
2316 "id".to_string(),
2317 SchemaFieldSlot::new(0),
2318 AcceptedFieldKind::Ulid,
2319 Vec::new(),
2320 false,
2321 SchemaInsertDefault::None,
2322 FieldStorageDecode::ByKind,
2323 LeafCodec::Scalar(ScalarCodec::Ulid),
2324 ),
2325 PersistedFieldSnapshot::new_initial(
2326 FieldId::new(2),
2327 "email".to_string(),
2328 SchemaFieldSlot::new(1),
2329 AcceptedFieldKind::Text { max_len: None },
2330 Vec::new(),
2331 true,
2332 SchemaInsertDefault::None,
2333 FieldStorageDecode::ByKind,
2334 LeafCodec::Scalar(ScalarCodec::Text),
2335 ),
2336 ],
2337 vec![PersistedIndexSnapshot::new(
2338 SchemaIndexId::new(1).expect("test index identity should be non-zero"),
2339 1,
2340 "account_email".to_string(),
2341 "tests::Account::account_email".to_string(),
2342 true,
2343 PersistedIndexKeySnapshot::from_field_paths(vec![
2344 PersistedIndexFieldPathSnapshot::new(
2345 FieldId::new(2),
2346 SchemaFieldSlot::new(1),
2347 vec!["email".to_string()],
2348 AcceptedFieldKind::Text { max_len: None },
2349 true,
2350 ),
2351 ]),
2352 Some(crate::db::schema::AcceptedIndexPredicate::test_non_null(2)),
2353 )],
2354 );
2355 let catalog = AcceptedConstraintCatalog::initial(
2356 snapshot.fields(),
2357 snapshot.indexes(),
2358 snapshot.relations(),
2359 )
2360 .expect("fixture constraints should build");
2361 let snapshot = snapshot.with_constraint_catalog(catalog);
2362 let value_catalog = AcceptedValueCatalogHandle::new_for_tests(
2363 empty_accepted_enum_catalog_for_tests(),
2364 AcceptedCompositeCatalog::empty(),
2365 AcceptedSchemaRevision::INITIAL,
2366 );
2367 (snapshot, value_catalog)
2368 }
2369
2370 #[test]
2371 fn filtered_unique_constraint_description_exposes_partial_backing_contract() {
2372 let (snapshot, value_catalog) = constraint_description_fixture();
2373 let constraint = snapshot
2374 .constraints()
2375 .iter()
2376 .find(|constraint| constraint.name() == "account_email")
2377 .expect("unique constraint should exist");
2378
2379 let description = describe_accepted_constraint(&snapshot, &value_catalog, constraint)
2380 .expect("accepted unique constraint should describe");
2381 assert_eq!(description.index_id(), Some(1));
2382 assert_eq!(description.index(), Some("account_email"));
2383 assert_eq!(description.predicate_sql(), Some("email IS NOT NULL"));
2384 assert_eq!(description.semantics(), "partial_unique_index_v1");
2385 }
2386
2387 #[test]
2388 fn constraint_descriptions_preserve_live_progress_and_reject_unbound_jobs() {
2389 let (snapshot, values) = constraint_description_fixture();
2390 let catalog = snapshot
2391 .constraint_catalog()
2392 .clone()
2393 .with_added_check_activation(
2394 "pending_check".into(),
2395 ConstraintOrigin::SqlDdl,
2396 AcceptedCheckExprV1::True,
2397 AcceptedSchemaFingerprint::new([1; 32]),
2398 7,
2399 )
2400 .unwrap();
2401 let id = catalog.activations()[0].id();
2402 let catalog = catalog.with_validation_started(id).unwrap();
2403 let job = ConstraintValidationJob::start(
2404 crate::types::EntityTag::new(1),
2405 snapshot.entity_path().into(),
2406 &catalog.activations()[0],
2407 None,
2408 )
2409 .unwrap();
2410 let schema = AcceptedSchemaSnapshot::new(snapshot.clone().with_constraint_catalog(catalog));
2411 let descriptions = describe_entity_constraints_with_persisted_schema(
2412 &schema,
2413 &values,
2414 std::slice::from_ref(&job),
2415 )
2416 .unwrap();
2417 assert!(
2418 descriptions
2419 .windows(2)
2420 .all(|pair| pair[0].id() < pair[1].id())
2421 );
2422 let active = descriptions
2423 .iter()
2424 .find(|row| row.id() == id.get())
2425 .unwrap();
2426 assert_eq!(active.validation_state(), "validating");
2427 assert_eq!(active.validation_progress().unwrap().phase(), "forward");
2428 assert_eq!(active.validation_progress().unwrap().rows_scanned(), 0);
2429
2430 let missing_job =
2431 describe_entity_constraints_with_persisted_schema(&schema, &values, &[]).unwrap_err();
2432 assert_eq!(
2433 missing_job.diagnostic_code(),
2434 icydb_diagnostic_code::DiagnosticCode::StoreInvariantViolation,
2435 );
2436 let unbound_job = describe_entity_constraints_with_persisted_schema(
2437 &AcceptedSchemaSnapshot::new(snapshot),
2438 &values,
2439 &[job],
2440 )
2441 .unwrap_err();
2442 assert_eq!(
2443 unbound_job.diagnostic_code(),
2444 icydb_diagnostic_code::DiagnosticCode::StoreInvariantViolation,
2445 );
2446 }
2447
2448 #[test]
2449 fn row_local_constraint_descriptions_preserve_meaning_across_activation() {
2450 let (snapshot, values) = constraint_description_fixture();
2451 let field_id = FieldId::new(2);
2452 let expression = Box::new(AcceptedCheckExprV1::IsNotNull(
2453 AcceptedCheckValueExprV1::Field(field_id),
2454 ));
2455 let target = AcceptedRuleTarget::new(
2456 field_id,
2457 AcceptedNamedTypeIdentity::Composite(CompositeTypeId::new(1).unwrap()),
2458 );
2459 let operation = Box::new(AcceptedRuleOperation::LengthRangeInclusive { min: 1, max: 10 });
2460 for (accepted, activating, semantics) in [
2461 (
2462 AcceptedConstraintKind::NotNull { field_id },
2463 ConstraintActivationKind::NotNull { field_id },
2464 "not_null_v1",
2465 ),
2466 (
2467 AcceptedConstraintKind::Check {
2468 expression: expression.clone(),
2469 },
2470 ConstraintActivationKind::Check { expression },
2471 "check_expr_v1",
2472 ),
2473 (
2474 AcceptedConstraintKind::TargetedRule {
2475 target,
2476 operation: operation.clone(),
2477 },
2478 ConstraintActivationKind::TargetedRule { target, operation },
2479 "targeted_length_range_v1",
2480 ),
2481 ] {
2482 assert_constraint_description_lifecycle(
2483 &snapshot, &values, accepted, activating, semantics,
2484 );
2485 }
2486 }
2487
2488 fn assert_constraint_description_lifecycle(
2489 snapshot: &PersistedSchemaSnapshot,
2490 values: &AcceptedValueCatalogHandle,
2491 accepted: AcceptedConstraintKind,
2492 activating: ConstraintActivationKind,
2493 semantics: &str,
2494 ) {
2495 let id = ConstraintId::new(10).unwrap();
2496 let constraint = AcceptedConstraintSnapshot::new(
2497 id,
2498 "rule".into(),
2499 ConstraintOrigin::Generated,
2500 accepted,
2501 );
2502 let mut expected = describe_accepted_constraint(snapshot, values, &constraint).unwrap();
2503 assert_eq!(expected.semantics(), semantics);
2504 assert_eq!(expected.fields(), &["email"]);
2505 assert_eq!(expected.validation_state(), "validated");
2506 for state in [
2507 ConstraintActivationState::EnforcingNewWrites,
2508 ConstraintActivationState::Validating,
2509 ] {
2510 let activation = ConstraintActivationSnapshot::new(
2511 id,
2512 "rule".into(),
2513 ConstraintOrigin::Generated,
2514 activating.clone(),
2515 state,
2516 AcceptedSchemaFingerprint::new([1; 32]),
2517 1,
2518 );
2519 let job = (state == ConstraintActivationState::Validating).then(|| {
2520 ConstraintValidationJob::start(
2521 crate::types::EntityTag::new(1),
2522 snapshot.entity_path().into(),
2523 &activation,
2524 None,
2525 )
2526 .unwrap()
2527 });
2528 if job.is_some() {
2529 assert!(
2530 describe_constraint_activation(snapshot, values, &activation, None).is_err()
2531 );
2532 }
2533 let description =
2534 describe_constraint_activation(snapshot, values, &activation, job.as_ref())
2535 .unwrap();
2536 assert_eq!(
2537 description.validation_state(),
2538 if job.is_some() {
2539 "validating"
2540 } else {
2541 "enforcing_new_writes"
2542 }
2543 );
2544 assert_eq!(description.validation_progress().is_some(), job.is_some());
2545 expected
2546 .validation_state
2547 .clone_from(&description.validation_state);
2548 expected
2549 .validation_progress
2550 .clone_from(&description.validation_progress);
2551 assert_eq!(description, expected);
2552 }
2553 }
2554
2555 #[test]
2556 fn relation_descriptions_use_the_owner_for_their_lifecycle_state() {
2557 let (snapshot, values) = constraint_description_fixture();
2558 let relation_id = RelationId::new(1).unwrap();
2559 let accepted = PersistedRelationEdgeSnapshot::new_direct(
2560 relation_id,
2561 "accepted_relation".into(),
2562 "AcceptedTarget".into(),
2563 vec![FieldId::new(1)],
2564 );
2565 let candidate = PersistedRelationEdgeSnapshot::new_direct(
2566 relation_id,
2567 "candidate_relation".into(),
2568 "CandidateTarget".into(),
2569 vec![FieldId::new(2)],
2570 );
2571 let snapshot = snapshot
2572 .with_relations(vec![accepted])
2573 .with_constraint_candidates(Vec::new(), vec![candidate]);
2574 let id = ConstraintId::new(10).unwrap();
2575 let constraint = AcceptedConstraintSnapshot::new(
2576 id,
2577 "relation".into(),
2578 ConstraintOrigin::SqlDdl,
2579 AcceptedConstraintKind::Relation { relation_id },
2580 );
2581 let activation = ConstraintActivationSnapshot::new(
2582 id,
2583 "relation".into(),
2584 ConstraintOrigin::SqlDdl,
2585 ConstraintActivationKind::Relation { relation_id },
2586 ConstraintActivationState::EnforcingNewWrites,
2587 AcceptedSchemaFingerprint::new([1; 32]),
2588 1,
2589 );
2590 let accepted = describe_accepted_constraint(&snapshot, &values, &constraint).unwrap();
2591 let candidate =
2592 describe_constraint_activation(&snapshot, &values, &activation, None).unwrap();
2593 assert_eq!(accepted.relation(), Some("accepted_relation"));
2594 assert_eq!(accepted.target_entity(), Some("AcceptedTarget"));
2595 assert_eq!(accepted.fields(), &["id"]);
2596 assert_eq!(candidate.relation(), Some("candidate_relation"));
2597 assert_eq!(candidate.target_entity(), Some("CandidateTarget"));
2598 assert_eq!(candidate.fields(), &["email"]);
2599 assert_eq!(candidate.action(), Some("restrict"));
2600 assert_eq!(candidate.semantics(), "relation_pk_restrict_v1");
2601 }
2602
2603 #[test]
2604 fn identity_description_reports_exact_remaining_capacity_and_exhaustion() {
2605 let available =
2606 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 254)
2607 .expect("in-domain Identity description should build");
2608 assert_eq!(available.minimum(), 1);
2609 assert_eq!(available.maximum(), 255);
2610 assert_eq!(available.high_water(), 254);
2611 assert_eq!(available.remaining(), 1);
2612 assert!(!available.exhausted());
2613
2614 let exhausted =
2615 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 255)
2616 .expect("exact-domain exhaustion should remain describable");
2617 assert_eq!(exhausted.remaining(), 0);
2618 assert!(exhausted.exhausted());
2619
2620 assert!(
2621 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 256).is_err(),
2622 "state beyond the accepted domain must not be described",
2623 );
2624 }
2625
2626 #[test]
2627 fn compact_key_contract_distinguishes_single_unique_from_compound_membership() {
2628 assert_eq!(
2629 classify_compact_column_key(true, [(true, 1), (false, 2)]),
2630 SqlColumnKey::Primary
2631 );
2632 assert_eq!(
2633 classify_compact_column_key(false, [(true, 2)]),
2634 SqlColumnKey::Multiple,
2635 "compound unique membership must not imply independent uniqueness",
2636 );
2637 assert_eq!(
2638 classify_compact_column_key(false, [(false, 1), (true, 1)]),
2639 SqlColumnKey::Unique,
2640 "single-field unique membership has precedence over non-unique membership",
2641 );
2642 assert_eq!(
2643 classify_compact_column_key(false, std::iter::empty()),
2644 SqlColumnKey::None
2645 );
2646 }
2647
2648 #[test]
2649 fn compact_extra_contract_is_closed_and_deterministically_ordered() {
2650 assert_eq!(
2651 compact_column_extras(true, true, true),
2652 vec![
2653 SqlColumnExtra::Identity,
2654 SqlColumnExtra::Generated,
2655 SqlColumnExtra::Relation,
2656 ]
2657 );
2658 assert_eq!(
2659 compact_column_extras(false, true, false),
2660 vec![SqlColumnExtra::Generated]
2661 );
2662 assert!(compact_column_extras(false, false, false).is_empty());
2663 }
2664
2665 #[test]
2666 fn compact_projection_bounds_accept_maximum_and_reject_max_plus_one() {
2667 assert_eq!(
2668 compact_column_capacity_from_counts(
2669 icydb_schema::MAX_FRAGMENT_FIELDS,
2670 std::iter::repeat_n(
2671 icydb_schema::MAX_FRAGMENT_FIELDS,
2672 icydb_schema::MAX_FRAGMENT_FIELDS,
2673 ),
2674 )
2675 .expect("accepted maximum should remain projectable"),
2676 super::MAX_SQL_COMPACT_COLUMN_ROWS,
2677 );
2678 assert!(
2679 compact_column_capacity_from_counts(
2680 icydb_schema::MAX_FRAGMENT_FIELDS + 1,
2681 std::iter::repeat_n(0, icydb_schema::MAX_FRAGMENT_FIELDS + 1),
2682 )
2683 .is_err()
2684 );
2685 assert!(
2686 compact_column_capacity_from_counts(1, [icydb_schema::MAX_FRAGMENT_FIELDS + 1],)
2687 .is_err()
2688 );
2689
2690 let valid = SqlColumnSummary::new(
2691 "value".to_string(),
2692 "text".to_string(),
2693 false,
2694 SqlColumnKey::None,
2695 SqlColumnDefault::Literal {
2696 text: "x".repeat(MAX_SCHEMA_VALUE_RENDER_CHARS),
2697 },
2698 vec![
2699 SqlColumnExtra::Identity,
2700 SqlColumnExtra::Generated,
2701 SqlColumnExtra::Relation,
2702 ],
2703 );
2704 let valid = valid.expect("the complete admitted compact row should remain valid");
2705 assert!(
2706 SqlColumnSummary::new(
2707 "value".to_string(),
2708 "text".to_string(),
2709 false,
2710 SqlColumnKey::None,
2711 SqlColumnDefault::Literal {
2712 text: "x".repeat(MAX_SCHEMA_VALUE_RENDER_CHARS + 1),
2713 },
2714 Vec::new(),
2715 )
2716 .is_err()
2717 );
2718 assert!(
2719 SqlColumnSummary::new(
2720 "value".to_string(),
2721 "text".to_string(),
2722 false,
2723 SqlColumnKey::None,
2724 SqlColumnDefault::Required,
2725 vec![SqlColumnExtra::Generated; 4],
2726 )
2727 .is_err()
2728 );
2729
2730 let maximum = SqlDescribeOutput::Compact {
2731 entity: "AcceptedMaximum".to_string(),
2732 columns: vec![valid; super::MAX_SQL_COMPACT_COLUMN_ROWS],
2733 };
2734 let first = Encode!(&maximum).expect("accepted maximum should encode to bounded Candid");
2735 let second = Encode!(&maximum).expect("accepted maximum should encode deterministically");
2736 assert_eq!(first, second);
2737 assert_eq!(first.len(), 9_737_853);
2738
2739 let relation = EntityRelationDescription::new(
2740 "owner_id".to_string(),
2741 "entities::Owner".to_string(),
2742 "Owner".to_string(),
2743 "stores::Owner".to_string(),
2744 EntityRelationCardinality::Single,
2745 );
2746 assert!(
2747 SqlShowRelationsOutput::new(
2748 "Entry".to_string(),
2749 vec![relation.clone(); icydb_schema::MAX_FRAGMENT_RELATIONS],
2750 )
2751 .is_ok()
2752 );
2753 assert!(
2754 SqlShowRelationsOutput::new(
2755 "Entry".to_string(),
2756 vec![relation; icydb_schema::MAX_FRAGMENT_RELATIONS + 1],
2757 )
2758 .is_err()
2759 );
2760 }
2761
2762 #[test]
2763 fn reachable_accepted_compact_projection_exceeds_the_public_query_reply_limit() {
2764 let accepted = reachable_compact_reply_schema();
2765 let value_catalog = reachable_compact_reply_value_catalog();
2766 let columns = describe_compact_columns_with_persisted_schema(&accepted, &value_catalog)
2767 .expect("reachable accepted schema should project compact columns");
2768
2769 assert_eq!(
2770 columns.len(),
2771 1 + REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES
2772 * (1 + REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS),
2773 );
2774 let output = SqlDescribeOutput::Compact {
2775 entity: accepted.entity_name().to_string(),
2776 columns,
2777 };
2778 let encoded_output =
2779 Encode!(&output).expect("reachable accepted compact output should encode");
2780 assert_eq!(encoded_output.len(), 3_693_116);
2781 assert!(
2782 encoded_output.len() > IC_QUERY_REPLY_BYTES,
2783 "a valid accepted schema must exercise the generated endpoint reply guard",
2784 );
2785 }
2786
2787 #[test]
2788 fn field_descriptions_preserve_root_and_nested_queryability() {
2789 let accepted = reachable_compact_reply_schema();
2790 let value_catalog = reachable_compact_reply_value_catalog();
2791 let fields = describe_entity_fields_with_persisted_schema(&accepted, &value_catalog)
2792 .expect("accepted root and leaf metadata should describe");
2793
2794 assert!(fields[0].queryable());
2795 let (groups, remainder) =
2796 fields[1..].as_chunks::<{ 1 + REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS }>();
2797 assert!(remainder.is_empty());
2798 assert_eq!(groups.len(), REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES);
2799 for group in groups {
2800 assert!(!group[0].queryable(), "record roots remain non-queryable");
2801 assert!(group[1..].iter().all(EntityFieldDescription::queryable));
2802 }
2803 }
2804
2805 #[test]
2806 fn nested_nullability_includes_nullable_ancestors() {
2807 let leaves = vec![
2808 PersistedNestedLeafSnapshot::new(
2809 vec!["address".to_string()],
2810 AcceptedFieldKind::Unit,
2811 true,
2812 ),
2813 PersistedNestedLeafSnapshot::new(
2814 vec!["address".to_string(), "city".to_string()],
2815 AcceptedFieldKind::Unit,
2816 false,
2817 ),
2818 ];
2819 assert!(nested_path_nullable(
2820 false,
2821 leaves.as_slice(),
2822 &["address".to_string(), "city".to_string()],
2823 ));
2824 assert!(nested_path_nullable(
2825 true,
2826 leaves.as_slice(),
2827 &["other".to_string()],
2828 ));
2829 assert!(!nested_path_nullable(
2830 false,
2831 leaves.as_slice(),
2832 &["other".to_string()],
2833 ));
2834 }
2835
2836 fn compact_reply_leaf_name(index: usize) -> String {
2837 let first = u8::try_from(index / 26).expect("bounded leaf prefix fits u8") + b'a';
2838 let second = u8::try_from(index % 26).expect("bounded leaf suffix fits u8") + b'a';
2839 String::from_utf8(vec![first, second]).expect("ASCII leaf name should be UTF-8")
2840 }
2841
2842 fn compact_reply_top_level_name(index: usize) -> String {
2843 let prefix = format!("field_{index:03}_");
2844 format!("{prefix}{}", "x".repeat(128 - prefix.len()))
2845 }
2846
2847 fn reachable_compact_reply_schema() -> AcceptedSchemaSnapshot {
2848 let composite_type_id = CompositeTypeId::new(1).expect("one is non-zero");
2849 let nested_leaves = (0..REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS)
2850 .map(|index| {
2851 PersistedNestedLeafSnapshot::new(
2852 vec![compact_reply_leaf_name(index)],
2853 AcceptedFieldKind::Unit,
2854 false,
2855 )
2856 })
2857 .collect::<Vec<_>>();
2858 let mut fields = vec![PersistedFieldSnapshot::new_initial(
2859 FieldId::new(1),
2860 "id".to_string(),
2861 SchemaFieldSlot::new(0),
2862 AcceptedFieldKind::Nat64,
2863 Vec::new(),
2864 false,
2865 SchemaInsertDefault::None,
2866 FieldStorageDecode::ByKind,
2867 LeafCodec::Scalar(ScalarCodec::Nat64),
2868 )];
2869 let mut layout = vec![(FieldId::new(1), SchemaFieldSlot::new(0))];
2870 for index in 0..REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES {
2871 let raw_id = u32::try_from(index)
2872 .expect("bounded top-level index fits u32")
2873 .checked_add(2)
2874 .expect("bounded top-level identity has a successor");
2875 let raw_slot = u16::try_from(index)
2876 .expect("bounded top-level index fits u16")
2877 .checked_add(1)
2878 .expect("bounded top-level slot has a successor");
2879 let id = FieldId::new(raw_id);
2880 let slot = SchemaFieldSlot::new(raw_slot);
2881 fields.push(PersistedFieldSnapshot::new_initial(
2882 id,
2883 compact_reply_top_level_name(index),
2884 slot,
2885 AcceptedFieldKind::Composite {
2886 type_id: composite_type_id,
2887 },
2888 nested_leaves.clone(),
2889 false,
2890 SchemaInsertDefault::None,
2891 FieldStorageDecode::ByKind,
2892 LeafCodec::Structural,
2893 ));
2894 layout.push((id, slot));
2895 }
2896 let persisted = PersistedSchemaSnapshot::new(
2897 SchemaVersion::initial(),
2898 "tests::ReachableCompactReply".to_string(),
2899 "ReachableCompactReply".to_string(),
2900 FieldId::new(1),
2901 SchemaRowLayout::initial(layout),
2902 fields,
2903 );
2904 let encoded = encode_persisted_schema_snapshot(&persisted)
2905 .expect("reachable compact-reply schema should fit its persisted payload limit");
2906 assert_eq!(encoded.len(), 310_865);
2907 assert!(encoded.len() <= MAX_SCHEMA_SNAPSHOT_BYTES as usize);
2908 AcceptedSchemaSnapshot::try_new(
2909 decode_persisted_schema_snapshot(encoded.as_slice())
2910 .expect("persisted compact-reply schema should decode"),
2911 )
2912 .expect("decoded compact-reply schema should satisfy accepted integrity")
2913 }
2914
2915 fn reachable_compact_reply_value_catalog() -> AcceptedValueCatalogHandle {
2916 let enum_catalog = empty_accepted_enum_catalog_for_tests();
2917 let composite_type_id = CompositeTypeId::new(1).expect("one is non-zero");
2918 let composite_fields = (0..REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS)
2919 .map(|index| {
2920 let raw_id = u32::try_from(index)
2921 .expect("bounded composite index fits u32")
2922 .checked_add(1)
2923 .expect("bounded composite identity has a successor");
2924 AcceptedCompositeField::new(
2925 CompositeFieldId::new(raw_id).expect("composite field identity is non-zero"),
2926 compact_reply_leaf_name(index),
2927 AcceptedCompositeElement::new(AcceptedFieldKind::Unit, false),
2928 )
2929 })
2930 .collect::<Vec<_>>();
2931 let composite_catalog = AcceptedCompositeCatalog::from_initial_definitions(
2932 BTreeMap::from([(
2933 composite_type_id,
2934 (
2935 "tests::CompactReplyRecord".to_string(),
2936 AcceptedCompositeShape::Record(composite_fields),
2937 ),
2938 )]),
2939 &enum_catalog,
2940 )
2941 .expect("bounded reusable record composite should admit");
2942 let encoded = encode_accepted_composite_catalog(&composite_catalog, &enum_catalog)
2943 .expect("reachable composite authority should fit its persisted payload limit");
2944 assert!(encoded.len() <= MAX_SCHEMA_SNAPSHOT_BYTES as usize);
2945 let composite_catalog = decode_accepted_composite_catalog(&encoded, &enum_catalog)
2946 .expect("persisted composite authority should decode");
2947 AcceptedValueCatalogHandle::new_for_tests(
2948 enum_catalog,
2949 composite_catalog,
2950 AcceptedSchemaRevision::INITIAL,
2951 )
2952 }
2953}