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