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