1use crate::{
7 db::schema::CompositeCodec,
8 db::{
9 data::decode_admitted_value_from_accepted_field_contract,
10 schema::{
11 AcceptedCheckExprV1, AcceptedConstraintKind, AcceptedFieldKind,
12 AcceptedFieldPersistenceContract, AcceptedIdentityInspection,
13 AcceptedInsertOmissionPolicy, AcceptedRowLayoutRuntimeContract, AcceptedRuleOperation,
14 AcceptedRuleTarget, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
15 ConstraintActivationKind, ConstraintActivationSnapshot, ConstraintActivationState,
16 ConstraintOrigin, ConstraintValidationJob, FieldId, FieldInsertGeneration,
17 PersistedIndexKeyItemSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
18 PersistedNestedLeafSnapshot, PersistedRelationEdgeSnapshot,
19 PersistedRelationPathStepSnapshot, PersistedRelationSourceSnapshot,
20 PersistedSchemaSnapshot, SchemaHistoricalFill,
21 composite_catalog::{AcceptedCompositeElement, AcceptedCompositeShape},
22 identity_kind_maximum, output_value_from_runtime, query_field_is_queryable,
23 render_accepted_check_expr_sql,
24 runtime::AcceptedRowLayoutRuntimeField,
25 },
26 },
27 error::InternalError,
28 value::{OutputValue, render_output_value_text},
29};
30use std::fmt::Write;
31
32use serde::Deserialize;
33use sha2::{Digest, Sha256};
34
35const ENTITY_FIELD_DESCRIPTION_NO_SLOT: u16 = u16::MAX;
36const MAX_SCHEMA_VALUE_RENDER_CHARS: usize = 128;
37const MAX_SQL_COLUMN_EXTRA_FLAGS: usize = 3;
38const MAX_SQL_COMPACT_COLUMN_ROWS: usize =
39 icydb_schema::MAX_FRAGMENT_FIELDS * (1 + icydb_schema::MAX_FRAGMENT_FIELDS);
40
41#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
43pub enum SqlColumnKey {
44 Primary,
46 Unique,
48 Multiple,
50 None,
52}
53
54#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
56pub enum SqlColumnDefault {
57 Auto,
59 Null,
61 Literal {
63 text: String,
65 },
66 Required,
68 NotApplicable,
70}
71
72impl SqlColumnDefault {
73 #[must_use]
75 pub const fn literal_text(&self) -> Option<&str> {
76 match self {
77 Self::Literal { text } => Some(text.as_str()),
78 Self::Auto | Self::Null | Self::Required | Self::NotApplicable => None,
79 }
80 }
81}
82
83#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
85pub enum SqlColumnExtra {
86 Identity,
88 Generated,
90 Relation,
92}
93
94#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
96pub struct SqlColumnSummary {
97 name: String,
98 field_type: String,
99 nullable: bool,
100 key: SqlColumnKey,
101 default: SqlColumnDefault,
102 extra: Vec<SqlColumnExtra>,
103}
104
105impl SqlColumnSummary {
106 fn new(
107 name: String,
108 field_type: String,
109 nullable: bool,
110 key: SqlColumnKey,
111 default: SqlColumnDefault,
112 extra: Vec<SqlColumnExtra>,
113 ) -> Result<Self, InternalError> {
114 if extra.len() > MAX_SQL_COLUMN_EXTRA_FLAGS
115 || default
116 .literal_text()
117 .is_some_and(|text| text.len() > MAX_SCHEMA_VALUE_RENDER_CHARS)
118 {
119 return Err(InternalError::store_invariant());
120 }
121 Ok(Self {
122 name,
123 field_type,
124 nullable,
125 key,
126 default,
127 extra,
128 })
129 }
130
131 #[must_use]
133 pub const fn name(&self) -> &str {
134 self.name.as_str()
135 }
136
137 #[must_use]
139 pub const fn field_type(&self) -> &str {
140 self.field_type.as_str()
141 }
142
143 #[must_use]
145 pub const fn nullable(&self) -> bool {
146 self.nullable
147 }
148
149 #[must_use]
151 pub const fn key(&self) -> SqlColumnKey {
152 self.key
153 }
154
155 #[must_use]
157 pub const fn default(&self) -> &SqlColumnDefault {
158 &self.default
159 }
160
161 #[must_use]
163 pub const fn extra(&self) -> &[SqlColumnExtra] {
164 self.extra.as_slice()
165 }
166}
167
168#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
170pub enum SqlDescribeOutput {
171 Compact {
173 entity: String,
175 columns: Vec<SqlColumnSummary>,
177 },
178 Verbose {
180 description: EntitySchemaDescription,
182 },
183}
184
185#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
187pub enum SqlShowColumnsOutput {
188 Compact {
190 entity: String,
192 columns: Vec<SqlColumnSummary>,
194 },
195 Verbose {
197 entity: String,
199 columns: Vec<EntityFieldDescription>,
201 },
202}
203
204#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
206pub struct SqlShowRelationsOutput {
207 entity: String,
208 relations: Vec<EntityRelationDescription>,
209}
210
211impl SqlShowRelationsOutput {
212 pub(in crate::db) fn new(
214 entity: String,
215 relations: Vec<EntityRelationDescription>,
216 ) -> Result<Self, InternalError> {
217 if relations.len() > icydb_schema::MAX_FRAGMENT_RELATIONS {
218 return Err(InternalError::store_invariant());
219 }
220 Ok(Self { entity, relations })
221 }
222
223 #[must_use]
225 pub const fn entity(&self) -> &str {
226 self.entity.as_str()
227 }
228
229 #[must_use]
231 pub const fn relations(&self) -> &[EntityRelationDescription] {
232 self.relations.as_slice()
233 }
234}
235
236#[cfg_attr(
237 doc,
238 doc = "EntitySchemaDescription\n\nStable describe payload for one entity model."
239)]
240#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
241pub struct EntitySchemaDescription {
242 pub(crate) entity_path: String,
243 pub(crate) entity_name: String,
244 pub(crate) entity_tag: u64,
245 pub(crate) accepted_schema_fingerprint_method: u8,
246 pub(crate) accepted_schema_fingerprint: [u8; 16],
247 pub(crate) primary_key: String,
248 pub(crate) primary_key_fields: Vec<String>,
249 pub(crate) identity: Option<Box<EntityIdentityDescription>>,
250 pub(crate) fields: Vec<EntityFieldDescription>,
251 pub(crate) indexes: Vec<EntityIndexDescription>,
252 pub(crate) relations: Vec<EntityRelationDescription>,
253 pub(crate) constraints: Vec<EntityConstraintDescription>,
254 pub(crate) row_layout_current: u32,
255 pub(crate) row_layout_history_floor: u32,
256}
257
258impl EntitySchemaDescription {
259 #[expect(
261 clippy::too_many_arguments,
262 reason = "schema description construction keeps identity, collections, and layout explicit"
263 )]
264 #[must_use]
265 pub const fn new(
266 entity_path: String,
267 entity_name: String,
268 entity_tag: u64,
269 accepted_schema_fingerprint_method: u8,
270 accepted_schema_fingerprint: [u8; 16],
271 primary_key: String,
272 primary_key_fields: Vec<String>,
273 fields: Vec<EntityFieldDescription>,
274 indexes: Vec<EntityIndexDescription>,
275 relations: Vec<EntityRelationDescription>,
276 constraints: Vec<EntityConstraintDescription>,
277 row_layout_current: u32,
278 row_layout_history_floor: u32,
279 ) -> Self {
280 Self {
281 entity_path,
282 entity_name,
283 entity_tag,
284 accepted_schema_fingerprint_method,
285 accepted_schema_fingerprint,
286 primary_key,
287 primary_key_fields,
288 identity: None,
289 fields,
290 indexes,
291 relations,
292 constraints,
293 row_layout_current,
294 row_layout_history_floor,
295 }
296 }
297
298 #[must_use]
300 pub const fn entity_path(&self) -> &str {
301 self.entity_path.as_str()
302 }
303
304 #[must_use]
306 pub const fn entity_name(&self) -> &str {
307 self.entity_name.as_str()
308 }
309
310 #[must_use]
312 pub const fn entity_tag(&self) -> u64 {
313 self.entity_tag
314 }
315
316 #[must_use]
318 pub const fn accepted_schema_fingerprint_method(&self) -> u8 {
319 self.accepted_schema_fingerprint_method
320 }
321
322 #[must_use]
324 pub const fn accepted_schema_fingerprint(&self) -> [u8; 16] {
325 self.accepted_schema_fingerprint
326 }
327
328 #[must_use]
330 pub const fn primary_key(&self) -> &str {
331 self.primary_key.as_str()
332 }
333
334 #[must_use]
336 pub const fn primary_key_fields(&self) -> &[String] {
337 self.primary_key_fields.as_slice()
338 }
339
340 #[must_use]
342 pub fn identity(&self) -> Option<&EntityIdentityDescription> {
343 self.identity.as_deref()
344 }
345
346 #[must_use]
348 pub const fn fields(&self) -> &[EntityFieldDescription] {
349 self.fields.as_slice()
350 }
351
352 #[must_use]
354 pub const fn indexes(&self) -> &[EntityIndexDescription] {
355 self.indexes.as_slice()
356 }
357
358 #[must_use]
360 pub const fn relations(&self) -> &[EntityRelationDescription] {
361 self.relations.as_slice()
362 }
363
364 #[must_use]
366 pub const fn constraints(&self) -> &[EntityConstraintDescription] {
367 self.constraints.as_slice()
368 }
369
370 #[must_use]
372 pub const fn row_layout_current(&self) -> u32 {
373 self.row_layout_current
374 }
375
376 #[must_use]
378 pub const fn row_layout_history_floor(&self) -> u32 {
379 self.row_layout_history_floor
380 }
381
382 fn with_identity(mut self, identity: Option<EntityIdentityDescription>) -> Self {
383 self.identity = identity.map(Box::new);
384 self
385 }
386}
387
388#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
391pub struct EntityIdentityDescription {
392 field: String,
393 generator: String,
394 accepted_kind: String,
395 minimum: u128,
396 maximum: u128,
397 high_water: u128,
398 remaining: u128,
399 exhausted: bool,
400}
401
402impl EntityIdentityDescription {
403 pub(in crate::db) fn new(
404 field: String,
405 accepted_kind: String,
406 maximum: u128,
407 high_water: u128,
408 ) -> Result<Self, InternalError> {
409 let remaining = maximum
410 .checked_sub(high_water)
411 .ok_or_else(InternalError::identity_state_corruption)?;
412 Ok(Self {
413 field,
414 generator: "Identity::next".to_string(),
415 accepted_kind,
416 minimum: 1,
417 maximum,
418 high_water,
419 remaining,
420 exhausted: high_water == maximum,
421 })
422 }
423
424 #[must_use]
426 pub const fn field(&self) -> &str {
427 self.field.as_str()
428 }
429
430 #[must_use]
432 pub const fn generator(&self) -> &str {
433 self.generator.as_str()
434 }
435
436 #[must_use]
438 pub const fn accepted_kind(&self) -> &str {
439 self.accepted_kind.as_str()
440 }
441
442 #[must_use]
444 pub const fn minimum(&self) -> u128 {
445 self.minimum
446 }
447
448 #[must_use]
450 pub const fn maximum(&self) -> u128 {
451 self.maximum
452 }
453
454 #[must_use]
456 pub const fn high_water(&self) -> u128 {
457 self.high_water
458 }
459
460 #[must_use]
462 pub const fn remaining(&self) -> u128 {
463 self.remaining
464 }
465
466 #[must_use]
468 pub const fn exhausted(&self) -> bool {
469 self.exhausted
470 }
471}
472
473pub(in crate::db) fn describe_accepted_identity(
474 identity: &AcceptedIdentityInspection,
475 high_water: u128,
476) -> Result<EntityIdentityDescription, InternalError> {
477 let accepted_kind = describe_kind_name(identity.accepted_kind())
478 .ok_or_else(InternalError::identity_state_corruption)?;
479 let maximum = identity_kind_maximum(identity.accepted_kind())
480 .ok_or_else(InternalError::identity_state_corruption)?;
481 EntityIdentityDescription::new(
482 identity.field_name().to_string(),
483 accepted_kind.to_string(),
484 maximum,
485 high_water,
486 )
487}
488
489#[cfg_attr(
490 doc,
491 doc = "EntityConstraintDescription\n\nOne accepted structural constraint entry in a describe payload."
492)]
493#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
494pub struct EntityConstraintDescription {
495 pub(crate) id: u32,
496 pub(crate) name: String,
497 pub(crate) kind: String,
498 pub(crate) origin: String,
499 pub(crate) validation_state: String,
500 pub(crate) validation_progress: Option<ConstraintValidationProgressDescription>,
501 pub(crate) field_id: Option<u32>,
502 pub(crate) index_id: Option<u32>,
503 pub(crate) relation_id: Option<u32>,
504 pub(crate) fields: Vec<String>,
505 pub(crate) index: Option<String>,
506 pub(crate) predicate_sql: Option<String>,
507 pub(crate) relation: Option<String>,
508 pub(crate) target_entity: Option<String>,
509 pub(crate) action: Option<String>,
510 pub(crate) semantics: String,
511 pub(crate) check_sql: Option<String>,
512}
513
514#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
516pub struct ConstraintValidationProgressDescription {
517 phase: String,
518 rows_scanned: u64,
519 findings_seen: u64,
520 restarts: u64,
521}
522
523impl ConstraintValidationProgressDescription {
524 fn from_job(job: &ConstraintValidationJob) -> Self {
525 Self {
526 phase: job.phase().as_str().to_string(),
527 rows_scanned: job.rows_scanned(),
528 findings_seen: job.findings_seen(),
529 restarts: job.restarts(),
530 }
531 }
532
533 #[must_use]
535 pub const fn phase(&self) -> &str {
536 self.phase.as_str()
537 }
538
539 #[must_use]
541 pub const fn rows_scanned(&self) -> u64 {
542 self.rows_scanned
543 }
544
545 #[must_use]
547 pub const fn findings_seen(&self) -> u64 {
548 self.findings_seen
549 }
550
551 #[must_use]
553 pub const fn restarts(&self) -> u64 {
554 self.restarts
555 }
556}
557
558impl EntityConstraintDescription {
559 #[must_use]
561 pub const fn id(&self) -> u32 {
562 self.id
563 }
564
565 #[must_use]
567 pub const fn name(&self) -> &str {
568 self.name.as_str()
569 }
570
571 #[must_use]
573 pub const fn kind(&self) -> &str {
574 self.kind.as_str()
575 }
576
577 #[must_use]
579 pub const fn origin(&self) -> &str {
580 self.origin.as_str()
581 }
582
583 #[must_use]
585 pub const fn validation_state(&self) -> &str {
586 self.validation_state.as_str()
587 }
588
589 #[must_use]
591 pub const fn validation_progress(&self) -> Option<&ConstraintValidationProgressDescription> {
592 self.validation_progress.as_ref()
593 }
594
595 #[must_use]
597 pub const fn field_id(&self) -> Option<u32> {
598 self.field_id
599 }
600
601 #[must_use]
603 pub const fn index_id(&self) -> Option<u32> {
604 self.index_id
605 }
606
607 #[must_use]
609 pub const fn relation_id(&self) -> Option<u32> {
610 self.relation_id
611 }
612
613 #[must_use]
615 pub const fn fields(&self) -> &[String] {
616 self.fields.as_slice()
617 }
618
619 #[must_use]
621 pub fn index(&self) -> Option<&str> {
622 self.index.as_deref()
623 }
624
625 #[must_use]
628 pub fn predicate_sql(&self) -> Option<&str> {
629 self.predicate_sql.as_deref()
630 }
631
632 #[must_use]
634 pub fn relation(&self) -> Option<&str> {
635 self.relation.as_deref()
636 }
637
638 #[must_use]
640 pub fn target_entity(&self) -> Option<&str> {
641 self.target_entity.as_deref()
642 }
643
644 #[must_use]
646 pub fn action(&self) -> Option<&str> {
647 self.action.as_deref()
648 }
649
650 #[must_use]
652 pub const fn semantics(&self) -> &str {
653 self.semantics.as_str()
654 }
655
656 #[must_use]
658 pub fn check_sql(&self) -> Option<&str> {
659 self.check_sql.as_deref()
660 }
661}
662
663#[cfg_attr(
664 doc,
665 doc = "EntityFieldDescription\n\nOne field entry in a describe payload."
666)]
667#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
668pub struct EntityFieldDescription {
669 pub(crate) name: String,
670 pub(crate) slot: u16,
671 pub(crate) kind: String,
672 pub(crate) nullable: bool,
673 pub(crate) primary_key: bool,
674 pub(crate) queryable: bool,
675 pub(crate) origin: String,
676 pub(crate) insert_omission: Option<String>,
677 pub(crate) insert_default: Option<String>,
678 pub(crate) insert_default_bytes: Option<u32>,
679 pub(crate) insert_default_hash: Option<String>,
680 pub(crate) introduced_in_layout: Option<u32>,
681 pub(crate) historical_fill: Option<String>,
682 pub(crate) historical_fill_bytes: Option<u32>,
683 pub(crate) historical_fill_hash: Option<String>,
684}
685
686struct EntityFieldTemporalFacts {
694 insert_omission: Option<String>,
695 insert_default: Option<String>,
696 insert_default_bytes: Option<u32>,
697 insert_default_hash: Option<String>,
698 introduced_in_layout: Option<u32>,
699 historical_fill: Option<String>,
700 historical_fill_bytes: Option<u32>,
701 historical_fill_hash: Option<String>,
702}
703
704impl EntityFieldTemporalFacts {
705 const fn nested() -> Self {
706 Self {
707 insert_omission: None,
708 insert_default: None,
709 insert_default_bytes: None,
710 insert_default_hash: None,
711 introduced_in_layout: None,
712 historical_fill: None,
713 historical_fill_bytes: None,
714 historical_fill_hash: None,
715 }
716 }
717}
718
719impl EntityFieldDescription {
720 #[expect(
722 clippy::too_many_arguments,
723 reason = "schema description construction keeps every temporal field fact explicit"
724 )]
725 #[must_use]
726 pub fn new(
727 name: String,
728 slot: Option<u16>,
729 kind: String,
730 nullable: bool,
731 primary_key: bool,
732 queryable: bool,
733 origin: String,
734 insert_omission: Option<String>,
735 insert_default: Option<String>,
736 insert_default_bytes: Option<u32>,
737 insert_default_hash: Option<String>,
738 introduced_in_layout: Option<u32>,
739 historical_fill: Option<String>,
740 historical_fill_bytes: Option<u32>,
741 historical_fill_hash: Option<String>,
742 ) -> Self {
743 Self::new_with_temporal_facts(
744 name,
745 slot,
746 primary_key,
747 DescribeFieldMetadata::new(kind, nullable, queryable, origin),
748 EntityFieldTemporalFacts {
749 insert_omission,
750 insert_default,
751 insert_default_bytes,
752 insert_default_hash,
753 introduced_in_layout,
754 historical_fill,
755 historical_fill_bytes,
756 historical_fill_hash,
757 },
758 )
759 }
760
761 fn new_with_temporal_facts(
762 name: String,
763 slot: Option<u16>,
764 primary_key: bool,
765 metadata: DescribeFieldMetadata,
766 temporal: EntityFieldTemporalFacts,
767 ) -> Self {
768 let slot = match slot {
769 Some(slot) => slot,
770 None => ENTITY_FIELD_DESCRIPTION_NO_SLOT,
771 };
772
773 Self {
774 name,
775 slot,
776 kind: metadata.kind,
777 nullable: metadata.nullable,
778 primary_key,
779 queryable: metadata.queryable,
780 origin: metadata.origin,
781 insert_omission: temporal.insert_omission,
782 insert_default: temporal.insert_default,
783 insert_default_bytes: temporal.insert_default_bytes,
784 insert_default_hash: temporal.insert_default_hash,
785 introduced_in_layout: temporal.introduced_in_layout,
786 historical_fill: temporal.historical_fill,
787 historical_fill_bytes: temporal.historical_fill_bytes,
788 historical_fill_hash: temporal.historical_fill_hash,
789 }
790 }
791
792 #[must_use]
794 pub const fn name(&self) -> &str {
795 self.name.as_str()
796 }
797
798 #[must_use]
800 pub const fn slot(&self) -> Option<u16> {
801 if self.slot == ENTITY_FIELD_DESCRIPTION_NO_SLOT {
802 None
803 } else {
804 Some(self.slot)
805 }
806 }
807
808 #[must_use]
810 pub const fn kind(&self) -> &str {
811 self.kind.as_str()
812 }
813
814 #[must_use]
816 pub const fn nullable(&self) -> bool {
817 self.nullable
818 }
819
820 #[must_use]
822 pub const fn primary_key(&self) -> bool {
823 self.primary_key
824 }
825
826 #[must_use]
828 pub const fn queryable(&self) -> bool {
829 self.queryable
830 }
831
832 #[must_use]
834 pub const fn origin(&self) -> &str {
835 self.origin.as_str()
836 }
837
838 #[must_use]
840 pub fn insert_omission(&self) -> Option<&str> {
841 self.insert_omission.as_deref()
842 }
843
844 #[must_use]
846 pub fn insert_default(&self) -> Option<&str> {
847 self.insert_default.as_deref()
848 }
849
850 #[must_use]
852 pub const fn insert_default_bytes(&self) -> Option<u32> {
853 self.insert_default_bytes
854 }
855
856 #[must_use]
858 pub fn insert_default_hash(&self) -> Option<&str> {
859 self.insert_default_hash.as_deref()
860 }
861
862 #[must_use]
864 pub const fn introduced_in_layout(&self) -> Option<u32> {
865 self.introduced_in_layout
866 }
867
868 #[must_use]
870 pub fn historical_fill(&self) -> Option<&str> {
871 self.historical_fill.as_deref()
872 }
873
874 #[must_use]
876 pub const fn historical_fill_bytes(&self) -> Option<u32> {
877 self.historical_fill_bytes
878 }
879
880 #[must_use]
882 pub fn historical_fill_hash(&self) -> Option<&str> {
883 self.historical_fill_hash.as_deref()
884 }
885}
886
887#[cfg_attr(
888 doc,
889 doc = "EntityIndexDescription\n\nOne index entry in a describe payload."
890)]
891#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
892pub struct EntityIndexDescription {
893 pub(crate) name: String,
894 pub(crate) unique: bool,
895 pub(crate) fields: Vec<String>,
896 pub(crate) origin: String,
897}
898
899impl EntityIndexDescription {
900 #[must_use]
902 pub const fn new(name: String, unique: bool, fields: Vec<String>, origin: String) -> Self {
903 Self {
904 name,
905 unique,
906 fields,
907 origin,
908 }
909 }
910
911 #[must_use]
913 pub const fn name(&self) -> &str {
914 self.name.as_str()
915 }
916
917 #[must_use]
919 pub const fn unique(&self) -> bool {
920 self.unique
921 }
922
923 #[must_use]
925 pub const fn fields(&self) -> &[String] {
926 self.fields.as_slice()
927 }
928
929 #[must_use]
931 pub const fn origin(&self) -> &str {
932 self.origin.as_str()
933 }
934}
935
936#[cfg_attr(
937 doc,
938 doc = "EntityRelationDescription\n\nOne relation entry in a describe payload."
939)]
940#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
941pub struct EntityRelationDescription {
942 pub(crate) field: String,
943 pub(crate) target_path: String,
944 pub(crate) target_entity_name: String,
945 pub(crate) target_store_path: String,
946 pub(crate) cardinality: EntityRelationCardinality,
947}
948
949impl EntityRelationDescription {
950 #[must_use]
952 pub const fn new(
953 field: String,
954 target_path: String,
955 target_entity_name: String,
956 target_store_path: String,
957 cardinality: EntityRelationCardinality,
958 ) -> Self {
959 Self {
960 field,
961 target_path,
962 target_entity_name,
963 target_store_path,
964 cardinality,
965 }
966 }
967
968 #[must_use]
970 pub const fn field(&self) -> &str {
971 self.field.as_str()
972 }
973
974 #[must_use]
976 pub const fn target_path(&self) -> &str {
977 self.target_path.as_str()
978 }
979
980 #[must_use]
982 pub const fn target_entity_name(&self) -> &str {
983 self.target_entity_name.as_str()
984 }
985
986 #[must_use]
988 pub const fn target_store_path(&self) -> &str {
989 self.target_store_path.as_str()
990 }
991
992 #[must_use]
994 pub const fn cardinality(&self) -> EntityRelationCardinality {
995 self.cardinality
996 }
997}
998
999#[cfg_attr(
1000 doc,
1001 doc = "EntityRelationCardinality\n\nDescribe source multiplicity: List includes list/map-value traversal; Set denotes set-only traversal."
1002)]
1003#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
1004pub enum EntityRelationCardinality {
1005 Single,
1006 List,
1007 Set,
1008}
1009
1010pub(in crate::db) struct AcceptedEntityDescriptionMetadata {
1012 identity: Option<EntityIdentityDescription>,
1013 entity_tag: u64,
1014 accepted_schema_fingerprint_method: u8,
1015 accepted_schema_fingerprint: [u8; 16],
1016}
1017
1018impl AcceptedEntityDescriptionMetadata {
1019 pub(in crate::db) const fn new(
1021 identity: Option<EntityIdentityDescription>,
1022 entity_tag: u64,
1023 accepted_schema_fingerprint_method: u8,
1024 accepted_schema_fingerprint: [u8; 16],
1025 ) -> Self {
1026 Self {
1027 identity,
1028 entity_tag,
1029 accepted_schema_fingerprint_method,
1030 accepted_schema_fingerprint,
1031 }
1032 }
1033}
1034
1035pub(in crate::db) fn describe_accepted_entity_with_persisted_schema(
1037 schema: &AcceptedSchemaSnapshot,
1038 value_catalog: &AcceptedValueCatalogHandle,
1039 validation_jobs: &[ConstraintValidationJob],
1040 metadata: AcceptedEntityDescriptionMetadata,
1041 resolve_relation_target: impl Fn(&str) -> Result<(String, String), InternalError>,
1042) -> Result<EntitySchemaDescription, InternalError> {
1043 describe_entity_with_persisted_schema(
1044 schema,
1045 value_catalog,
1046 validation_jobs,
1047 metadata,
1048 &resolve_relation_target,
1049 )
1050}
1051
1052fn describe_entity_with_persisted_schema(
1053 schema: &AcceptedSchemaSnapshot,
1054 value_catalog: &AcceptedValueCatalogHandle,
1055 validation_jobs: &[ConstraintValidationJob],
1056 metadata: AcceptedEntityDescriptionMetadata,
1057 resolve_relation_target: &impl Fn(&str) -> Result<(String, String), InternalError>,
1058) -> Result<EntitySchemaDescription, InternalError> {
1059 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
1060 let fields = describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)?;
1061 let primary_key_fields = schema.primary_key_field_names();
1062 if primary_key_fields.is_empty() {
1063 return Err(InternalError::store_invariant());
1064 }
1065 let primary_key_fields = primary_key_fields
1066 .into_iter()
1067 .map(str::to_string)
1068 .collect::<Vec<_>>();
1069 let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
1070
1071 Ok(describe_entity_model_from_description_rows(
1072 schema.entity_path(),
1073 schema.entity_name(),
1074 metadata.entity_tag,
1075 metadata.accepted_schema_fingerprint_method,
1076 metadata.accepted_schema_fingerprint,
1077 primary_key.as_str(),
1078 primary_key_fields,
1079 fields,
1080 describe_entity_indexes_with_persisted_schema(schema),
1081 describe_entity_relations_with_persisted_schema(schema, resolve_relation_target)?,
1082 describe_entity_constraints_with_persisted_schema(schema, value_catalog, validation_jobs)?,
1083 row_layout.current_layout_version().get(),
1084 row_layout.history_floor().get(),
1085 )
1086 .with_identity(metadata.identity))
1087}
1088
1089#[expect(
1094 clippy::too_many_arguments,
1095 reason = "one final schema DTO assembly keeps every already-owned section explicit"
1096)]
1097fn describe_entity_model_from_description_rows(
1098 entity_path: &str,
1099 entity_name: &str,
1100 entity_tag: u64,
1101 accepted_schema_fingerprint_method: u8,
1102 accepted_schema_fingerprint: [u8; 16],
1103 primary_key: &str,
1104 primary_key_fields: Vec<String>,
1105 fields: Vec<EntityFieldDescription>,
1106 indexes: Vec<EntityIndexDescription>,
1107 relations: Vec<EntityRelationDescription>,
1108 constraints: Vec<EntityConstraintDescription>,
1109 row_layout_current: u32,
1110 row_layout_history_floor: u32,
1111) -> EntitySchemaDescription {
1112 EntitySchemaDescription::new(
1113 entity_path.to_string(),
1114 entity_name.to_string(),
1115 entity_tag,
1116 accepted_schema_fingerprint_method,
1117 accepted_schema_fingerprint,
1118 primary_key.to_string(),
1119 primary_key_fields,
1120 fields,
1121 indexes,
1122 relations,
1123 constraints,
1124 row_layout_current,
1125 row_layout_history_floor,
1126 )
1127}
1128
1129pub(in crate::db) fn describe_entity_constraints_with_persisted_schema(
1131 schema: &AcceptedSchemaSnapshot,
1132 value_catalog: &AcceptedValueCatalogHandle,
1133 validation_jobs: &[ConstraintValidationJob],
1134) -> Result<Vec<EntityConstraintDescription>, InternalError> {
1135 let snapshot = schema.persisted_snapshot();
1136 let mut descriptions = snapshot
1137 .constraints()
1138 .iter()
1139 .map(|constraint| describe_accepted_constraint(snapshot, value_catalog, constraint))
1140 .collect::<Result<Vec<_>, InternalError>>()?;
1141 descriptions.extend(
1142 snapshot
1143 .constraint_activations()
1144 .iter()
1145 .map(|activation| {
1146 let job = validation_jobs
1147 .iter()
1148 .find(|job| job.constraint_id() == activation.id());
1149 describe_constraint_activation(snapshot, value_catalog, activation, job)
1150 })
1151 .collect::<Result<Vec<_>, InternalError>>()?,
1152 );
1153 if validation_jobs.iter().any(|job| {
1154 !snapshot
1155 .constraint_activations()
1156 .iter()
1157 .any(|activation| activation.id() == job.constraint_id())
1158 }) {
1159 return Err(InternalError::store_invariant());
1160 }
1161 icydb_schema::compact_sort_unstable_by(&mut descriptions, |left, right| {
1162 (left.id(), left.validation_state() != "validated")
1163 .cmp(&(right.id(), right.validation_state() != "validated"))
1164 });
1165 Ok(descriptions)
1166}
1167
1168fn describe_accepted_constraint(
1169 snapshot: &PersistedSchemaSnapshot,
1170 value_catalog: &AcceptedValueCatalogHandle,
1171 constraint: &crate::db::schema::AcceptedConstraintSnapshot,
1172) -> Result<EntityConstraintDescription, InternalError> {
1173 let mut description = accepted_constraint_description(
1174 constraint.id().get(),
1175 constraint.name(),
1176 constraint.origin(),
1177 );
1178 match constraint.kind() {
1179 AcceptedConstraintKind::PrimaryKey => {
1180 description.kind = "primary_key".to_string();
1181 description.fields = snapshot
1182 .primary_key_field_ids()
1183 .iter()
1184 .map(|field_id| accepted_field_name(snapshot, *field_id))
1185 .collect::<Result<Vec<_>, _>>()?;
1186 description.semantics = "primary_key_v1".to_string();
1187 }
1188 AcceptedConstraintKind::NotNull { field_id } => {
1189 apply_not_null_description(&mut description, snapshot, *field_id)?;
1190 }
1191 AcceptedConstraintKind::Unique { index_id } => {
1192 let index = snapshot
1193 .indexes()
1194 .iter()
1195 .find(|index| index.schema_id() == *index_id)
1196 .ok_or_else(InternalError::store_invariant)?;
1197 apply_unique_index_description(&mut description, index, snapshot, value_catalog)?;
1198 }
1199 AcceptedConstraintKind::Relation { relation_id } => {
1200 let relation = snapshot
1201 .relations()
1202 .iter()
1203 .find(|relation| relation.id() == *relation_id)
1204 .ok_or_else(InternalError::store_invariant)?;
1205 apply_relation_description(&mut description, snapshot, relation)?;
1206 }
1207 AcceptedConstraintKind::Check { expression } => {
1208 apply_check_description(&mut description, snapshot, value_catalog, expression)?;
1209 }
1210 AcceptedConstraintKind::TargetedRule { target, operation } => {
1211 apply_targeted_rule_description(&mut description, snapshot, target, operation)?;
1212 }
1213 }
1214 Ok(description)
1215}
1216
1217fn describe_constraint_activation(
1218 snapshot: &PersistedSchemaSnapshot,
1219 value_catalog: &AcceptedValueCatalogHandle,
1220 activation: &ConstraintActivationSnapshot,
1221 validation_job: Option<&ConstraintValidationJob>,
1222) -> Result<EntityConstraintDescription, InternalError> {
1223 let mut description = accepted_constraint_description(
1224 activation.id().get(),
1225 activation.name(),
1226 activation.origin(),
1227 );
1228 match activation.state() {
1229 ConstraintActivationState::EnforcingNewWrites if validation_job.is_none() => {
1230 description.validation_state = "enforcing_new_writes".to_string();
1231 }
1232 ConstraintActivationState::Validating => {
1233 let job = validation_job.ok_or_else(InternalError::store_invariant)?;
1234 job.validate(Some(activation))?;
1235 description.validation_state = "validating".to_string();
1236 description.validation_progress =
1237 Some(ConstraintValidationProgressDescription::from_job(job));
1238 }
1239 ConstraintActivationState::EnforcingNewWrites => {
1240 return Err(InternalError::store_invariant());
1241 }
1242 }
1243 match activation.kind() {
1244 ConstraintActivationKind::NotNull { field_id } => {
1245 apply_not_null_description(&mut description, snapshot, *field_id)?;
1246 }
1247 ConstraintActivationKind::Unique { index_id } => {
1248 let index = snapshot
1249 .candidate_indexes()
1250 .iter()
1251 .find(|index| index.schema_id() == *index_id)
1252 .ok_or_else(InternalError::store_invariant)?;
1253 apply_unique_index_description(&mut description, index, snapshot, value_catalog)?;
1254 }
1255 ConstraintActivationKind::Relation { relation_id } => {
1256 let relation = snapshot
1257 .candidate_relations()
1258 .iter()
1259 .find(|relation| relation.id() == *relation_id)
1260 .ok_or_else(InternalError::store_invariant)?;
1261 apply_relation_description(&mut description, snapshot, relation)?;
1262 }
1263 ConstraintActivationKind::Check { expression } => {
1264 apply_check_description(&mut description, snapshot, value_catalog, expression)?;
1265 }
1266 ConstraintActivationKind::TargetedRule { target, operation } => {
1267 apply_targeted_rule_description(&mut description, snapshot, target, operation)?;
1268 }
1269 }
1270 Ok(description)
1271}
1272
1273fn accepted_constraint_description(
1274 id: u32,
1275 name: &str,
1276 origin: ConstraintOrigin,
1277) -> EntityConstraintDescription {
1278 EntityConstraintDescription {
1279 id,
1280 name: name.to_string(),
1281 kind: String::new(),
1282 origin: accepted_constraint_origin_label(origin).to_string(),
1283 validation_state: "validated".to_string(),
1284 validation_progress: None,
1285 field_id: None,
1286 index_id: None,
1287 relation_id: None,
1288 fields: Vec::new(),
1289 index: None,
1290 predicate_sql: None,
1291 relation: None,
1292 target_entity: None,
1293 action: None,
1294 semantics: String::new(),
1295 check_sql: None,
1296 }
1297}
1298
1299fn apply_not_null_description(
1302 description: &mut EntityConstraintDescription,
1303 snapshot: &PersistedSchemaSnapshot,
1304 field_id: FieldId,
1305) -> Result<(), InternalError> {
1306 description.kind = "not_null".to_string();
1307 description.field_id = Some(field_id.get());
1308 description.fields = vec![accepted_field_name(snapshot, field_id)?];
1309 description.semantics = "not_null_v1".to_string();
1310 Ok(())
1311}
1312
1313fn apply_relation_description(
1314 description: &mut EntityConstraintDescription,
1315 snapshot: &PersistedSchemaSnapshot,
1316 relation: &PersistedRelationEdgeSnapshot,
1317) -> Result<(), InternalError> {
1318 description.kind = "relation".to_string();
1319 description.relation_id = Some(relation.id().get());
1320 description.fields = relation
1321 .source()
1322 .root_field_ids()
1323 .iter()
1324 .map(|field_id| accepted_field_name(snapshot, *field_id))
1325 .collect::<Result<Vec<_>, _>>()?;
1326 description.relation = Some(relation.name().to_string());
1327 description.target_entity = Some(relation.target_path().to_string());
1328 description.action = Some("restrict".to_string());
1329 description.semantics = "relation_pk_restrict_v1".to_string();
1330 Ok(())
1331}
1332
1333fn apply_check_description(
1334 description: &mut EntityConstraintDescription,
1335 snapshot: &PersistedSchemaSnapshot,
1336 value_catalog: &AcceptedValueCatalogHandle,
1337 expression: &AcceptedCheckExprV1,
1338) -> Result<(), InternalError> {
1339 description.kind = "check".to_string();
1340 description.fields = expression
1341 .dependencies()
1342 .into_iter()
1343 .map(|field_id| accepted_field_name(snapshot, field_id))
1344 .collect::<Result<Vec<_>, _>>()?;
1345 description.semantics = "check_expr_v1".to_string();
1346 description.check_sql = Some(render_accepted_check_expr_sql(
1347 expression,
1348 snapshot,
1349 value_catalog,
1350 )?);
1351 Ok(())
1352}
1353
1354fn apply_targeted_rule_description(
1355 description: &mut EntityConstraintDescription,
1356 snapshot: &PersistedSchemaSnapshot,
1357 target: &AcceptedRuleTarget,
1358 operation: &AcceptedRuleOperation,
1359) -> Result<(), InternalError> {
1360 description.kind = "targeted_rule".to_string();
1361 description.field_id = Some(target.root_field_id().get());
1362 description.fields = vec![accepted_field_name(snapshot, target.root_field_id())?];
1363 description.semantics = match operation {
1364 AcceptedRuleOperation::LengthRangeInclusive { .. } => "targeted_length_range_v1",
1365 AcceptedRuleOperation::MultipleOf { .. } => "targeted_multiple_of_v1",
1366 AcceptedRuleOperation::NumericMaximumInclusive { .. } => "targeted_numeric_maximum_v1",
1367 AcceptedRuleOperation::NumericMinimumInclusive { .. } => "targeted_numeric_minimum_v1",
1368 AcceptedRuleOperation::NumericRangeInclusive { .. } => "targeted_numeric_range_v1",
1369 }
1370 .to_string();
1371 Ok(())
1372}
1373
1374fn apply_unique_index_description(
1375 description: &mut EntityConstraintDescription,
1376 index: &PersistedIndexSnapshot,
1377 snapshot: &PersistedSchemaSnapshot,
1378 value_catalog: &AcceptedValueCatalogHandle,
1379) -> Result<(), InternalError> {
1380 description.kind = "unique".to_string();
1381 description.index_id = Some(index.schema_id().get());
1382 description.fields = describe_persisted_index_fields(index.key());
1383 description.index = Some(index.name().to_string());
1384 description.predicate_sql = index
1385 .predicate()
1386 .map(|predicate| predicate.render_sql(snapshot.fields(), value_catalog))
1387 .transpose()?;
1388 description.semantics = if index.predicate().is_some() {
1389 "partial_unique_index_v1"
1390 } else {
1391 "unique_index_v1"
1392 }
1393 .to_string();
1394 Ok(())
1395}
1396
1397const fn accepted_constraint_origin_label(origin: ConstraintOrigin) -> &'static str {
1398 match origin {
1399 ConstraintOrigin::Generated => "generated",
1400 ConstraintOrigin::SqlDdl => "sql_ddl",
1401 }
1402}
1403
1404fn accepted_field_name(
1405 snapshot: &crate::db::schema::PersistedSchemaSnapshot,
1406 field_id: FieldId,
1407) -> Result<String, InternalError> {
1408 snapshot
1409 .fields()
1410 .iter()
1411 .find(|field| field.id() == field_id)
1412 .map(|field| field.name().to_string())
1413 .ok_or_else(InternalError::store_invariant)
1414}
1415
1416fn render_primary_key_fields(fields: &[String]) -> String {
1417 fields.join(", ")
1418}
1419
1420fn describe_entity_indexes_with_persisted_schema(
1421 schema: &AcceptedSchemaSnapshot,
1422) -> Vec<EntityIndexDescription> {
1423 schema
1424 .persisted_snapshot()
1425 .indexes()
1426 .iter()
1427 .map(|index| {
1428 EntityIndexDescription::new(
1429 index.name().to_string(),
1430 index.unique(),
1431 describe_persisted_index_fields(index.key()),
1432 if index.generated() {
1433 "generated".to_string()
1434 } else {
1435 "ddl".to_string()
1436 },
1437 )
1438 })
1439 .collect()
1440}
1441
1442fn describe_persisted_index_fields(key: &PersistedIndexKeySnapshot) -> Vec<String> {
1443 key.items()
1444 .iter()
1445 .map(|item| match item {
1446 PersistedIndexKeyItemSnapshot::FieldPath(path) => path.path().join("."),
1447 PersistedIndexKeyItemSnapshot::Expression(expression) => {
1448 expression.canonical_text().to_string()
1449 }
1450 })
1451 .collect()
1452}
1453
1454pub(in crate::db) fn describe_compact_columns_with_persisted_schema(
1456 schema: &AcceptedSchemaSnapshot,
1457 value_catalog: &AcceptedValueCatalogHandle,
1458) -> Result<Vec<SqlColumnSummary>, InternalError> {
1459 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
1460 let snapshot = schema.persisted_snapshot();
1461 if snapshot.fields().len() != row_layout.fields().len()
1462 || snapshot.fields().len() > icydb_schema::MAX_FRAGMENT_FIELDS
1463 {
1464 return Err(InternalError::store_invariant());
1465 }
1466
1467 let capacity = compact_column_capacity(snapshot.fields())?;
1468 let mut accepted_fields = snapshot
1469 .fields()
1470 .iter()
1471 .zip(row_layout.fields())
1472 .collect::<Vec<_>>();
1473 icydb_schema::compact_sort_unstable_by(&mut accepted_fields, |left, right| {
1474 left.0.id().cmp(&right.0.id())
1475 });
1476 let mut columns = Vec::with_capacity(capacity);
1477 for (field, runtime_field) in accepted_fields {
1478 let matching_identity = field.id() == runtime_field.field_id();
1479 let matching_name = field.name() == runtime_field.name();
1480 if !matching_identity || !matching_name {
1481 return Err(InternalError::store_invariant());
1482 }
1483
1484 let generated = accepted_write_policy_generates(runtime_field);
1485 let relation = snapshot
1486 .relations()
1487 .iter()
1488 .any(|relation| relation.source().uses_root_field(field.id()));
1489 let extra = compact_column_extras(
1490 runtime_field.write_policy().insert_generation()
1491 == Some(FieldInsertGeneration::Identity),
1492 generated,
1493 relation,
1494 );
1495
1496 columns.push(SqlColumnSummary::new(
1497 field.name().to_string(),
1498 summarize_persisted_field_kind(field.kind(), value_catalog)?,
1499 field.nullable(),
1500 compact_column_key(snapshot, field.name()),
1501 compact_column_default(runtime_field, value_catalog)?,
1502 extra,
1503 )?);
1504
1505 let mut nested = field.nested_leaves().iter().collect::<Vec<_>>();
1506 icydb_schema::compact_sort_unstable_by(&mut nested, |left, right| {
1507 left.path().cmp(right.path())
1508 });
1509 for leaf in nested {
1510 let mut canonical_path = Vec::with_capacity(leaf.path().len().saturating_add(1));
1511 canonical_path.push(field.name());
1512 canonical_path.extend(leaf.path().iter().map(String::as_str));
1513 let canonical_name = canonical_path.join(".");
1514 columns.push(SqlColumnSummary::new(
1515 canonical_name.clone(),
1516 summarize_persisted_field_kind(leaf.kind(), value_catalog)?,
1517 nested_path_nullable(field.nullable(), field.nested_leaves(), leaf.path()),
1518 compact_column_key(snapshot, canonical_name.as_str()),
1519 SqlColumnDefault::NotApplicable,
1520 compact_column_extras(false, generated, false),
1521 )?);
1522 }
1523 }
1524
1525 if columns.len() != capacity {
1526 return Err(InternalError::store_invariant());
1527 }
1528 Ok(columns)
1529}
1530
1531fn compact_column_capacity(
1532 fields: &[crate::db::schema::PersistedFieldSnapshot],
1533) -> Result<usize, InternalError> {
1534 compact_column_capacity_from_counts(
1535 fields.len(),
1536 fields.iter().map(|field| field.nested_leaves().len()),
1537 )
1538}
1539
1540fn compact_column_capacity_from_counts(
1541 field_count: usize,
1542 nested_counts: impl IntoIterator<Item = usize>,
1543) -> Result<usize, InternalError> {
1544 if field_count > icydb_schema::MAX_FRAGMENT_FIELDS {
1545 return Err(InternalError::store_invariant());
1546 }
1547 let mut seen_fields = 0usize;
1548 let mut total = field_count;
1549 for nested_count in nested_counts {
1550 seen_fields = seen_fields
1551 .checked_add(1)
1552 .ok_or_else(InternalError::store_invariant)?;
1553 if nested_count > icydb_schema::MAX_FRAGMENT_FIELDS {
1554 return Err(InternalError::store_invariant());
1555 }
1556 total = total
1557 .checked_add(nested_count)
1558 .ok_or_else(InternalError::store_invariant)?;
1559 }
1560 if seen_fields != field_count {
1561 return Err(InternalError::store_invariant());
1562 }
1563 if total > MAX_SQL_COMPACT_COLUMN_ROWS {
1564 return Err(InternalError::store_invariant());
1565 }
1566 Ok(total)
1567}
1568
1569const fn accepted_write_policy_generates(field: &AcceptedRowLayoutRuntimeField<'_>) -> bool {
1570 let policy = field.write_policy();
1571 policy.insert_generation().is_some() || policy.write_management().is_some()
1572}
1573
1574fn compact_column_extras(identity: bool, generated: bool, relation: bool) -> Vec<SqlColumnExtra> {
1575 let mut extra = Vec::with_capacity(MAX_SQL_COLUMN_EXTRA_FLAGS);
1576 if identity {
1577 extra.push(SqlColumnExtra::Identity);
1578 }
1579 if generated {
1580 extra.push(SqlColumnExtra::Generated);
1581 }
1582 if relation {
1583 extra.push(SqlColumnExtra::Relation);
1584 }
1585 extra
1586}
1587
1588fn compact_column_default(
1589 field: &AcceptedRowLayoutRuntimeField<'_>,
1590 value_catalog: &AcceptedValueCatalogHandle,
1591) -> Result<SqlColumnDefault, InternalError> {
1592 if accepted_write_policy_generates(field) {
1593 return Ok(SqlColumnDefault::Auto);
1594 }
1595 match field.insert_omission_policy() {
1596 AcceptedInsertOmissionPolicy::NullIfMissing => Ok(SqlColumnDefault::Null),
1597 AcceptedInsertOmissionPolicy::DefaultIfMissing => {
1598 let payload = field
1599 .insert_default()
1600 .slot_payload()
1601 .ok_or_else(InternalError::store_invariant)?;
1602 let rendered = accepted_payload_facts(field, value_catalog, payload)?;
1603 Ok(SqlColumnDefault::Literal {
1604 text: rendered.value,
1605 })
1606 }
1607 AcceptedInsertOmissionPolicy::Required => Ok(SqlColumnDefault::Required),
1608 }
1609}
1610
1611fn nested_path_nullable(
1612 top_level_nullable: bool,
1613 leaves: &[PersistedNestedLeafSnapshot],
1614 path: &[String],
1615) -> bool {
1616 top_level_nullable
1617 || leaves.iter().any(|candidate| {
1618 candidate.path().len() <= path.len()
1619 && path.starts_with(candidate.path())
1620 && candidate.nullable()
1621 })
1622}
1623
1624fn compact_column_key(snapshot: &PersistedSchemaSnapshot, path: &str) -> SqlColumnKey {
1625 let top_level_field = snapshot.fields().iter().find(|field| field.name() == path);
1626 let primary =
1627 top_level_field.is_some_and(|field| snapshot.primary_key_field_ids().contains(&field.id()));
1628 let memberships = snapshot.indexes().iter().filter_map(|index| {
1629 let key_items = index.key().component_count();
1630 let exact_path_member = index
1631 .key()
1632 .field_paths()
1633 .any(|item| item.path().join(".") == path);
1634 if !exact_path_member {
1635 return None;
1636 }
1637 Some((index.unique() && index.predicate().is_none(), key_items))
1640 });
1641 classify_compact_column_key(primary, memberships)
1642}
1643
1644fn classify_compact_column_key(
1645 primary: bool,
1646 memberships: impl IntoIterator<Item = (bool, usize)>,
1647) -> SqlColumnKey {
1648 if primary {
1649 return SqlColumnKey::Primary;
1650 }
1651 let mut multiple = false;
1652 for (unique, key_items) in memberships {
1653 if unique && key_items == 1 {
1654 return SqlColumnKey::Unique;
1655 }
1656 multiple = true;
1657 }
1658 if multiple {
1659 SqlColumnKey::Multiple
1660 } else {
1661 SqlColumnKey::None
1662 }
1663}
1664
1665#[cfg_attr(
1666 doc,
1667 doc = "Build field descriptors using accepted persisted schema slot metadata."
1668)]
1669#[cfg(any(test, feature = "sql"))]
1670pub(in crate::db) fn describe_entity_fields_with_persisted_schema(
1671 schema: &AcceptedSchemaSnapshot,
1672 value_catalog: &AcceptedValueCatalogHandle,
1673) -> Result<Vec<EntityFieldDescription>, InternalError> {
1674 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
1675 describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)
1676}
1677
1678fn describe_entity_fields_with_runtime_contract(
1679 schema: &AcceptedSchemaSnapshot,
1680 row_layout: &AcceptedRowLayoutRuntimeContract<'_>,
1681 value_catalog: &AcceptedValueCatalogHandle,
1682) -> Result<Vec<EntityFieldDescription>, InternalError> {
1683 let snapshot = schema.persisted_snapshot();
1684 if snapshot.fields().len() != row_layout.fields().len() {
1685 return Err(InternalError::store_invariant());
1686 }
1687 let mut fields = Vec::with_capacity(snapshot.fields().len());
1688
1689 for (field, runtime_field) in snapshot.fields().iter().zip(row_layout.fields()) {
1692 if field.id() != runtime_field.field_id() {
1693 return Err(InternalError::store_invariant());
1694 }
1695 let primary_key = snapshot.primary_key_field_ids().contains(&field.id());
1696 let slot = Some(runtime_field.slot().get());
1697 let metadata = DescribeFieldMetadata::new(
1698 summarize_persisted_field_kind(field.kind(), value_catalog)?,
1699 field.nullable(),
1700 query_field_is_queryable(field.kind(), value_catalog.composite_catalog()),
1701 field_origin_label(field.generated()),
1702 );
1703 let temporal = accepted_field_temporal_facts(runtime_field, value_catalog)?;
1704
1705 push_described_field_row(
1706 &mut fields,
1707 field.name(),
1708 slot,
1709 primary_key,
1710 None,
1711 metadata,
1712 temporal,
1713 );
1714
1715 if !field.nested_leaves().is_empty() {
1716 describe_persisted_nested_leaves(
1717 &mut fields,
1718 field.nested_leaves(),
1719 field_origin_label(field.generated()),
1720 value_catalog,
1721 )?;
1722 }
1723 }
1724
1725 Ok(fields)
1726}
1727
1728struct DescribeFieldMetadata {
1735 kind: String,
1736 nullable: bool,
1737 queryable: bool,
1738 origin: String,
1739}
1740
1741impl DescribeFieldMetadata {
1742 const fn new(kind: String, nullable: bool, queryable: bool, origin: String) -> Self {
1744 Self {
1745 kind,
1746 nullable,
1747 queryable,
1748 origin,
1749 }
1750 }
1751}
1752
1753fn push_described_field_row(
1756 fields: &mut Vec<EntityFieldDescription>,
1757 name: &str,
1758 slot: Option<u16>,
1759 primary_key: bool,
1760 tree_prefix: Option<&'static str>,
1761 metadata: DescribeFieldMetadata,
1762 temporal: EntityFieldTemporalFacts,
1763) {
1764 let display_name = if let Some(prefix) = tree_prefix {
1767 format!("{prefix}{name}")
1768 } else {
1769 name.to_string()
1770 };
1771
1772 fields.push(EntityFieldDescription::new_with_temporal_facts(
1773 display_name,
1774 slot,
1775 primary_key,
1776 metadata,
1777 temporal,
1778 ));
1779}
1780
1781fn describe_persisted_nested_leaves(
1784 fields: &mut Vec<EntityFieldDescription>,
1785 nested_leaves: &[PersistedNestedLeafSnapshot],
1786 origin: String,
1787 value_catalog: &AcceptedValueCatalogHandle,
1788) -> Result<(), InternalError> {
1789 for (index, leaf) in nested_leaves.iter().enumerate() {
1790 let prefix = if index + 1 == nested_leaves.len() {
1791 "└─ "
1792 } else {
1793 "├─ "
1794 };
1795 let name = leaf.path().join(".");
1796 let metadata = DescribeFieldMetadata::new(
1797 summarize_persisted_field_kind(leaf.kind(), value_catalog)?,
1798 leaf.nullable(),
1799 query_field_is_queryable(leaf.kind(), value_catalog.composite_catalog()),
1800 origin.clone(),
1801 );
1802
1803 push_described_field_row(
1804 fields,
1805 &name,
1806 None,
1807 false,
1808 Some(prefix),
1809 metadata,
1810 EntityFieldTemporalFacts::nested(),
1811 );
1812 }
1813
1814 Ok(())
1815}
1816
1817fn field_origin_label(generated: bool) -> String {
1818 if generated {
1819 "generated".to_string()
1820 } else {
1821 "ddl".to_string()
1822 }
1823}
1824
1825pub(in crate::db) fn describe_entity_relations_with_persisted_schema(
1826 schema: &AcceptedSchemaSnapshot,
1827 resolve_target: &impl Fn(&str) -> Result<(String, String), InternalError>,
1828) -> Result<Vec<EntityRelationDescription>, InternalError> {
1829 let snapshot = schema.persisted_snapshot();
1830 if snapshot.relations().len() > icydb_schema::MAX_FRAGMENT_RELATIONS {
1831 return Err(InternalError::store_invariant());
1832 }
1833 snapshot
1834 .relations()
1835 .iter()
1836 .map(|relation| {
1837 let local_fields = relation
1838 .source()
1839 .root_field_ids()
1840 .iter()
1841 .map(|field_id| accepted_field_name(snapshot, *field_id))
1842 .collect::<Result<Vec<_>, _>>()?;
1843 let (target_entity_name, target_store_path) = resolve_target(relation.target_path())?;
1844
1845 Ok(EntityRelationDescription::new(
1846 render_primary_key_fields(local_fields.as_slice()),
1847 relation.target_path().to_string(),
1848 target_entity_name,
1849 target_store_path,
1850 persisted_relation_cardinality(snapshot, relation)?,
1851 ))
1852 })
1853 .collect()
1854}
1855
1856fn persisted_relation_cardinality(
1857 snapshot: &PersistedSchemaSnapshot,
1858 relation: &PersistedRelationEdgeSnapshot,
1859) -> Result<EntityRelationCardinality, InternalError> {
1860 let field_ids = match relation.source() {
1861 PersistedRelationSourceSnapshot::Direct { field_ids } => field_ids,
1862 PersistedRelationSourceSnapshot::Nested { steps, .. } => {
1863 let mut cardinality = EntityRelationCardinality::Single;
1866 for step in steps {
1867 match step {
1868 PersistedRelationPathStepSnapshot::ListItems
1869 | PersistedRelationPathStepSnapshot::MapValues => {
1870 return Ok(EntityRelationCardinality::List);
1871 }
1872 PersistedRelationPathStepSnapshot::SetItems => {
1873 cardinality = EntityRelationCardinality::Set;
1874 }
1875 _ => {}
1876 }
1877 }
1878 return Ok(cardinality);
1879 }
1880 };
1881 let [field_id] = field_ids.as_slice() else {
1882 return Ok(EntityRelationCardinality::Single);
1883 };
1884 let field = snapshot
1885 .fields()
1886 .iter()
1887 .find(|field| field.id() == *field_id)
1888 .ok_or_else(InternalError::store_invariant)?;
1889
1890 Ok(match field.kind() {
1891 AcceptedFieldKind::List(_) => EntityRelationCardinality::List,
1892 AcceptedFieldKind::Set(_) => EntityRelationCardinality::Set,
1893 _ => EntityRelationCardinality::Single,
1894 })
1895}
1896
1897fn write_accepted_composite_shape_summary(
1898 out: &mut String,
1899 shape: &AcceptedCompositeShape,
1900 value_catalog: &AcceptedValueCatalogHandle,
1901) -> Result<(), InternalError> {
1902 match shape {
1903 AcceptedCompositeShape::Record(fields) => {
1904 out.push_str("record{");
1905 for (index, field) in fields.iter().enumerate() {
1906 if index > 0 {
1907 out.push_str(", ");
1908 }
1909 out.push_str(field.name());
1910 out.push(':');
1911 write_accepted_composite_element_summary(out, field.contract(), value_catalog)?;
1912 }
1913 out.push('}');
1914 }
1915 AcceptedCompositeShape::Tuple(elements) => {
1916 out.push_str("tuple<");
1917 for (index, element) in elements.iter().enumerate() {
1918 if index > 0 {
1919 out.push_str(", ");
1920 }
1921 write_accepted_composite_element_summary(out, element, value_catalog)?;
1922 }
1923 out.push('>');
1924 }
1925 AcceptedCompositeShape::Newtype(inner) => {
1926 out.push_str("newtype<");
1927 write_accepted_composite_element_summary(out, inner, value_catalog)?;
1928 out.push('>');
1929 }
1930 }
1931
1932 Ok(())
1933}
1934
1935fn write_accepted_composite_element_summary(
1936 out: &mut String,
1937 element: &AcceptedCompositeElement,
1938 value_catalog: &AcceptedValueCatalogHandle,
1939) -> Result<(), InternalError> {
1940 write_persisted_field_kind_summary(out, element.kind(), value_catalog)?;
1941 write_composite_nullability_summary(out, element.nullable());
1942 Ok(())
1943}
1944
1945fn write_composite_codec_summary(out: &mut String, codec: CompositeCodec) {
1946 match codec {
1947 CompositeCodec::StructuralV1 => out.push_str("structural_v1"),
1948 }
1949}
1950
1951fn write_composite_nullability_summary(out: &mut String, nullable: bool) {
1952 if nullable {
1953 out.push('?');
1954 }
1955}
1956
1957fn write_length_bounded_field_kind_summary(
1961 out: &mut String,
1962 kind_name: &str,
1963 max_len: Option<u32>,
1964) {
1965 out.push_str(kind_name);
1966 if let Some(max_len) = max_len {
1967 out.push_str("(max_len=");
1968 out.push_str(&max_len.to_string());
1969 out.push(')');
1970 } else {
1971 out.push_str("(unbounded)");
1972 }
1973}
1974
1975struct RenderedTemporalPayload {
1983 value: String,
1984 bytes: u32,
1985 hash: String,
1986}
1987
1988fn accepted_field_temporal_facts(
1989 field: &AcceptedRowLayoutRuntimeField<'_>,
1990 value_catalog: &AcceptedValueCatalogHandle,
1991) -> Result<EntityFieldTemporalFacts, InternalError> {
1992 let write_policy = field.write_policy();
1993 let insert_omission = if write_policy.insert_generation().is_some() {
1994 "generated"
1995 } else if write_policy.write_management().is_some() {
1996 "managed"
1997 } else {
1998 match field.insert_omission_policy() {
1999 AcceptedInsertOmissionPolicy::NullIfMissing => "null",
2000 AcceptedInsertOmissionPolicy::DefaultIfMissing => "default",
2001 AcceptedInsertOmissionPolicy::Required => "required",
2002 }
2003 };
2004 let insert_default = field
2005 .insert_default()
2006 .slot_payload()
2007 .map(|payload| accepted_payload_facts(field, value_catalog, payload))
2008 .transpose()?;
2009 let (insert_default, insert_default_bytes, insert_default_hash) = match insert_default {
2010 Some(payload) => (Some(payload.value), Some(payload.bytes), Some(payload.hash)),
2011 None => (None, None, None),
2012 };
2013 let (historical_fill, historical_fill_bytes, historical_fill_hash) =
2014 match field.historical_fill() {
2015 SchemaHistoricalFill::Reject => (Some("reject".to_string()), None, None),
2016 SchemaHistoricalFill::Null => (Some("null".to_string()), None, None),
2017 SchemaHistoricalFill::SlotPayload(payload) => {
2018 let rendered = accepted_payload_facts(field, value_catalog, payload.as_slice())?;
2019 (
2020 Some(rendered.value),
2021 Some(rendered.bytes),
2022 Some(rendered.hash),
2023 )
2024 }
2025 };
2026
2027 Ok(EntityFieldTemporalFacts {
2028 insert_omission: Some(insert_omission.to_string()),
2029 insert_default,
2030 insert_default_bytes,
2031 insert_default_hash,
2032 introduced_in_layout: Some(field.introduced_in_layout().get()),
2033 historical_fill,
2034 historical_fill_bytes,
2035 historical_fill_hash,
2036 })
2037}
2038
2039fn accepted_payload_facts(
2040 field: &AcceptedRowLayoutRuntimeField<'_>,
2041 value_catalog: &AcceptedValueCatalogHandle,
2042 payload: &[u8],
2043) -> Result<RenderedTemporalPayload, InternalError> {
2044 let persistence = AcceptedFieldPersistenceContract::new(value_catalog, field.decode_contract())
2045 .map_err(|_| InternalError::store_invariant())?;
2046 let admitted = decode_admitted_value_from_accepted_field_contract(persistence, payload)?;
2047 let output = output_value_from_runtime(value_catalog.enum_catalog(), admitted.into_value())
2048 .map_err(|_| InternalError::store_invariant())?;
2049 let hash = short_default_payload_fingerprint(payload);
2050 let rendered = bounded_schema_value_rendering(&output, payload, hash.as_str());
2051 let bytes = u32::try_from(payload.len()).map_err(|_| InternalError::store_invariant())?;
2052
2053 Ok(RenderedTemporalPayload {
2054 value: rendered,
2055 bytes,
2056 hash,
2057 })
2058}
2059
2060fn bounded_schema_value_rendering(value: &OutputValue, payload: &[u8], hash: &str) -> String {
2061 let rendered = match value.as_public() {
2062 crate::value::PublicValue::Text(value) => format!("'{}'", value.escape_default()),
2063 _ => render_output_value_text(value),
2064 };
2065 if rendered.len() <= MAX_SCHEMA_VALUE_RENDER_CHARS {
2066 return rendered;
2067 }
2068
2069 format!(
2070 "{}(bytes={}, sha256={})",
2071 output_value_kind_label(value),
2072 payload.len(),
2073 hash,
2074 )
2075}
2076
2077const fn output_value_kind_label(value: &OutputValue) -> &'static str {
2078 match value.as_public() {
2079 crate::value::PublicValue::Account(_) => "account",
2080 crate::value::PublicValue::Blob(_) => "blob",
2081 crate::value::PublicValue::Bool(_) => "bool",
2082 crate::value::PublicValue::Date(_) => "date",
2083 crate::value::PublicValue::Decimal(_) => "decimal",
2084 crate::value::PublicValue::Duration(_) => "duration",
2085 crate::value::PublicValue::Enum(_) => "enum",
2086 crate::value::PublicValue::Float32(_) => "float32",
2087 crate::value::PublicValue::Float64(_) => "float64",
2088 crate::value::PublicValue::Int64(_) => "int64",
2089 crate::value::PublicValue::Int128(_) => "int128",
2090 crate::value::PublicValue::List(_) => "list",
2091 crate::value::PublicValue::Map(_) => "map",
2092 crate::value::PublicValue::Null => "null",
2093 crate::value::PublicValue::Principal(_) => "principal",
2094 crate::value::PublicValue::Subaccount(_) => "subaccount",
2095 crate::value::PublicValue::Text(_) => "text",
2096 crate::value::PublicValue::Timestamp(_) => "timestamp",
2097 crate::value::PublicValue::Nat64(_) => "nat64",
2098 crate::value::PublicValue::Nat128(_) => "nat128",
2099 crate::value::PublicValue::Ulid(_) => "ulid",
2100 crate::value::PublicValue::Unit => "unit",
2101 crate::value::PublicValue::U256(_) => "u256",
2102 }
2103}
2104
2105fn short_default_payload_fingerprint(payload: &[u8]) -> String {
2106 let digest = Sha256::digest(payload);
2107 let mut out = String::with_capacity(16);
2108 for byte in &digest[..8] {
2109 let _ = write!(out, "{byte:02x}");
2110 }
2111 out
2112}
2113
2114#[cfg_attr(
2115 doc,
2116 doc = "Render one stable field-kind label from accepted persisted schema metadata."
2117)]
2118fn summarize_persisted_field_kind(
2119 kind: &AcceptedFieldKind,
2120 value_catalog: &AcceptedValueCatalogHandle,
2121) -> Result<String, InternalError> {
2122 let mut out = String::new();
2123 write_persisted_field_kind_summary(&mut out, kind, value_catalog)?;
2124
2125 Ok(out)
2126}
2127
2128fn write_persisted_field_kind_summary(
2131 out: &mut String,
2132 kind: &AcceptedFieldKind,
2133 value_catalog: &AcceptedValueCatalogHandle,
2134) -> Result<(), InternalError> {
2135 if let Some(name) = describe_kind_name(kind) {
2136 out.push_str(name);
2137 return Ok(());
2138 }
2139
2140 match kind {
2141 AcceptedFieldKind::Blob { max_len } => {
2142 write_length_bounded_field_kind_summary(out, "blob", *max_len);
2143 }
2144 AcceptedFieldKind::Decimal { scale } => {
2145 let _ = write!(out, "decimal(scale={scale})");
2146 }
2147 AcceptedFieldKind::Enum { type_id } => {
2148 let definition = value_catalog
2149 .enum_catalog()
2150 .enum_type(*type_id)
2151 .ok_or_else(InternalError::store_invariant)?;
2152 out.push_str("enum(");
2153 out.push_str(definition.path());
2154 out.push(')');
2155 }
2156 AcceptedFieldKind::Text { max_len } => {
2157 write_length_bounded_field_kind_summary(out, "text", *max_len);
2158 }
2159 AcceptedFieldKind::Relation {
2160 target_entity_name,
2161 key_kind,
2162 ..
2163 } => {
2164 out.push_str("relation(target=");
2165 out.push_str(target_entity_name);
2166 out.push_str(", key=");
2167 write_persisted_field_kind_summary(out, key_kind, value_catalog)?;
2168 out.push(')');
2169 }
2170 AcceptedFieldKind::List(inner) => {
2171 out.push_str("list<");
2172 write_persisted_field_kind_summary(out, inner, value_catalog)?;
2173 out.push('>');
2174 }
2175 AcceptedFieldKind::Set(inner) => {
2176 out.push_str("set<");
2177 write_persisted_field_kind_summary(out, inner, value_catalog)?;
2178 out.push('>');
2179 }
2180 AcceptedFieldKind::Map { key, value } => {
2181 out.push_str("map<");
2182 write_persisted_field_kind_summary(out, key, value_catalog)?;
2183 out.push_str(", ");
2184 write_persisted_field_kind_summary(out, value, value_catalog)?;
2185 out.push('>');
2186 }
2187 AcceptedFieldKind::Composite { type_id } => {
2188 let composite_catalog = value_catalog.composite_catalog();
2189 let definition = composite_catalog
2190 .composite_type(*type_id)
2191 .ok_or_else(InternalError::store_invariant)?;
2192 out.push_str("composite(path=");
2193 out.push_str(definition.path());
2194 out.push_str(", codec=");
2195 write_composite_codec_summary(out, definition.codec());
2196 out.push_str(", shape=");
2197 write_accepted_composite_shape_summary(out, definition.shape(), value_catalog)?;
2198 out.push(')');
2199 }
2200 AcceptedFieldKind::Account
2201 | AcceptedFieldKind::Bool
2202 | AcceptedFieldKind::Date
2203 | AcceptedFieldKind::Duration
2204 | AcceptedFieldKind::Float32
2205 | AcceptedFieldKind::Float64
2206 | AcceptedFieldKind::Int8
2207 | AcceptedFieldKind::Int16
2208 | AcceptedFieldKind::Int32
2209 | AcceptedFieldKind::Int64
2210 | AcceptedFieldKind::Int128
2211 | AcceptedFieldKind::Principal
2212 | AcceptedFieldKind::Subaccount
2213 | AcceptedFieldKind::Timestamp
2214 | AcceptedFieldKind::Nat8
2215 | AcceptedFieldKind::Nat16
2216 | AcceptedFieldKind::Nat32
2217 | AcceptedFieldKind::Nat64
2218 | AcceptedFieldKind::Nat128
2219 | AcceptedFieldKind::Ulid
2220 | AcceptedFieldKind::Unit
2221 | AcceptedFieldKind::U256 => return Err(InternalError::store_invariant()),
2222 }
2223
2224 Ok(())
2225}
2226
2227const fn describe_kind_name(kind: &AcceptedFieldKind) -> Option<&'static str> {
2228 Some(match kind {
2229 AcceptedFieldKind::Account => "account",
2230 AcceptedFieldKind::Bool => "bool",
2231 AcceptedFieldKind::Date => "date",
2232 AcceptedFieldKind::Duration => "duration",
2233 AcceptedFieldKind::Float32 => "float32",
2234 AcceptedFieldKind::Float64 => "float64",
2235 AcceptedFieldKind::Int8 => "int8",
2236 AcceptedFieldKind::Int16 => "int16",
2237 AcceptedFieldKind::Int32 => "int32",
2238 AcceptedFieldKind::Int64 => "int64",
2239 AcceptedFieldKind::Int128 => "int128",
2240 AcceptedFieldKind::Principal => "principal",
2241 AcceptedFieldKind::Subaccount => "subaccount",
2242 AcceptedFieldKind::Timestamp => "timestamp",
2243 AcceptedFieldKind::Nat8 => "nat8",
2244 AcceptedFieldKind::Nat16 => "nat16",
2245 AcceptedFieldKind::Nat32 => "nat32",
2246 AcceptedFieldKind::Nat64 => "nat64",
2247 AcceptedFieldKind::Nat128 => "nat128",
2248 AcceptedFieldKind::Ulid => "ulid",
2249 AcceptedFieldKind::Unit => "unit",
2250 AcceptedFieldKind::U256 => "u256",
2251 AcceptedFieldKind::Blob { .. }
2252 | AcceptedFieldKind::Decimal { .. }
2253 | AcceptedFieldKind::Enum { .. }
2254 | AcceptedFieldKind::Text { .. }
2255 | AcceptedFieldKind::Relation { .. }
2256 | AcceptedFieldKind::List(_)
2257 | AcceptedFieldKind::Set(_)
2258 | AcceptedFieldKind::Map { .. }
2259 | AcceptedFieldKind::Composite { .. } => return None,
2260 })
2261}
2262
2263#[cfg(test)]
2268mod tests {
2269 use std::collections::BTreeMap;
2270
2271 use super::{
2272 EntityFieldDescription, EntityIdentityDescription, EntityRelationCardinality,
2273 EntityRelationDescription, MAX_SCHEMA_VALUE_RENDER_CHARS, SqlColumnDefault, SqlColumnExtra,
2274 SqlColumnKey, SqlColumnSummary, SqlDescribeOutput, SqlShowRelationsOutput,
2275 classify_compact_column_key, compact_column_capacity_from_counts, compact_column_extras,
2276 describe_accepted_constraint, describe_compact_columns_with_persisted_schema,
2277 describe_constraint_activation, describe_entity_constraints_with_persisted_schema,
2278 describe_entity_fields_with_persisted_schema,
2279 describe_entity_relations_with_persisted_schema, nested_path_nullable,
2280 persisted_relation_cardinality,
2281 };
2282 use crate::db::schema::{
2283 AcceptedCheckExprV1, AcceptedCheckValueExprV1, AcceptedCompositeCatalog,
2284 AcceptedConstraintCatalog, AcceptedConstraintKind, AcceptedConstraintSnapshot,
2285 AcceptedFieldKind, AcceptedIndexPredicate, AcceptedNamedTypeIdentity,
2286 AcceptedRuleOperation, AcceptedRuleTarget, AcceptedSchemaFingerprint,
2287 AcceptedSchemaRevision, AcceptedSchemaSnapshot, AcceptedValueCatalogHandle,
2288 CompositeFieldId, CompositeTypeId, ConstraintActivationKind, ConstraintActivationSnapshot,
2289 ConstraintActivationState, ConstraintId, ConstraintOrigin, ConstraintValidationJob,
2290 FieldId, FieldStorageDecode, LeafCodec, MAX_SCHEMA_SNAPSHOT_BYTES, PersistedFieldSnapshot,
2291 PersistedIndexFieldPathSnapshot, PersistedIndexKeySnapshot, PersistedIndexSnapshot,
2292 PersistedNestedLeafSnapshot, PersistedRelationEdgeSnapshot,
2293 PersistedRelationPathStepSnapshot, PersistedSchemaSnapshot, RelationId, ScalarCodec,
2294 SchemaFieldSlot, SchemaIndexId, SchemaInsertDefault, SchemaRowLayout, SchemaVersion,
2295 composite_catalog::{
2296 AcceptedCompositeElement, AcceptedCompositeField, AcceptedCompositeShape,
2297 decode_accepted_composite_catalog, encode_accepted_composite_catalog,
2298 },
2299 decode_persisted_schema_snapshot, empty_accepted_enum_catalog_for_tests,
2300 encode_persisted_schema_snapshot,
2301 };
2302
2303 const REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS: usize = icydb_schema::MAX_FRAGMENT_FIELDS;
2304 const REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES: usize = 95;
2305 const IC_QUERY_REPLY_BYTES: usize = 3 * 1024 * 1024;
2306
2307 fn constraint_description_fixture() -> (PersistedSchemaSnapshot, AcceptedValueCatalogHandle) {
2308 let snapshot = PersistedSchemaSnapshot::new_with_indexes(
2309 SchemaVersion::initial(),
2310 "tests::Account".to_string(),
2311 "Account".to_string(),
2312 FieldId::new(1),
2313 SchemaRowLayout::initial(vec![
2314 (FieldId::new(1), SchemaFieldSlot::new(0)),
2315 (FieldId::new(2), SchemaFieldSlot::new(1)),
2316 ]),
2317 vec![
2318 PersistedFieldSnapshot::new_initial(
2319 FieldId::new(1),
2320 "id".to_string(),
2321 SchemaFieldSlot::new(0),
2322 AcceptedFieldKind::Ulid,
2323 Vec::new(),
2324 false,
2325 SchemaInsertDefault::None,
2326 FieldStorageDecode::ByKind,
2327 LeafCodec::Scalar(ScalarCodec::Ulid),
2328 ),
2329 PersistedFieldSnapshot::new_initial(
2330 FieldId::new(2),
2331 "email".to_string(),
2332 SchemaFieldSlot::new(1),
2333 AcceptedFieldKind::Text { max_len: None },
2334 Vec::new(),
2335 true,
2336 SchemaInsertDefault::None,
2337 FieldStorageDecode::ByKind,
2338 LeafCodec::Scalar(ScalarCodec::Text),
2339 ),
2340 ],
2341 vec![PersistedIndexSnapshot::new(
2342 SchemaIndexId::new(1).expect("test index identity should be non-zero"),
2343 1,
2344 "account_email".to_string(),
2345 "tests::Account::account_email".to_string(),
2346 true,
2347 PersistedIndexKeySnapshot::from_field_paths(vec![
2348 PersistedIndexFieldPathSnapshot::new(
2349 FieldId::new(2),
2350 SchemaFieldSlot::new(1),
2351 vec!["email".to_string()],
2352 AcceptedFieldKind::Text { max_len: None },
2353 true,
2354 ),
2355 ]),
2356 Some(crate::db::schema::AcceptedIndexPredicate::test_non_null(2)),
2357 )],
2358 );
2359 let catalog = AcceptedConstraintCatalog::initial(
2360 snapshot.fields(),
2361 snapshot.indexes(),
2362 snapshot.relations(),
2363 )
2364 .expect("fixture constraints should build");
2365 let snapshot = snapshot.with_constraint_catalog(catalog);
2366 let value_catalog = AcceptedValueCatalogHandle::new_for_tests(
2367 empty_accepted_enum_catalog_for_tests(),
2368 AcceptedCompositeCatalog::empty(),
2369 AcceptedSchemaRevision::INITIAL,
2370 );
2371 (snapshot, value_catalog)
2372 }
2373
2374 #[test]
2375 fn filtered_unique_constraint_description_exposes_partial_backing_contract() {
2376 let (snapshot, value_catalog) = constraint_description_fixture();
2377 let constraint = snapshot
2378 .constraints()
2379 .iter()
2380 .find(|constraint| constraint.name() == "account_email")
2381 .expect("unique constraint should exist");
2382
2383 let description = describe_accepted_constraint(&snapshot, &value_catalog, constraint)
2384 .expect("accepted unique constraint should describe");
2385 assert_eq!(description.index_id(), Some(1));
2386 assert_eq!(description.index(), Some("account_email"));
2387 assert_eq!(description.predicate_sql(), Some("email IS NOT NULL"));
2388 assert_eq!(description.semantics(), "partial_unique_index_v1");
2389 }
2390
2391 #[test]
2392 fn constraint_descriptions_preserve_live_progress_and_reject_unbound_jobs() {
2393 let (snapshot, values) = constraint_description_fixture();
2394 let catalog = snapshot
2395 .constraint_catalog()
2396 .clone()
2397 .with_added_check_activation(
2398 "pending_check".into(),
2399 ConstraintOrigin::SqlDdl,
2400 AcceptedCheckExprV1::True,
2401 AcceptedSchemaFingerprint::new([1; 32]),
2402 7,
2403 )
2404 .unwrap();
2405 let id = catalog.activations()[0].id();
2406 let catalog = catalog.with_validation_started(id).unwrap();
2407 let job = ConstraintValidationJob::start(
2408 crate::types::EntityTag::new(1),
2409 snapshot.entity_path().into(),
2410 &catalog.activations()[0],
2411 None,
2412 )
2413 .unwrap();
2414 let schema = AcceptedSchemaSnapshot::new(snapshot.clone().with_constraint_catalog(catalog));
2415 let descriptions = describe_entity_constraints_with_persisted_schema(
2416 &schema,
2417 &values,
2418 std::slice::from_ref(&job),
2419 )
2420 .unwrap();
2421 assert!(
2422 descriptions
2423 .windows(2)
2424 .all(|pair| pair[0].id() < pair[1].id())
2425 );
2426 let active = descriptions
2427 .iter()
2428 .find(|row| row.id() == id.get())
2429 .unwrap();
2430 assert_eq!(active.validation_state(), "validating");
2431 assert_eq!(active.validation_progress().unwrap().phase(), "forward");
2432 assert_eq!(active.validation_progress().unwrap().rows_scanned(), 0);
2433
2434 let missing_job =
2435 describe_entity_constraints_with_persisted_schema(&schema, &values, &[]).unwrap_err();
2436 assert_eq!(
2437 missing_job.diagnostic_code(),
2438 icydb_diagnostic_code::DiagnosticCode::StoreInvariantViolation,
2439 );
2440 let unbound_job = describe_entity_constraints_with_persisted_schema(
2441 &AcceptedSchemaSnapshot::new(snapshot),
2442 &values,
2443 &[job],
2444 )
2445 .unwrap_err();
2446 assert_eq!(
2447 unbound_job.diagnostic_code(),
2448 icydb_diagnostic_code::DiagnosticCode::StoreInvariantViolation,
2449 );
2450 }
2451
2452 #[test]
2453 fn row_local_constraint_descriptions_preserve_meaning_across_activation() {
2454 let (snapshot, values) = constraint_description_fixture();
2455 let field_id = FieldId::new(2);
2456 let expression = Box::new(AcceptedCheckExprV1::IsNotNull(
2457 AcceptedCheckValueExprV1::Field(field_id),
2458 ));
2459 let target = AcceptedRuleTarget::new(
2460 field_id,
2461 AcceptedNamedTypeIdentity::Composite(CompositeTypeId::new(1).unwrap()),
2462 );
2463 let operation = Box::new(AcceptedRuleOperation::LengthRangeInclusive { min: 1, max: 10 });
2464 for (accepted, activating, semantics) in [
2465 (
2466 AcceptedConstraintKind::NotNull { field_id },
2467 ConstraintActivationKind::NotNull { field_id },
2468 "not_null_v1",
2469 ),
2470 (
2471 AcceptedConstraintKind::Check {
2472 expression: expression.clone(),
2473 },
2474 ConstraintActivationKind::Check { expression },
2475 "check_expr_v1",
2476 ),
2477 (
2478 AcceptedConstraintKind::TargetedRule {
2479 target,
2480 operation: operation.clone(),
2481 },
2482 ConstraintActivationKind::TargetedRule { target, operation },
2483 "targeted_length_range_v1",
2484 ),
2485 ] {
2486 assert_constraint_description_lifecycle(
2487 &snapshot, &values, accepted, activating, semantics,
2488 );
2489 }
2490 }
2491
2492 fn assert_constraint_description_lifecycle(
2493 snapshot: &PersistedSchemaSnapshot,
2494 values: &AcceptedValueCatalogHandle,
2495 accepted: AcceptedConstraintKind,
2496 activating: ConstraintActivationKind,
2497 semantics: &str,
2498 ) {
2499 let id = ConstraintId::new(10).unwrap();
2500 let constraint = AcceptedConstraintSnapshot::new(
2501 id,
2502 "rule".into(),
2503 ConstraintOrigin::Generated,
2504 accepted,
2505 );
2506 let mut expected = describe_accepted_constraint(snapshot, values, &constraint).unwrap();
2507 assert_eq!(expected.semantics(), semantics);
2508 assert_eq!(expected.fields(), &["email"]);
2509 assert_eq!(expected.validation_state(), "validated");
2510 for state in [
2511 ConstraintActivationState::EnforcingNewWrites,
2512 ConstraintActivationState::Validating,
2513 ] {
2514 let activation = ConstraintActivationSnapshot::new(
2515 id,
2516 "rule".into(),
2517 ConstraintOrigin::Generated,
2518 activating.clone(),
2519 state,
2520 AcceptedSchemaFingerprint::new([1; 32]),
2521 1,
2522 );
2523 let job = (state == ConstraintActivationState::Validating).then(|| {
2524 ConstraintValidationJob::start(
2525 crate::types::EntityTag::new(1),
2526 snapshot.entity_path().into(),
2527 &activation,
2528 None,
2529 )
2530 .unwrap()
2531 });
2532 if job.is_some() {
2533 assert!(
2534 describe_constraint_activation(snapshot, values, &activation, None).is_err()
2535 );
2536 }
2537 let description =
2538 describe_constraint_activation(snapshot, values, &activation, job.as_ref())
2539 .unwrap();
2540 assert_eq!(
2541 description.validation_state(),
2542 if job.is_some() {
2543 "validating"
2544 } else {
2545 "enforcing_new_writes"
2546 }
2547 );
2548 assert_eq!(description.validation_progress().is_some(), job.is_some());
2549 expected
2550 .validation_state
2551 .clone_from(&description.validation_state);
2552 expected
2553 .validation_progress
2554 .clone_from(&description.validation_progress);
2555 assert_eq!(description, expected);
2556 }
2557 }
2558
2559 #[test]
2560 fn relation_descriptions_use_the_owner_for_their_lifecycle_state() {
2561 let (snapshot, values) = constraint_description_fixture();
2562 let relation_id = RelationId::new(1).unwrap();
2563 let accepted = PersistedRelationEdgeSnapshot::new_direct(
2564 relation_id,
2565 "accepted_relation".into(),
2566 "AcceptedTarget".into(),
2567 vec![FieldId::new(1)],
2568 );
2569 let candidate = PersistedRelationEdgeSnapshot::new_direct(
2570 relation_id,
2571 "candidate_relation".into(),
2572 "CandidateTarget".into(),
2573 vec![FieldId::new(2)],
2574 );
2575 let snapshot = snapshot
2576 .with_relations(vec![accepted])
2577 .with_constraint_candidates(Vec::new(), vec![candidate]);
2578 let id = ConstraintId::new(10).unwrap();
2579 let constraint = AcceptedConstraintSnapshot::new(
2580 id,
2581 "relation".into(),
2582 ConstraintOrigin::SqlDdl,
2583 AcceptedConstraintKind::Relation { relation_id },
2584 );
2585 let activation = ConstraintActivationSnapshot::new(
2586 id,
2587 "relation".into(),
2588 ConstraintOrigin::SqlDdl,
2589 ConstraintActivationKind::Relation { relation_id },
2590 ConstraintActivationState::EnforcingNewWrites,
2591 AcceptedSchemaFingerprint::new([1; 32]),
2592 1,
2593 );
2594 let accepted = describe_accepted_constraint(&snapshot, &values, &constraint).unwrap();
2595 let candidate =
2596 describe_constraint_activation(&snapshot, &values, &activation, None).unwrap();
2597 assert_eq!(accepted.relation(), Some("accepted_relation"));
2598 assert_eq!(accepted.target_entity(), Some("AcceptedTarget"));
2599 assert_eq!(accepted.fields(), &["id"]);
2600 assert_eq!(candidate.relation(), Some("candidate_relation"));
2601 assert_eq!(candidate.target_entity(), Some("CandidateTarget"));
2602 assert_eq!(candidate.fields(), &["email"]);
2603 assert_eq!(candidate.action(), Some("restrict"));
2604 assert_eq!(candidate.semantics(), "relation_pk_restrict_v1");
2605 }
2606
2607 #[test]
2608 fn identity_description_reports_exact_remaining_capacity_and_exhaustion() {
2609 let available =
2610 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 254)
2611 .expect("in-domain Identity description should build");
2612 assert_eq!(available.minimum(), 1);
2613 assert_eq!(available.maximum(), 255);
2614 assert_eq!(available.high_water(), 254);
2615 assert_eq!(available.remaining(), 1);
2616 assert!(!available.exhausted());
2617
2618 let exhausted =
2619 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 255)
2620 .expect("exact-domain exhaustion should remain describable");
2621 assert_eq!(exhausted.remaining(), 0);
2622 assert!(exhausted.exhausted());
2623
2624 assert!(
2625 EntityIdentityDescription::new("id".to_string(), "nat8".to_string(), 255, 256).is_err(),
2626 "state beyond the accepted domain must not be described",
2627 );
2628 }
2629
2630 #[test]
2631 fn compact_key_contract_distinguishes_single_unique_from_compound_membership() {
2632 assert_eq!(
2633 classify_compact_column_key(true, [(true, 1), (false, 2)]),
2634 SqlColumnKey::Primary
2635 );
2636 assert_eq!(
2637 classify_compact_column_key(false, [(true, 2)]),
2638 SqlColumnKey::Multiple,
2639 "compound unique membership must not imply independent uniqueness",
2640 );
2641 assert_eq!(
2642 classify_compact_column_key(false, [(false, 1), (true, 1)]),
2643 SqlColumnKey::Unique,
2644 "single-field unique membership has precedence over non-unique membership",
2645 );
2646 assert_eq!(
2647 classify_compact_column_key(false, std::iter::empty()),
2648 SqlColumnKey::None
2649 );
2650 }
2651
2652 #[test]
2653 fn descriptive_column_keys_require_unconditional_uniqueness() {
2654 let (snapshot, values) = constraint_description_fixture();
2655 let email = PersistedFieldSnapshot::new_initial(
2658 FieldId::new(2),
2659 "email".into(),
2660 SchemaFieldSlot::new(1),
2661 AcceptedFieldKind::Text { max_len: None },
2662 Vec::new(),
2663 false,
2664 SchemaInsertDefault::None,
2665 FieldStorageDecode::ByKind,
2666 LeafCodec::Scalar(ScalarCodec::Text),
2667 );
2668 let snapshot = snapshot
2669 .clone()
2670 .with_fields(vec![snapshot.fields()[0].clone(), email]);
2671 let partial = snapshot.indexes()[0]
2672 .clone_with_top_level_field_nullability(FieldId::new(2), false)
2673 .clone_with_predicate_for_tests(Some(AcceptedIndexPredicate::bind_test_sql(
2674 "email <> 'excluded'",
2675 snapshot.fields(),
2676 )));
2677 let key = partial.key().clone();
2678 let unconditional = |ordinal| {
2679 PersistedIndexSnapshot::new(
2680 SchemaIndexId::new(2).unwrap(),
2681 ordinal,
2682 "account_all_email".into(),
2683 "tests::Account::account_all_email".into(),
2684 true,
2685 key.clone(),
2686 None,
2687 )
2688 };
2689 for (indexes, expected) in [
2690 (vec![partial.clone()], SqlColumnKey::Multiple),
2691 (vec![unconditional(1)], SqlColumnKey::Unique),
2692 (vec![partial, unconditional(2)], SqlColumnKey::Unique),
2693 (Vec::new(), SqlColumnKey::None),
2694 ] {
2695 let snapshot = snapshot.clone().with_indexes(indexes);
2696 let constraints = AcceptedConstraintCatalog::initial(
2697 snapshot.fields(),
2698 snapshot.indexes(),
2699 snapshot.relations(),
2700 )
2701 .unwrap();
2702 let schema = AcceptedSchemaSnapshot::try_new_with_acceptance(
2703 snapshot.with_constraint_catalog(constraints),
2704 )
2705 .unwrap();
2706 let columns = describe_compact_columns_with_persisted_schema(&schema, &values).unwrap();
2707 assert_eq!(columns[0].key(), SqlColumnKey::Primary);
2708 assert_eq!(columns[1].key(), expected);
2709 }
2710 }
2711
2712 fn nested_description_fixture() -> (AcceptedSchemaSnapshot, AcceptedValueCatalogHandle) {
2715 let (snapshot, _) = constraint_description_fixture();
2716 let profile_id = CompositeTypeId::new(1).unwrap();
2717 let address_id = CompositeTypeId::new(2).unwrap();
2718 let text = AcceptedFieldKind::Text { max_len: None };
2719 let member = |id, name: &str, kind, nullable| {
2720 AcceptedCompositeField::new(
2721 CompositeFieldId::new(id).unwrap(),
2722 name.into(),
2723 AcceptedCompositeElement::new(kind, nullable),
2724 )
2725 };
2726 let enums = empty_accepted_enum_catalog_for_tests();
2727 let profile_shape = AcceptedCompositeShape::Record(vec![
2728 member(
2729 1,
2730 "address",
2731 AcceptedFieldKind::Composite {
2732 type_id: address_id,
2733 },
2734 false,
2735 ),
2736 member(2, "city", text.clone(), true),
2737 ]);
2738 let address_shape =
2739 AcceptedCompositeShape::Record(vec![member(1, "city", text.clone(), false)]);
2740 let composites = AcceptedCompositeCatalog::from_initial_definitions(
2741 BTreeMap::from([
2742 (profile_id, ("tests::Profile".into(), profile_shape)),
2743 (address_id, ("tests::Address".into(), address_shape)),
2744 ]),
2745 &enums,
2746 )
2747 .unwrap();
2748 let values = AcceptedValueCatalogHandle::new_for_tests(
2749 enums,
2750 composites,
2751 AcceptedSchemaRevision::INITIAL,
2752 );
2753 let leaves = vec![
2754 PersistedNestedLeafSnapshot::new(
2755 vec!["address".into(), "city".into()],
2756 text.clone(),
2757 false,
2758 ),
2759 PersistedNestedLeafSnapshot::new(vec!["city".into()], text, true),
2760 ];
2761 let profile = PersistedFieldSnapshot::new_initial(
2762 FieldId::new(2),
2763 "profile".into(),
2764 SchemaFieldSlot::new(1),
2765 AcceptedFieldKind::Composite {
2766 type_id: profile_id,
2767 },
2768 leaves,
2769 false,
2770 SchemaInsertDefault::None,
2771 FieldStorageDecode::CatalogValue,
2772 LeafCodec::Structural,
2773 );
2774 let snapshot = snapshot
2775 .clone()
2776 .with_fields(vec![snapshot.fields()[0].clone(), profile])
2777 .with_indexes(Vec::new());
2778 let constraints = AcceptedConstraintCatalog::initial(
2779 snapshot.fields(),
2780 snapshot.indexes(),
2781 snapshot.relations(),
2782 )
2783 .unwrap();
2784 let schema =
2785 AcceptedSchemaSnapshot::try_new(snapshot.with_constraint_catalog(constraints)).unwrap();
2786
2787 (schema, values)
2788 }
2789
2790 #[test]
2791 fn descriptive_nested_names_retain_the_complete_relative_path() {
2792 let (schema, values) = nested_description_fixture();
2793 let fields = describe_entity_fields_with_persisted_schema(&schema, &values).unwrap();
2794 let columns = describe_compact_columns_with_persisted_schema(&schema, &values).unwrap();
2795 assert_eq!(fields[1].name(), "profile");
2796 for (index, display, canonical) in [
2797 (2, "├─ address.city", "profile.address.city"),
2798 (3, "└─ city", "profile.city"),
2799 ] {
2800 assert_eq!(fields[index].name(), display);
2801 assert_eq!(columns[index].name(), canonical);
2802 assert_eq!(fields[index].slot(), None);
2803 assert_eq!(fields[index].nullable(), columns[index].nullable());
2804 }
2805 assert!(!fields[2].nullable());
2806 assert!(fields[3].nullable());
2807 }
2808
2809 #[test]
2810 fn descriptive_nested_relations_report_collection_multiplicity() {
2811 use PersistedRelationPathStepSnapshot::{ListItems, MapValues, OptionalSome, SetItems};
2812 for (steps, expected) in [
2815 (vec![OptionalSome], EntityRelationCardinality::Single),
2816 (
2817 vec![OptionalSome, ListItems],
2818 EntityRelationCardinality::List,
2819 ),
2820 (vec![SetItems], EntityRelationCardinality::Set),
2821 (vec![MapValues], EntityRelationCardinality::List),
2822 (vec![SetItems, ListItems], EntityRelationCardinality::List),
2823 (vec![ListItems, SetItems], EntityRelationCardinality::List),
2824 (vec![SetItems, SetItems], EntityRelationCardinality::Set),
2825 (vec![SetItems, MapValues], EntityRelationCardinality::List),
2826 ] {
2827 let kind = steps
2828 .iter()
2829 .rev()
2830 .fold(AcceptedFieldKind::Ulid, |kind, step| match step {
2831 ListItems => AcceptedFieldKind::List(Box::new(kind)),
2832 SetItems => AcceptedFieldKind::Set(Box::new(kind)),
2833 MapValues => AcceptedFieldKind::Map {
2834 key: Box::new(AcceptedFieldKind::Nat64),
2835 value: Box::new(kind),
2836 },
2837 OptionalSome => kind,
2838 _ => unreachable!("fixture contains only collection/optional traversal"),
2839 });
2840 let nullable = steps.first() == Some(&OptionalSome);
2841 let relation = PersistedRelationEdgeSnapshot::new_nested(
2842 RelationId::new(1).unwrap(),
2843 "nested_owner".into(),
2844 "tests::Owner".into(),
2845 FieldId::new(2),
2846 steps,
2847 );
2848 let schema = relation_description_fixture(kind, nullable, relation);
2849 let rows = describe_entity_relations_with_persisted_schema(&schema, &|_| {
2850 Ok(("Owner".into(), "tests::OwnerStore".into()))
2851 })
2852 .unwrap();
2853 assert_eq!(rows.len(), 1);
2854 assert_eq!(rows[0].cardinality(), expected);
2855 }
2856 }
2857
2858 fn relation_description_fixture(
2861 kind: AcceptedFieldKind,
2862 nullable: bool,
2863 relation: PersistedRelationEdgeSnapshot,
2864 ) -> AcceptedSchemaSnapshot {
2865 let (snapshot, _) = constraint_description_fixture();
2866 let (storage, codec) = if kind == AcceptedFieldKind::Ulid {
2867 (
2868 FieldStorageDecode::ByKind,
2869 LeafCodec::Scalar(ScalarCodec::Ulid),
2870 )
2871 } else {
2872 (FieldStorageDecode::CatalogValue, LeafCodec::Structural)
2873 };
2874 let source = PersistedFieldSnapshot::new_initial(
2875 FieldId::new(2),
2876 "owner".into(),
2877 SchemaFieldSlot::new(1),
2878 kind,
2879 Vec::new(),
2880 nullable,
2881 SchemaInsertDefault::None,
2882 storage,
2883 codec,
2884 );
2885 let snapshot = snapshot
2886 .clone()
2887 .with_fields(vec![snapshot.fields()[0].clone(), source])
2888 .with_indexes(Vec::new())
2889 .with_relations(vec![relation]);
2890 let constraints = AcceptedConstraintCatalog::initial(
2891 snapshot.fields(),
2892 snapshot.indexes(),
2893 snapshot.relations(),
2894 )
2895 .unwrap();
2896 AcceptedSchemaSnapshot::try_new(snapshot.with_constraint_catalog(constraints)).unwrap()
2897 }
2898
2899 #[test]
2900 fn descriptive_direct_relations_preserve_scalar_tuple_list_and_set_hints() {
2901 for (kind, expected) in [
2902 (AcceptedFieldKind::Ulid, EntityRelationCardinality::Single),
2903 (
2904 AcceptedFieldKind::List(Box::new(AcceptedFieldKind::Ulid)),
2905 EntityRelationCardinality::List,
2906 ),
2907 (
2908 AcceptedFieldKind::Set(Box::new(AcceptedFieldKind::Ulid)),
2909 EntityRelationCardinality::Set,
2910 ),
2911 ] {
2912 let relation = PersistedRelationEdgeSnapshot::new_direct(
2913 RelationId::new(1).unwrap(),
2914 "owner".into(),
2915 "tests::Owner".into(),
2916 vec![FieldId::new(2)],
2917 );
2918 let schema = relation_description_fixture(kind, false, relation);
2919 assert_eq!(
2920 persisted_relation_cardinality(
2921 schema.persisted_snapshot(),
2922 &schema.persisted_snapshot().relations()[0]
2923 )
2924 .unwrap(),
2925 expected
2926 );
2927 }
2928 let (snapshot, _) = constraint_description_fixture();
2929 let tuple = PersistedRelationEdgeSnapshot::new_direct(
2930 RelationId::new(1).unwrap(),
2931 "tuple_owner".into(),
2932 "tests::Owner".into(),
2933 vec![FieldId::new(1), FieldId::new(2)],
2934 );
2935 assert_eq!(
2936 persisted_relation_cardinality(&snapshot, &tuple).unwrap(),
2937 EntityRelationCardinality::Single
2938 );
2939 }
2940
2941 #[test]
2942 fn compact_extra_contract_is_closed_and_deterministically_ordered() {
2943 assert_eq!(
2944 compact_column_extras(true, true, true),
2945 vec![
2946 SqlColumnExtra::Identity,
2947 SqlColumnExtra::Generated,
2948 SqlColumnExtra::Relation,
2949 ]
2950 );
2951 assert_eq!(
2952 compact_column_extras(false, true, false),
2953 vec![SqlColumnExtra::Generated]
2954 );
2955 assert!(compact_column_extras(false, false, false).is_empty());
2956 }
2957
2958 #[test]
2959 fn compact_projection_bounds_accept_maximum_and_reject_max_plus_one() {
2960 assert_eq!(
2961 compact_column_capacity_from_counts(
2962 icydb_schema::MAX_FRAGMENT_FIELDS,
2963 std::iter::repeat_n(
2964 icydb_schema::MAX_FRAGMENT_FIELDS,
2965 icydb_schema::MAX_FRAGMENT_FIELDS,
2966 ),
2967 )
2968 .expect("accepted maximum should remain projectable"),
2969 super::MAX_SQL_COMPACT_COLUMN_ROWS,
2970 );
2971 assert!(
2972 compact_column_capacity_from_counts(
2973 icydb_schema::MAX_FRAGMENT_FIELDS + 1,
2974 std::iter::repeat_n(0, icydb_schema::MAX_FRAGMENT_FIELDS + 1),
2975 )
2976 .is_err()
2977 );
2978 assert!(
2979 compact_column_capacity_from_counts(1, [icydb_schema::MAX_FRAGMENT_FIELDS + 1],)
2980 .is_err()
2981 );
2982
2983 let valid = SqlColumnSummary::new(
2984 "value".to_string(),
2985 "text".to_string(),
2986 false,
2987 SqlColumnKey::None,
2988 SqlColumnDefault::Literal {
2989 text: "x".repeat(MAX_SCHEMA_VALUE_RENDER_CHARS),
2990 },
2991 vec![
2992 SqlColumnExtra::Identity,
2993 SqlColumnExtra::Generated,
2994 SqlColumnExtra::Relation,
2995 ],
2996 );
2997 let valid = valid.expect("the complete admitted compact row should remain valid");
2998 assert!(
2999 SqlColumnSummary::new(
3000 "value".to_string(),
3001 "text".to_string(),
3002 false,
3003 SqlColumnKey::None,
3004 SqlColumnDefault::Literal {
3005 text: "x".repeat(MAX_SCHEMA_VALUE_RENDER_CHARS + 1),
3006 },
3007 Vec::new(),
3008 )
3009 .is_err()
3010 );
3011 assert!(
3012 SqlColumnSummary::new(
3013 "value".to_string(),
3014 "text".to_string(),
3015 false,
3016 SqlColumnKey::None,
3017 SqlColumnDefault::Required,
3018 vec![SqlColumnExtra::Generated; 4],
3019 )
3020 .is_err()
3021 );
3022
3023 let maximum = SqlDescribeOutput::Compact {
3024 entity: "AcceptedMaximum".to_string(),
3025 columns: vec![valid; super::MAX_SQL_COMPACT_COLUMN_ROWS],
3026 };
3027 assert!(matches!(
3028 icydb_schema::wire::encoded_len(&maximum),
3029 Err(icydb_schema::wire::Error::Limit),
3030 ));
3031 assert!(matches!(
3032 icydb_schema::wire::encode_one(&maximum),
3033 Err(icydb_schema::wire::Error::Limit),
3034 ));
3035
3036 let relation = EntityRelationDescription::new(
3037 "owner_id".to_string(),
3038 "entities::Owner".to_string(),
3039 "Owner".to_string(),
3040 "stores::Owner".to_string(),
3041 EntityRelationCardinality::Single,
3042 );
3043 assert!(
3044 SqlShowRelationsOutput::new(
3045 "Entry".to_string(),
3046 vec![relation.clone(); icydb_schema::MAX_FRAGMENT_RELATIONS],
3047 )
3048 .is_ok()
3049 );
3050 assert!(
3051 SqlShowRelationsOutput::new(
3052 "Entry".to_string(),
3053 vec![relation; icydb_schema::MAX_FRAGMENT_RELATIONS + 1],
3054 )
3055 .is_err()
3056 );
3057 }
3058
3059 #[test]
3060 fn reachable_accepted_compact_projection_exceeds_the_public_query_reply_limit() {
3061 let accepted = reachable_compact_reply_schema();
3062 let value_catalog = reachable_compact_reply_value_catalog();
3063 let columns = describe_compact_columns_with_persisted_schema(&accepted, &value_catalog)
3064 .expect("reachable accepted schema should project compact columns");
3065
3066 assert_eq!(
3067 columns.len(),
3068 1 + REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES
3069 * (1 + REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS),
3070 );
3071 let output = SqlDescribeOutput::Compact {
3072 entity: accepted.entity_name().to_string(),
3073 columns,
3074 };
3075 let encoded_len = icydb_schema::wire::encoded_len(&output)
3076 .expect("reachable accepted compact output should admit allocation-free Wire sizing");
3077 assert!(
3078 encoded_len > IC_QUERY_REPLY_BYTES,
3079 "a valid accepted schema must exercise the generated endpoint reply guard",
3080 );
3081 assert!(matches!(
3082 icydb_schema::wire::encode_one(&output),
3083 Err(icydb_schema::wire::Error::Limit),
3084 ));
3085 }
3086
3087 #[test]
3088 fn field_descriptions_preserve_root_and_nested_queryability() {
3089 let accepted = reachable_compact_reply_schema();
3090 let value_catalog = reachable_compact_reply_value_catalog();
3091 let fields = describe_entity_fields_with_persisted_schema(&accepted, &value_catalog)
3092 .expect("accepted root and leaf metadata should describe");
3093
3094 assert!(fields[0].queryable());
3095 let (groups, remainder) =
3096 fields[1..].as_chunks::<{ 1 + REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS }>();
3097 assert!(remainder.is_empty());
3098 assert_eq!(groups.len(), REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES);
3099 for group in groups {
3100 assert!(!group[0].queryable(), "record roots remain non-queryable");
3101 assert!(group[1..].iter().all(EntityFieldDescription::queryable));
3102 }
3103 }
3104
3105 #[test]
3106 fn nested_nullability_includes_nullable_ancestors() {
3107 let leaves = vec![
3108 PersistedNestedLeafSnapshot::new(
3109 vec!["address".to_string()],
3110 AcceptedFieldKind::Unit,
3111 true,
3112 ),
3113 PersistedNestedLeafSnapshot::new(
3114 vec!["address".to_string(), "city".to_string()],
3115 AcceptedFieldKind::Unit,
3116 false,
3117 ),
3118 ];
3119 assert!(nested_path_nullable(
3120 false,
3121 leaves.as_slice(),
3122 &["address".to_string(), "city".to_string()],
3123 ));
3124 assert!(nested_path_nullable(
3125 true,
3126 leaves.as_slice(),
3127 &["other".to_string()],
3128 ));
3129 assert!(!nested_path_nullable(
3130 false,
3131 leaves.as_slice(),
3132 &["other".to_string()],
3133 ));
3134 }
3135
3136 fn compact_reply_leaf_name(index: usize) -> String {
3137 let first = u8::try_from(index / 26).expect("bounded leaf prefix fits u8") + b'a';
3138 let second = u8::try_from(index % 26).expect("bounded leaf suffix fits u8") + b'a';
3139 String::from_utf8(vec![first, second]).expect("ASCII leaf name should be UTF-8")
3140 }
3141
3142 fn compact_reply_top_level_name(index: usize) -> String {
3143 let prefix = format!("field_{index:03}_");
3144 format!("{prefix}{}", "x".repeat(128 - prefix.len()))
3145 }
3146
3147 fn reachable_compact_reply_schema() -> AcceptedSchemaSnapshot {
3148 let composite_type_id = CompositeTypeId::new(1).expect("one is non-zero");
3149 let nested_leaves = (0..REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS)
3150 .map(|index| {
3151 PersistedNestedLeafSnapshot::new(
3152 vec![compact_reply_leaf_name(index)],
3153 AcceptedFieldKind::Unit,
3154 false,
3155 )
3156 })
3157 .collect::<Vec<_>>();
3158 let mut fields = vec![PersistedFieldSnapshot::new_initial(
3159 FieldId::new(1),
3160 "id".to_string(),
3161 SchemaFieldSlot::new(0),
3162 AcceptedFieldKind::Nat64,
3163 Vec::new(),
3164 false,
3165 SchemaInsertDefault::None,
3166 FieldStorageDecode::ByKind,
3167 LeafCodec::Scalar(ScalarCodec::Nat64),
3168 )];
3169 let mut layout = vec![(FieldId::new(1), SchemaFieldSlot::new(0))];
3170 for index in 0..REACHABLE_COMPACT_REPLY_TOP_LEVEL_COMPOSITES {
3171 let raw_id = u32::try_from(index)
3172 .expect("bounded top-level index fits u32")
3173 .checked_add(2)
3174 .expect("bounded top-level identity has a successor");
3175 let raw_slot = u16::try_from(index)
3176 .expect("bounded top-level index fits u16")
3177 .checked_add(1)
3178 .expect("bounded top-level slot has a successor");
3179 let id = FieldId::new(raw_id);
3180 let slot = SchemaFieldSlot::new(raw_slot);
3181 fields.push(PersistedFieldSnapshot::new_initial(
3182 id,
3183 compact_reply_top_level_name(index),
3184 slot,
3185 AcceptedFieldKind::Composite {
3186 type_id: composite_type_id,
3187 },
3188 nested_leaves.clone(),
3189 false,
3190 SchemaInsertDefault::None,
3191 FieldStorageDecode::ByKind,
3192 LeafCodec::Structural,
3193 ));
3194 layout.push((id, slot));
3195 }
3196 let persisted = PersistedSchemaSnapshot::new(
3197 SchemaVersion::initial(),
3198 "tests::ReachableCompactReply".to_string(),
3199 "ReachableCompactReply".to_string(),
3200 FieldId::new(1),
3201 SchemaRowLayout::initial(layout),
3202 fields,
3203 );
3204 let encoded = encode_persisted_schema_snapshot(&persisted)
3205 .expect("reachable compact-reply schema should fit its persisted payload limit");
3206 assert_eq!(encoded.len(), 310_865);
3207 assert!(encoded.len() <= MAX_SCHEMA_SNAPSHOT_BYTES as usize);
3208 AcceptedSchemaSnapshot::try_new(
3209 decode_persisted_schema_snapshot(encoded.as_slice())
3210 .expect("persisted compact-reply schema should decode"),
3211 )
3212 .expect("decoded compact-reply schema should satisfy accepted integrity")
3213 }
3214
3215 fn reachable_compact_reply_value_catalog() -> AcceptedValueCatalogHandle {
3216 let enum_catalog = empty_accepted_enum_catalog_for_tests();
3217 let composite_type_id = CompositeTypeId::new(1).expect("one is non-zero");
3218 let composite_fields = (0..REACHABLE_COMPACT_REPLY_COMPOSITE_FIELDS)
3219 .map(|index| {
3220 let raw_id = u32::try_from(index)
3221 .expect("bounded composite index fits u32")
3222 .checked_add(1)
3223 .expect("bounded composite identity has a successor");
3224 AcceptedCompositeField::new(
3225 CompositeFieldId::new(raw_id).expect("composite field identity is non-zero"),
3226 compact_reply_leaf_name(index),
3227 AcceptedCompositeElement::new(AcceptedFieldKind::Unit, false),
3228 )
3229 })
3230 .collect::<Vec<_>>();
3231 let composite_catalog = AcceptedCompositeCatalog::from_initial_definitions(
3232 BTreeMap::from([(
3233 composite_type_id,
3234 (
3235 "tests::CompactReplyRecord".to_string(),
3236 AcceptedCompositeShape::Record(composite_fields),
3237 ),
3238 )]),
3239 &enum_catalog,
3240 )
3241 .expect("bounded reusable record composite should admit");
3242 let encoded = encode_accepted_composite_catalog(&composite_catalog, &enum_catalog)
3243 .expect("reachable composite authority should fit its persisted payload limit");
3244 assert!(encoded.len() <= MAX_SCHEMA_SNAPSHOT_BYTES as usize);
3245 let composite_catalog = decode_accepted_composite_catalog(&encoded, &enum_catalog)
3246 .expect("persisted composite authority should decode");
3247 AcceptedValueCatalogHandle::new_for_tests(
3248 enum_catalog,
3249 composite_catalog,
3250 AcceptedSchemaRevision::INITIAL,
3251 )
3252 }
3253}