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