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 {
2075 OutputValue::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 {
2092 OutputValue::Account(_) => "account",
2093 OutputValue::Blob(_) => "blob",
2094 OutputValue::Bool(_) => "bool",
2095 OutputValue::Date(_) => "date",
2096 OutputValue::Decimal(_) => "decimal",
2097 OutputValue::Duration(_) => "duration",
2098 OutputValue::Enum(_) => "enum",
2099 OutputValue::Float32(_) => "float32",
2100 OutputValue::Float64(_) => "float64",
2101 OutputValue::Int64(_) => "int64",
2102 OutputValue::Int128(_) => "int128",
2103 OutputValue::IntBig(_) => "int_big",
2104 OutputValue::List(_) => "list",
2105 OutputValue::Map(_) => "map",
2106 OutputValue::Null => "null",
2107 OutputValue::Principal(_) => "principal",
2108 OutputValue::Subaccount(_) => "subaccount",
2109 OutputValue::Text(_) => "text",
2110 OutputValue::Timestamp(_) => "timestamp",
2111 OutputValue::Nat64(_) => "nat64",
2112 OutputValue::Nat128(_) => "nat128",
2113 OutputValue::NatBig(_) => "nat_big",
2114 OutputValue::Ulid(_) => "ulid",
2115 OutputValue::Unit => "unit",
2116 }
2117}
2118
2119fn short_default_payload_fingerprint(payload: &[u8]) -> String {
2120 let digest = Sha256::digest(payload);
2121 let mut out = String::with_capacity(16);
2122 for byte in &digest[..8] {
2123 let _ = write!(out, "{byte:02x}");
2124 }
2125 out
2126}
2127
2128#[cfg_attr(
2129 doc,
2130 doc = "Render one stable field-kind label from accepted persisted schema metadata."
2131)]
2132fn summarize_persisted_field_kind(
2133 kind: &AcceptedFieldKind,
2134 value_catalog: &AcceptedValueCatalogHandle,
2135) -> Result<String, InternalError> {
2136 let mut out = String::new();
2137 write_persisted_field_kind_summary(&mut out, kind, value_catalog)?;
2138
2139 Ok(out)
2140}
2141
2142fn write_persisted_field_kind_summary(
2145 out: &mut String,
2146 kind: &AcceptedFieldKind,
2147 value_catalog: &AcceptedValueCatalogHandle,
2148) -> Result<(), InternalError> {
2149 if let Some(name) = describe_kind_name(kind) {
2150 out.push_str(name);
2151 return Ok(());
2152 }
2153
2154 match kind {
2155 AcceptedFieldKind::Blob { max_len } => {
2156 write_length_bounded_field_kind_summary(out, "blob", *max_len);
2157 }
2158 AcceptedFieldKind::Decimal { scale } => {
2159 let _ = write!(out, "decimal(scale={scale})");
2160 }
2161 AcceptedFieldKind::IntBig { max_bytes } => {
2162 write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
2163 }
2164 AcceptedFieldKind::Enum { type_id } => {
2165 let definition = value_catalog
2166 .enum_catalog()
2167 .enum_type(*type_id)
2168 .ok_or_else(InternalError::store_invariant)?;
2169 out.push_str("enum(");
2170 out.push_str(definition.path());
2171 out.push(')');
2172 }
2173 AcceptedFieldKind::Text { max_len } => {
2174 write_length_bounded_field_kind_summary(out, "text", *max_len);
2175 }
2176 AcceptedFieldKind::Relation {
2177 target_entity_name,
2178 key_kind,
2179 ..
2180 } => {
2181 out.push_str("relation(target=");
2182 out.push_str(target_entity_name);
2183 out.push_str(", key=");
2184 write_persisted_field_kind_summary(out, key_kind, value_catalog)?;
2185 out.push(')');
2186 }
2187 AcceptedFieldKind::List(inner) => {
2188 out.push_str("list<");
2189 write_persisted_field_kind_summary(out, inner, value_catalog)?;
2190 out.push('>');
2191 }
2192 AcceptedFieldKind::Set(inner) => {
2193 out.push_str("set<");
2194 write_persisted_field_kind_summary(out, inner, value_catalog)?;
2195 out.push('>');
2196 }
2197 AcceptedFieldKind::Map { key, value } => {
2198 out.push_str("map<");
2199 write_persisted_field_kind_summary(out, key, value_catalog)?;
2200 out.push_str(", ");
2201 write_persisted_field_kind_summary(out, value, value_catalog)?;
2202 out.push('>');
2203 }
2204 AcceptedFieldKind::Composite { type_id } => {
2205 let composite_catalog = value_catalog.composite_catalog();
2206 let definition = composite_catalog
2207 .composite_type(*type_id)
2208 .ok_or_else(InternalError::store_invariant)?;
2209 out.push_str("composite(path=");
2210 out.push_str(definition.path());
2211 out.push_str(", codec=");
2212 write_composite_codec_summary(out, definition.codec());
2213 out.push_str(", shape=");
2214 write_accepted_composite_shape_summary(out, definition.shape(), value_catalog)?;
2215 out.push(')');
2216 }
2217 AcceptedFieldKind::Account
2218 | AcceptedFieldKind::Bool
2219 | AcceptedFieldKind::Date
2220 | AcceptedFieldKind::Duration
2221 | AcceptedFieldKind::Float32
2222 | AcceptedFieldKind::Float64
2223 | AcceptedFieldKind::Int8
2224 | AcceptedFieldKind::Int16
2225 | AcceptedFieldKind::Int32
2226 | AcceptedFieldKind::Int64
2227 | AcceptedFieldKind::Int128
2228 | AcceptedFieldKind::Principal
2229 | AcceptedFieldKind::Subaccount
2230 | AcceptedFieldKind::Timestamp
2231 | AcceptedFieldKind::Nat8
2232 | AcceptedFieldKind::Nat16
2233 | AcceptedFieldKind::Nat32
2234 | AcceptedFieldKind::Nat64
2235 | AcceptedFieldKind::Nat128
2236 | AcceptedFieldKind::Ulid
2237 | AcceptedFieldKind::Unit => return Err(InternalError::store_invariant()),
2238 AcceptedFieldKind::NatBig { max_bytes } => {
2239 write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
2240 }
2241 }
2242
2243 Ok(())
2244}
2245
2246const fn describe_kind_name(kind: &AcceptedFieldKind) -> Option<&'static str> {
2247 Some(match kind {
2248 AcceptedFieldKind::Account => "account",
2249 AcceptedFieldKind::Bool => "bool",
2250 AcceptedFieldKind::Date => "date",
2251 AcceptedFieldKind::Duration => "duration",
2252 AcceptedFieldKind::Float32 => "float32",
2253 AcceptedFieldKind::Float64 => "float64",
2254 AcceptedFieldKind::Int8 => "int8",
2255 AcceptedFieldKind::Int16 => "int16",
2256 AcceptedFieldKind::Int32 => "int32",
2257 AcceptedFieldKind::Int64 => "int64",
2258 AcceptedFieldKind::Int128 => "int128",
2259 AcceptedFieldKind::Principal => "principal",
2260 AcceptedFieldKind::Subaccount => "subaccount",
2261 AcceptedFieldKind::Timestamp => "timestamp",
2262 AcceptedFieldKind::Nat8 => "nat8",
2263 AcceptedFieldKind::Nat16 => "nat16",
2264 AcceptedFieldKind::Nat32 => "nat32",
2265 AcceptedFieldKind::Nat64 => "nat64",
2266 AcceptedFieldKind::Nat128 => "nat128",
2267 AcceptedFieldKind::Ulid => "ulid",
2268 AcceptedFieldKind::Unit => "unit",
2269 AcceptedFieldKind::Blob { .. }
2270 | AcceptedFieldKind::Decimal { .. }
2271 | AcceptedFieldKind::Enum { .. }
2272 | AcceptedFieldKind::IntBig { .. }
2273 | AcceptedFieldKind::NatBig { .. }
2274 | AcceptedFieldKind::Text { .. }
2275 | AcceptedFieldKind::Relation { .. }
2276 | AcceptedFieldKind::List(_)
2277 | AcceptedFieldKind::Set(_)
2278 | AcceptedFieldKind::Map { .. }
2279 | AcceptedFieldKind::Composite { .. } => return None,
2280 })
2281}
2282
2283#[cfg(test)]
2288mod tests {
2289 use std::collections::BTreeMap;
2290
2291 use super::{
2292 EntityIdentityDescription, EntityRelationCardinality, EntityRelationDescription,
2293 MAX_SCHEMA_VALUE_RENDER_CHARS, SqlColumnDefault, SqlColumnExtra, SqlColumnKey,
2294 SqlColumnSummary, SqlDescribeOutput, SqlShowRelationsOutput, classify_compact_column_key,
2295 compact_column_capacity_from_counts, compact_column_extras, describe_accepted_constraint,
2296 describe_compact_columns_with_persisted_schema, nested_path_nullable,
2297 };
2298 use crate::db::schema::{
2299 AcceptedCompositeCatalog, AcceptedConstraintCatalog, AcceptedFieldKind,
2300 AcceptedSchemaRevision, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
2301 CompositeFieldId, CompositeTypeId, FieldId, FieldStorageDecode, LeafCodec,
2302 MAX_SCHEMA_SNAPSHOT_BYTES, PersistedFieldSnapshot, PersistedIndexFieldPathSnapshot,
2303 PersistedIndexKeySnapshot, PersistedIndexSnapshot, PersistedNestedLeafSnapshot,
2304 PersistedSchemaSnapshot, ScalarCodec, SchemaFieldSlot, SchemaIndexId, SchemaInsertDefault,
2305 SchemaRowLayout, SchemaVersion,
2306 composite_catalog::{
2307 AcceptedCompositeElement, AcceptedCompositeField, AcceptedCompositeShape,
2308 decode_accepted_composite_catalog, encode_accepted_composite_catalog,
2309 },
2310 decode_persisted_schema_snapshot, empty_accepted_enum_catalog_for_tests,
2311 encode_persisted_schema_snapshot,
2312 };
2313
2314 use candid::Encode;
2315
2316 const REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS: usize = icydb_schema::MAX_FRAGMENT_FIELDS;
2317 const REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES: usize = 95;
2318 const IC_QUERY_REPLY_BYTES: usize = 3 * 1024 * 1024;
2319
2320 #[test]
2321 fn filtered_unique_constraint_description_exposes_partial_backing_contract() {
2322 let snapshot = PersistedSchemaSnapshot::new_with_indexes(
2323 SchemaVersion::initial(),
2324 "tests::Account".to_string(),
2325 "Account".to_string(),
2326 FieldId::new(1),
2327 SchemaRowLayout::initial(vec![
2328 (FieldId::new(1), SchemaFieldSlot::new(0)),
2329 (FieldId::new(2), SchemaFieldSlot::new(1)),
2330 ]),
2331 vec![
2332 PersistedFieldSnapshot::new_initial(
2333 FieldId::new(1),
2334 "id".to_string(),
2335 SchemaFieldSlot::new(0),
2336 AcceptedFieldKind::Ulid,
2337 Vec::new(),
2338 false,
2339 SchemaInsertDefault::None,
2340 FieldStorageDecode::ByKind,
2341 LeafCodec::Scalar(ScalarCodec::Ulid),
2342 ),
2343 PersistedFieldSnapshot::new_initial(
2344 FieldId::new(2),
2345 "email".to_string(),
2346 SchemaFieldSlot::new(1),
2347 AcceptedFieldKind::Text { max_len: None },
2348 Vec::new(),
2349 true,
2350 SchemaInsertDefault::None,
2351 FieldStorageDecode::ByKind,
2352 LeafCodec::Scalar(ScalarCodec::Text),
2353 ),
2354 ],
2355 vec![PersistedIndexSnapshot::new(
2356 SchemaIndexId::new(1).expect("test index identity should be non-zero"),
2357 1,
2358 "account_email".to_string(),
2359 "tests::Account::account_email".to_string(),
2360 true,
2361 PersistedIndexKeySnapshot::FieldPath(vec![PersistedIndexFieldPathSnapshot::new(
2362 FieldId::new(2),
2363 SchemaFieldSlot::new(1),
2364 vec!["email".to_string()],
2365 AcceptedFieldKind::Text { max_len: None },
2366 true,
2367 )]),
2368 Some("email IS NOT NULL".to_string()),
2369 )],
2370 );
2371 let catalog = AcceptedConstraintCatalog::initial(
2372 snapshot.fields(),
2373 snapshot.indexes(),
2374 snapshot.relations(),
2375 )
2376 .expect("fixture constraints should build");
2377 let snapshot = snapshot.with_constraint_catalog(catalog);
2378 let value_catalog = AcceptedValueCatalogHandle::new_for_tests(
2379 empty_accepted_enum_catalog_for_tests(),
2380 AcceptedCompositeCatalog::empty(),
2381 AcceptedSchemaRevision::INITIAL,
2382 );
2383 let constraint = snapshot
2384 .constraints()
2385 .iter()
2386 .find(|constraint| constraint.name() == "account_email")
2387 .expect("unique constraint should exist");
2388
2389 let description = describe_accepted_constraint(&snapshot, &value_catalog, constraint)
2390 .expect("accepted unique constraint should describe");
2391 assert_eq!(description.index_id(), Some(1));
2392 assert_eq!(description.index(), Some("account_email"));
2393 assert_eq!(description.predicate_sql(), Some("email IS NOT NULL"));
2394 assert_eq!(description.semantics(), "partial_unique_index_v1");
2395 }
2396
2397 #[test]
2398 fn identity_description_reports_exact_remaining_capacity_and_exhaustion() {
2399 let available =
2400 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 254)
2401 .expect("in-domain Identity description should build");
2402 assert_eq!(available.minimum(), 1);
2403 assert_eq!(available.maximum(), 255);
2404 assert_eq!(available.high_water(), 254);
2405 assert_eq!(available.remaining(), 1);
2406 assert!(!available.exhausted());
2407
2408 let exhausted =
2409 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 255)
2410 .expect("exact-domain exhaustion should remain describable");
2411 assert_eq!(exhausted.remaining(), 0);
2412 assert!(exhausted.exhausted());
2413
2414 assert!(
2415 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 256).is_err(),
2416 "state beyond the accepted domain must not be described",
2417 );
2418 }
2419
2420 #[test]
2421 fn compact_key_contract_distinguishes_single_unique_from_compound_membership() {
2422 assert_eq!(
2423 classify_compact_column_key(true, [(true, 1), (false, 2)]),
2424 SqlColumnKey::Primary
2425 );
2426 assert_eq!(
2427 classify_compact_column_key(false, [(true, 2)]),
2428 SqlColumnKey::Multiple,
2429 "compound unique membership must not imply independent uniqueness",
2430 );
2431 assert_eq!(
2432 classify_compact_column_key(false, [(false, 1), (true, 1)]),
2433 SqlColumnKey::Unique,
2434 "single-field unique membership has precedence over non-unique membership",
2435 );
2436 assert_eq!(
2437 classify_compact_column_key(false, std::iter::empty()),
2438 SqlColumnKey::None
2439 );
2440 }
2441
2442 #[test]
2443 fn compact_extra_contract_is_closed_and_deterministically_ordered() {
2444 assert_eq!(
2445 compact_column_extras(true, true, true),
2446 vec![
2447 SqlColumnExtra::Identity,
2448 SqlColumnExtra::Generated,
2449 SqlColumnExtra::Relation,
2450 ]
2451 );
2452 assert_eq!(
2453 compact_column_extras(false, true, false),
2454 vec![SqlColumnExtra::Generated]
2455 );
2456 assert!(compact_column_extras(false, false, false).is_empty());
2457 }
2458
2459 #[test]
2460 fn compact_projection_bounds_accept_maximum_and_reject_max_plus_one() {
2461 assert_eq!(
2462 compact_column_capacity_from_counts(
2463 icydb_schema::MAX_FRAGMENT_FIELDS,
2464 std::iter::repeat_n(
2465 icydb_schema::MAX_FRAGMENT_FIELDS,
2466 icydb_schema::MAX_FRAGMENT_FIELDS,
2467 ),
2468 )
2469 .expect("accepted maximum should remain projectable"),
2470 super::MAX_SQL_COMPACT_COLUMN_ROWS,
2471 );
2472 assert!(
2473 compact_column_capacity_from_counts(
2474 icydb_schema::MAX_FRAGMENT_FIELDS + 1,
2475 std::iter::repeat_n(0, icydb_schema::MAX_FRAGMENT_FIELDS + 1),
2476 )
2477 .is_err()
2478 );
2479 assert!(
2480 compact_column_capacity_from_counts(1, [icydb_schema::MAX_FRAGMENT_FIELDS + 1],)
2481 .is_err()
2482 );
2483
2484 let valid = SqlColumnSummary::new(
2485 "value".to_string(),
2486 "text".to_string(),
2487 false,
2488 SqlColumnKey::None,
2489 SqlColumnDefault::Literal {
2490 text: "x".repeat(MAX_SCHEMA_VALUE_RENDER_CHARS),
2491 },
2492 vec![
2493 SqlColumnExtra::Identity,
2494 SqlColumnExtra::Generated,
2495 SqlColumnExtra::Relation,
2496 ],
2497 );
2498 let valid = valid.expect("the complete admitted compact row should remain valid");
2499 assert!(
2500 SqlColumnSummary::new(
2501 "value".to_string(),
2502 "text".to_string(),
2503 false,
2504 SqlColumnKey::None,
2505 SqlColumnDefault::Literal {
2506 text: "x".repeat(MAX_SCHEMA_VALUE_RENDER_CHARS + 1),
2507 },
2508 Vec::new(),
2509 )
2510 .is_err()
2511 );
2512 assert!(
2513 SqlColumnSummary::new(
2514 "value".to_string(),
2515 "text".to_string(),
2516 false,
2517 SqlColumnKey::None,
2518 SqlColumnDefault::Required,
2519 vec![SqlColumnExtra::Generated; 4],
2520 )
2521 .is_err()
2522 );
2523
2524 let maximum = SqlDescribeOutput::Compact {
2525 entity: "AcceptedMaximum".to_string(),
2526 columns: vec![valid; super::MAX_SQL_COMPACT_COLUMN_ROWS],
2527 };
2528 let first = Encode!(&maximum).expect("accepted maximum should encode to bounded Candid");
2529 let second = Encode!(&maximum).expect("accepted maximum should encode deterministically");
2530 assert_eq!(first, second);
2531 assert_eq!(first.len(), 9_737_853);
2532
2533 let relation = EntityRelationDescription::new(
2534 "owner_id".to_string(),
2535 "entities::Owner".to_string(),
2536 "Owner".to_string(),
2537 "stores::Owner".to_string(),
2538 EntityRelationCardinality::Single,
2539 );
2540 assert!(
2541 SqlShowRelationsOutput::new(
2542 "Entry".to_string(),
2543 vec![relation.clone(); icydb_schema::MAX_FRAGMENT_RELATIONS],
2544 )
2545 .is_ok()
2546 );
2547 assert!(
2548 SqlShowRelationsOutput::new(
2549 "Entry".to_string(),
2550 vec![relation; icydb_schema::MAX_FRAGMENT_RELATIONS + 1],
2551 )
2552 .is_err()
2553 );
2554 }
2555
2556 #[test]
2557 fn reachable_accepted_compact_projection_exceeds_the_public_query_reply_limit() {
2558 let accepted = reachable_compact_reply_schema();
2559 let value_catalog = reachable_compact_reply_value_catalog();
2560 let columns = describe_compact_columns_with_persisted_schema(&accepted, &value_catalog)
2561 .expect("reachable accepted schema should project compact columns");
2562
2563 assert_eq!(
2564 columns.len(),
2565 1 + REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES
2566 * (1 + REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS),
2567 );
2568 let output = SqlDescribeOutput::Compact {
2569 entity: accepted.entity_name().to_string(),
2570 columns,
2571 };
2572 let encoded_output =
2573 Encode!(&output).expect("reachable accepted compact output should encode");
2574 assert_eq!(encoded_output.len(), 3_693_116);
2575 assert!(
2576 encoded_output.len() > IC_QUERY_REPLY_BYTES,
2577 "a valid accepted schema must exercise the generated endpoint reply guard",
2578 );
2579 }
2580
2581 #[test]
2582 fn nested_nullability_includes_nullable_ancestors() {
2583 let leaves = vec![
2584 PersistedNestedLeafSnapshot::new(
2585 vec!["address".to_string()],
2586 AcceptedFieldKind::Unit,
2587 true,
2588 ),
2589 PersistedNestedLeafSnapshot::new(
2590 vec!["address".to_string(), "city".to_string()],
2591 AcceptedFieldKind::Unit,
2592 false,
2593 ),
2594 ];
2595 assert!(nested_path_nullable(
2596 false,
2597 leaves.as_slice(),
2598 &["address".to_string(), "city".to_string()],
2599 ));
2600 assert!(nested_path_nullable(
2601 true,
2602 leaves.as_slice(),
2603 &["other".to_string()],
2604 ));
2605 assert!(!nested_path_nullable(
2606 false,
2607 leaves.as_slice(),
2608 &["other".to_string()],
2609 ));
2610 }
2611
2612 fn compact_reply_leaf_name(index: usize) -> String {
2613 let first = u8::try_from(index / 26).expect("bounded leaf prefix fits u8") + b'a';
2614 let second = u8::try_from(index % 26).expect("bounded leaf suffix fits u8") + b'a';
2615 String::from_utf8(vec![first, second]).expect("ASCII leaf name should be UTF-8")
2616 }
2617
2618 fn compact_reply_top_level_name(index: usize) -> String {
2619 let prefix = format!("field_{index:03}_");
2620 format!("{prefix}{}", "x".repeat(128 - prefix.len()))
2621 }
2622
2623 fn reachable_compact_reply_schema() -> AcceptedSchemaSnapshot {
2624 let composite_type_id = CompositeTypeId::new(1).expect("one is non-zero");
2625 let nested_leaves = (0..REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS)
2626 .map(|index| {
2627 PersistedNestedLeafSnapshot::new(
2628 vec![compact_reply_leaf_name(index)],
2629 AcceptedFieldKind::Unit,
2630 false,
2631 )
2632 })
2633 .collect::<Vec<_>>();
2634 let mut fields = vec![PersistedFieldSnapshot::new_initial(
2635 FieldId::new(1),
2636 "id".to_string(),
2637 SchemaFieldSlot::new(0),
2638 AcceptedFieldKind::Nat64,
2639 Vec::new(),
2640 false,
2641 SchemaInsertDefault::None,
2642 FieldStorageDecode::ByKind,
2643 LeafCodec::Scalar(ScalarCodec::Nat64),
2644 )];
2645 let mut layout = vec![(FieldId::new(1), SchemaFieldSlot::new(0))];
2646 for index in 0..REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES {
2647 let raw_id = u32::try_from(index)
2648 .expect("bounded top-level index fits u32")
2649 .checked_add(2)
2650 .expect("bounded top-level identity has a successor");
2651 let raw_slot = u16::try_from(index)
2652 .expect("bounded top-level index fits u16")
2653 .checked_add(1)
2654 .expect("bounded top-level slot has a successor");
2655 let id = FieldId::new(raw_id);
2656 let slot = SchemaFieldSlot::new(raw_slot);
2657 fields.push(PersistedFieldSnapshot::new_initial(
2658 id,
2659 compact_reply_top_level_name(index),
2660 slot,
2661 AcceptedFieldKind::Composite {
2662 type_id: composite_type_id,
2663 },
2664 nested_leaves.clone(),
2665 false,
2666 SchemaInsertDefault::None,
2667 FieldStorageDecode::ByKind,
2668 LeafCodec::Structural,
2669 ));
2670 layout.push((id, slot));
2671 }
2672 let persisted = PersistedSchemaSnapshot::new(
2673 SchemaVersion::initial(),
2674 "tests::ReachableCompactReply".to_string(),
2675 "ReachableCompactReply".to_string(),
2676 FieldId::new(1),
2677 SchemaRowLayout::initial(layout),
2678 fields,
2679 );
2680 let encoded = encode_persisted_schema_snapshot(&persisted)
2681 .expect("reachable compact-reply schema should fit its persisted payload limit");
2682 assert_eq!(encoded.len(), 310_861);
2683 assert!(encoded.len() <= MAX_SCHEMA_SNAPSHOT_BYTES as usize);
2684 AcceptedSchemaSnapshot::try_new(
2685 decode_persisted_schema_snapshot(encoded.as_slice())
2686 .expect("persisted compact-reply schema should decode"),
2687 )
2688 .expect("decoded compact-reply schema should satisfy accepted integrity")
2689 }
2690
2691 fn reachable_compact_reply_value_catalog() -> AcceptedValueCatalogHandle {
2692 let enum_catalog = empty_accepted_enum_catalog_for_tests();
2693 let composite_type_id = CompositeTypeId::new(1).expect("one is non-zero");
2694 let composite_fields = (0..REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS)
2695 .map(|index| {
2696 let raw_id = u32::try_from(index)
2697 .expect("bounded composite index fits u32")
2698 .checked_add(1)
2699 .expect("bounded composite identity has a successor");
2700 AcceptedCompositeField::new(
2701 CompositeFieldId::new(raw_id).expect("composite field identity is non-zero"),
2702 compact_reply_leaf_name(index),
2703 AcceptedCompositeElement::new(AcceptedFieldKind::Unit, false),
2704 )
2705 })
2706 .collect::<Vec<_>>();
2707 let composite_catalog = AcceptedCompositeCatalog::from_initial_definitions(
2708 BTreeMap::from([(
2709 composite_type_id,
2710 (
2711 "tests::CompactReplyRecord".to_string(),
2712 AcceptedCompositeShape::Record(composite_fields),
2713 ),
2714 )]),
2715 &enum_catalog,
2716 )
2717 .expect("bounded reusable record composite should admit");
2718 let encoded = encode_accepted_composite_catalog(&composite_catalog, &enum_catalog)
2719 .expect("reachable composite authority should fit its persisted payload limit");
2720 assert!(encoded.len() <= MAX_SCHEMA_SNAPSHOT_BYTES as usize);
2721 let composite_catalog = decode_accepted_composite_catalog(&encoded, &enum_catalog)
2722 .expect("persisted composite authority should decode");
2723 AcceptedValueCatalogHandle::new_for_tests(
2724 enum_catalog,
2725 composite_catalog,
2726 AcceptedSchemaRevision::INITIAL,
2727 )
2728 }
2729}