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