1use crate::{
7 db::{
8 data::decode_admitted_value_from_accepted_field_contract,
9 relation::{
10 RelationFieldCardinality, RelationFieldMetadata, relation_field_metadata_for_model_iter,
11 },
12 schema::{
13 AcceptedConstraintKind, AcceptedFieldKind, AcceptedFieldPersistenceContract,
14 AcceptedInsertOmissionPolicy, AcceptedRowLayoutRuntimeContract, AcceptedSchemaSnapshot,
15 AcceptedValueCatalogHandle, ConstraintActivationKind, ConstraintActivationSnapshot,
16 ConstraintActivationState, ConstraintOrigin, ConstraintValidationJob, FieldId,
17 PersistedIndexKeyItemSnapshot, PersistedIndexKeySnapshot, PersistedNestedLeafSnapshot,
18 PersistedSchemaSnapshot, RelationId, SchemaHistoricalFill,
19 composite_catalog::{AcceptedCompositeElement, AcceptedCompositeShape},
20 field_type_from_persisted_kind, not_null_constraint_name, output_value_from_runtime,
21 primary_key_constraint_name, render_accepted_check_expr_sql,
22 runtime::AcceptedRowLayoutRuntimeField,
23 },
24 },
25 error::InternalError,
26 model::{
27 entity::EntityModel,
28 field::{
29 CompositeCodec, CompositeElementModel, CompositeShapeModel, FieldDatabaseDefault,
30 FieldKind, FieldModel,
31 },
32 },
33 value::{OutputValue, render_output_value_text},
34};
35use std::fmt::Write;
36
37use candid::CandidType;
38use serde::Deserialize;
39use sha2::{Digest, Sha256};
40
41const ENTITY_FIELD_DESCRIPTION_NO_SLOT: u16 = u16::MAX;
42const MAX_SCHEMA_VALUE_RENDER_CHARS: usize = 128;
43
44#[cfg_attr(
45 doc,
46 doc = "EntitySchemaDescription\n\nStable describe payload for one entity model."
47)]
48#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
49pub struct EntitySchemaDescription {
50 pub(crate) entity_path: String,
51 pub(crate) entity_name: String,
52 pub(crate) primary_key: String,
53 pub(crate) primary_key_fields: Vec<String>,
54 pub(crate) fields: Vec<EntityFieldDescription>,
55 pub(crate) indexes: Vec<EntityIndexDescription>,
56 pub(crate) relations: Vec<EntityRelationDescription>,
57 pub(crate) constraints: Vec<EntityConstraintDescription>,
58 pub(crate) row_layout_current: u32,
59 pub(crate) row_layout_history_floor: u32,
60}
61
62#[cfg_attr(
63 doc,
64 doc = "EntitySchemaCheckDescription\n\nGenerated-vs-accepted schema description payload for one entity."
65)]
66#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
67pub struct EntitySchemaCheckDescription {
68 pub(crate) generated: EntitySchemaDescription,
69 pub(crate) accepted: EntitySchemaDescription,
70}
71
72impl EntitySchemaCheckDescription {
73 #[must_use]
75 pub const fn new(
76 generated: EntitySchemaDescription,
77 accepted: EntitySchemaDescription,
78 ) -> Self {
79 Self {
80 generated,
81 accepted,
82 }
83 }
84
85 #[must_use]
87 pub const fn generated(&self) -> &EntitySchemaDescription {
88 &self.generated
89 }
90
91 #[must_use]
93 pub const fn accepted(&self) -> &EntitySchemaDescription {
94 &self.accepted
95 }
96}
97
98impl EntitySchemaDescription {
99 #[expect(
101 clippy::too_many_arguments,
102 reason = "schema description construction keeps identity, collections, and layout explicit"
103 )]
104 #[must_use]
105 pub const fn new(
106 entity_path: String,
107 entity_name: String,
108 primary_key: String,
109 primary_key_fields: Vec<String>,
110 fields: Vec<EntityFieldDescription>,
111 indexes: Vec<EntityIndexDescription>,
112 relations: Vec<EntityRelationDescription>,
113 constraints: Vec<EntityConstraintDescription>,
114 row_layout_current: u32,
115 row_layout_history_floor: u32,
116 ) -> Self {
117 Self {
118 entity_path,
119 entity_name,
120 primary_key,
121 primary_key_fields,
122 fields,
123 indexes,
124 relations,
125 constraints,
126 row_layout_current,
127 row_layout_history_floor,
128 }
129 }
130
131 #[must_use]
133 pub const fn entity_path(&self) -> &str {
134 self.entity_path.as_str()
135 }
136
137 #[must_use]
139 pub const fn entity_name(&self) -> &str {
140 self.entity_name.as_str()
141 }
142
143 #[must_use]
145 pub const fn primary_key(&self) -> &str {
146 self.primary_key.as_str()
147 }
148
149 #[must_use]
151 pub const fn primary_key_fields(&self) -> &[String] {
152 self.primary_key_fields.as_slice()
153 }
154
155 #[must_use]
157 pub const fn fields(&self) -> &[EntityFieldDescription] {
158 self.fields.as_slice()
159 }
160
161 #[must_use]
163 pub const fn indexes(&self) -> &[EntityIndexDescription] {
164 self.indexes.as_slice()
165 }
166
167 #[must_use]
169 pub const fn relations(&self) -> &[EntityRelationDescription] {
170 self.relations.as_slice()
171 }
172
173 #[must_use]
175 pub const fn constraints(&self) -> &[EntityConstraintDescription] {
176 self.constraints.as_slice()
177 }
178
179 #[must_use]
181 pub const fn row_layout_current(&self) -> u32 {
182 self.row_layout_current
183 }
184
185 #[must_use]
187 pub const fn row_layout_history_floor(&self) -> u32 {
188 self.row_layout_history_floor
189 }
190}
191
192#[cfg_attr(
193 doc,
194 doc = "EntityConstraintDescription\n\nOne accepted structural constraint entry in a describe payload."
195)]
196#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
197pub struct EntityConstraintDescription {
198 pub(crate) id: u32,
199 pub(crate) name: String,
200 pub(crate) kind: String,
201 pub(crate) origin: String,
202 pub(crate) validation_state: String,
203 pub(crate) validation_progress: Option<ConstraintValidationProgressDescription>,
204 pub(crate) field_id: Option<u32>,
205 pub(crate) index_id: Option<u32>,
206 pub(crate) relation_id: Option<u32>,
207 pub(crate) fields: Vec<String>,
208 pub(crate) index: Option<String>,
209 pub(crate) relation: Option<String>,
210 pub(crate) target_entity: Option<String>,
211 pub(crate) action: Option<String>,
212 pub(crate) semantics: String,
213 pub(crate) check_sql: Option<String>,
214}
215
216#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
218pub struct ConstraintValidationProgressDescription {
219 phase: String,
220 rows_scanned: u64,
221 findings_seen: u64,
222 restarts: u64,
223}
224
225impl ConstraintValidationProgressDescription {
226 fn from_job(job: &ConstraintValidationJob) -> Self {
227 Self {
228 phase: job.phase().as_str().to_string(),
229 rows_scanned: job.rows_scanned(),
230 findings_seen: job.findings_seen(),
231 restarts: job.restarts(),
232 }
233 }
234
235 #[must_use]
237 pub const fn phase(&self) -> &str {
238 self.phase.as_str()
239 }
240
241 #[must_use]
243 pub const fn rows_scanned(&self) -> u64 {
244 self.rows_scanned
245 }
246
247 #[must_use]
249 pub const fn findings_seen(&self) -> u64 {
250 self.findings_seen
251 }
252
253 #[must_use]
255 pub const fn restarts(&self) -> u64 {
256 self.restarts
257 }
258}
259
260impl EntityConstraintDescription {
261 #[must_use]
263 pub const fn id(&self) -> u32 {
264 self.id
265 }
266
267 #[must_use]
269 pub const fn name(&self) -> &str {
270 self.name.as_str()
271 }
272
273 #[must_use]
275 pub const fn kind(&self) -> &str {
276 self.kind.as_str()
277 }
278
279 #[must_use]
281 pub const fn origin(&self) -> &str {
282 self.origin.as_str()
283 }
284
285 #[must_use]
287 pub const fn validation_state(&self) -> &str {
288 self.validation_state.as_str()
289 }
290
291 #[must_use]
293 pub const fn validation_progress(&self) -> Option<&ConstraintValidationProgressDescription> {
294 self.validation_progress.as_ref()
295 }
296
297 #[must_use]
299 pub const fn field_id(&self) -> Option<u32> {
300 self.field_id
301 }
302
303 #[must_use]
305 pub const fn index_id(&self) -> Option<u32> {
306 self.index_id
307 }
308
309 #[must_use]
311 pub const fn relation_id(&self) -> Option<u32> {
312 self.relation_id
313 }
314
315 #[must_use]
317 pub const fn fields(&self) -> &[String] {
318 self.fields.as_slice()
319 }
320
321 #[must_use]
323 pub fn index(&self) -> Option<&str> {
324 self.index.as_deref()
325 }
326
327 #[must_use]
329 pub fn relation(&self) -> Option<&str> {
330 self.relation.as_deref()
331 }
332
333 #[must_use]
335 pub fn target_entity(&self) -> Option<&str> {
336 self.target_entity.as_deref()
337 }
338
339 #[must_use]
341 pub fn action(&self) -> Option<&str> {
342 self.action.as_deref()
343 }
344
345 #[must_use]
347 pub const fn semantics(&self) -> &str {
348 self.semantics.as_str()
349 }
350
351 #[must_use]
353 pub fn check_sql(&self) -> Option<&str> {
354 self.check_sql.as_deref()
355 }
356}
357
358#[cfg_attr(
359 doc,
360 doc = "EntityFieldDescription\n\nOne field entry in a describe payload."
361)]
362#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
363pub struct EntityFieldDescription {
364 pub(crate) name: String,
365 pub(crate) slot: u16,
366 pub(crate) kind: String,
367 pub(crate) nullable: bool,
368 pub(crate) primary_key: bool,
369 pub(crate) queryable: bool,
370 pub(crate) origin: String,
371 pub(crate) insert_omission: Option<String>,
372 pub(crate) insert_default: Option<String>,
373 pub(crate) insert_default_bytes: Option<u32>,
374 pub(crate) insert_default_hash: Option<String>,
375 pub(crate) introduced_in_layout: Option<u32>,
376 pub(crate) historical_fill: Option<String>,
377 pub(crate) historical_fill_bytes: Option<u32>,
378 pub(crate) historical_fill_hash: Option<String>,
379}
380
381struct EntityFieldTemporalFacts {
389 insert_omission: Option<String>,
390 insert_default: Option<String>,
391 insert_default_bytes: Option<u32>,
392 insert_default_hash: Option<String>,
393 introduced_in_layout: Option<u32>,
394 historical_fill: Option<String>,
395 historical_fill_bytes: Option<u32>,
396 historical_fill_hash: Option<String>,
397}
398
399impl EntityFieldTemporalFacts {
400 const fn nested() -> Self {
401 Self {
402 insert_omission: None,
403 insert_default: None,
404 insert_default_bytes: None,
405 insert_default_hash: None,
406 introduced_in_layout: None,
407 historical_fill: None,
408 historical_fill_bytes: None,
409 historical_fill_hash: None,
410 }
411 }
412
413 fn generated(field: &FieldModel) -> Self {
414 let insert_omission = if field.insert_generation().is_some() {
415 "generated"
416 } else if field.write_management().is_some() {
417 "managed"
418 } else {
419 match field.database_default() {
420 FieldDatabaseDefault::EncodedSlotPayload(_)
421 | FieldDatabaseDefault::AuthoredEnumUnit { .. } => "default",
422 FieldDatabaseDefault::None if field.nullable() => "null",
423 FieldDatabaseDefault::None => "required",
424 }
425 };
426 let (insert_default, insert_default_bytes, insert_default_hash) =
427 generated_insert_default_facts(field.database_default());
428
429 Self {
430 insert_omission: Some(insert_omission.to_string()),
431 insert_default,
432 insert_default_bytes,
433 insert_default_hash,
434 introduced_in_layout: Some(1),
435 historical_fill: Some("reject".to_string()),
436 historical_fill_bytes: None,
437 historical_fill_hash: None,
438 }
439 }
440}
441
442impl EntityFieldDescription {
443 #[expect(
445 clippy::too_many_arguments,
446 reason = "schema description construction keeps every temporal field fact explicit"
447 )]
448 #[must_use]
449 pub fn new(
450 name: String,
451 slot: Option<u16>,
452 kind: String,
453 nullable: bool,
454 primary_key: bool,
455 queryable: bool,
456 origin: String,
457 insert_omission: Option<String>,
458 insert_default: Option<String>,
459 insert_default_bytes: Option<u32>,
460 insert_default_hash: Option<String>,
461 introduced_in_layout: Option<u32>,
462 historical_fill: Option<String>,
463 historical_fill_bytes: Option<u32>,
464 historical_fill_hash: Option<String>,
465 ) -> Self {
466 Self::new_with_temporal_facts(
467 name,
468 slot,
469 primary_key,
470 DescribeFieldMetadata::new(kind, nullable, queryable, origin),
471 EntityFieldTemporalFacts {
472 insert_omission,
473 insert_default,
474 insert_default_bytes,
475 insert_default_hash,
476 introduced_in_layout,
477 historical_fill,
478 historical_fill_bytes,
479 historical_fill_hash,
480 },
481 )
482 }
483
484 fn new_with_temporal_facts(
485 name: String,
486 slot: Option<u16>,
487 primary_key: bool,
488 metadata: DescribeFieldMetadata,
489 temporal: EntityFieldTemporalFacts,
490 ) -> Self {
491 let slot = match slot {
492 Some(slot) => slot,
493 None => ENTITY_FIELD_DESCRIPTION_NO_SLOT,
494 };
495
496 Self {
497 name,
498 slot,
499 kind: metadata.kind,
500 nullable: metadata.nullable,
501 primary_key,
502 queryable: metadata.queryable,
503 origin: metadata.origin,
504 insert_omission: temporal.insert_omission,
505 insert_default: temporal.insert_default,
506 insert_default_bytes: temporal.insert_default_bytes,
507 insert_default_hash: temporal.insert_default_hash,
508 introduced_in_layout: temporal.introduced_in_layout,
509 historical_fill: temporal.historical_fill,
510 historical_fill_bytes: temporal.historical_fill_bytes,
511 historical_fill_hash: temporal.historical_fill_hash,
512 }
513 }
514
515 #[must_use]
517 pub const fn name(&self) -> &str {
518 self.name.as_str()
519 }
520
521 #[must_use]
523 pub const fn slot(&self) -> Option<u16> {
524 if self.slot == ENTITY_FIELD_DESCRIPTION_NO_SLOT {
525 None
526 } else {
527 Some(self.slot)
528 }
529 }
530
531 #[must_use]
533 pub const fn kind(&self) -> &str {
534 self.kind.as_str()
535 }
536
537 #[must_use]
539 pub const fn nullable(&self) -> bool {
540 self.nullable
541 }
542
543 #[must_use]
545 pub const fn primary_key(&self) -> bool {
546 self.primary_key
547 }
548
549 #[must_use]
551 pub const fn queryable(&self) -> bool {
552 self.queryable
553 }
554
555 #[must_use]
557 pub const fn origin(&self) -> &str {
558 self.origin.as_str()
559 }
560
561 #[must_use]
563 pub fn insert_omission(&self) -> Option<&str> {
564 self.insert_omission.as_deref()
565 }
566
567 #[must_use]
569 pub fn insert_default(&self) -> Option<&str> {
570 self.insert_default.as_deref()
571 }
572
573 #[must_use]
575 pub const fn insert_default_bytes(&self) -> Option<u32> {
576 self.insert_default_bytes
577 }
578
579 #[must_use]
581 pub fn insert_default_hash(&self) -> Option<&str> {
582 self.insert_default_hash.as_deref()
583 }
584
585 #[must_use]
587 pub const fn introduced_in_layout(&self) -> Option<u32> {
588 self.introduced_in_layout
589 }
590
591 #[must_use]
593 pub fn historical_fill(&self) -> Option<&str> {
594 self.historical_fill.as_deref()
595 }
596
597 #[must_use]
599 pub const fn historical_fill_bytes(&self) -> Option<u32> {
600 self.historical_fill_bytes
601 }
602
603 #[must_use]
605 pub fn historical_fill_hash(&self) -> Option<&str> {
606 self.historical_fill_hash.as_deref()
607 }
608}
609
610#[cfg_attr(
611 doc,
612 doc = "EntityIndexDescription\n\nOne index entry in a describe payload."
613)]
614#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
615pub struct EntityIndexDescription {
616 pub(crate) name: String,
617 pub(crate) unique: bool,
618 pub(crate) fields: Vec<String>,
619 pub(crate) origin: String,
620}
621
622impl EntityIndexDescription {
623 #[must_use]
625 pub const fn new(name: String, unique: bool, fields: Vec<String>, origin: String) -> Self {
626 Self {
627 name,
628 unique,
629 fields,
630 origin,
631 }
632 }
633
634 #[must_use]
636 pub const fn name(&self) -> &str {
637 self.name.as_str()
638 }
639
640 #[must_use]
642 pub const fn unique(&self) -> bool {
643 self.unique
644 }
645
646 #[must_use]
648 pub const fn fields(&self) -> &[String] {
649 self.fields.as_slice()
650 }
651
652 #[must_use]
654 pub const fn origin(&self) -> &str {
655 self.origin.as_str()
656 }
657}
658
659#[cfg_attr(
660 doc,
661 doc = "EntityRelationDescription\n\nOne relation entry in a describe payload."
662)]
663#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
664pub struct EntityRelationDescription {
665 pub(crate) field: String,
666 pub(crate) target_path: String,
667 pub(crate) target_entity_name: String,
668 pub(crate) target_store_path: String,
669 pub(crate) cardinality: EntityRelationCardinality,
670}
671
672impl EntityRelationDescription {
673 #[must_use]
675 pub const fn new(
676 field: String,
677 target_path: String,
678 target_entity_name: String,
679 target_store_path: String,
680 cardinality: EntityRelationCardinality,
681 ) -> Self {
682 Self {
683 field,
684 target_path,
685 target_entity_name,
686 target_store_path,
687 cardinality,
688 }
689 }
690
691 #[must_use]
693 pub const fn field(&self) -> &str {
694 self.field.as_str()
695 }
696
697 #[must_use]
699 pub const fn target_path(&self) -> &str {
700 self.target_path.as_str()
701 }
702
703 #[must_use]
705 pub const fn target_entity_name(&self) -> &str {
706 self.target_entity_name.as_str()
707 }
708
709 #[must_use]
711 pub const fn target_store_path(&self) -> &str {
712 self.target_store_path.as_str()
713 }
714
715 #[must_use]
717 pub const fn cardinality(&self) -> EntityRelationCardinality {
718 self.cardinality
719 }
720}
721
722#[cfg_attr(
723 doc,
724 doc = "EntityRelationCardinality\n\nDescribe relation cardinality."
725)]
726#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
727pub enum EntityRelationCardinality {
728 Single,
729 List,
730 Set,
731}
732
733#[cfg_attr(
734 doc,
735 doc = "Build one stable entity-schema description from one runtime `EntityModel`."
736)]
737#[must_use]
738pub(in crate::db) fn describe_entity_model(model: &EntityModel) -> EntitySchemaDescription {
739 let fields = describe_entity_fields(model);
740 let primary_key_fields = primary_key_field_names_from_model(model);
741 let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
742
743 describe_entity_model_from_description_rows(
744 model.path,
745 model.entity_name,
746 primary_key.as_str(),
747 primary_key_fields,
748 fields,
749 describe_entity_indexes_from_model(model),
750 describe_entity_relations_from_model(model),
751 describe_entity_constraints_from_model(model),
752 1,
753 1,
754 )
755}
756
757#[cfg_attr(
758 doc,
759 doc = "Build one entity-schema description using accepted persisted schema slot metadata."
760)]
761pub(in crate::db) fn describe_entity_model_with_persisted_schema(
762 model: &EntityModel,
763 schema: &AcceptedSchemaSnapshot,
764 value_catalog: &AcceptedValueCatalogHandle,
765 validation_jobs: &[ConstraintValidationJob],
766) -> Result<EntitySchemaDescription, InternalError> {
767 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
768 let fields = describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)?;
769 let primary_key_fields = schema.primary_key_field_names();
770 let primary_key_fields = if primary_key_fields.is_empty() {
771 vec![model.primary_key.name.to_string()]
772 } else {
773 primary_key_fields
774 .into_iter()
775 .map(str::to_string)
776 .collect::<Vec<_>>()
777 };
778 let primary_key = render_primary_key_fields(primary_key_fields.as_slice());
779
780 Ok(describe_entity_model_from_description_rows(
781 schema.entity_path(),
782 schema.entity_name(),
783 primary_key.as_str(),
784 primary_key_fields,
785 fields,
786 describe_entity_indexes_with_persisted_schema(schema),
787 describe_entity_relations_with_persisted_schema(schema),
788 describe_entity_constraints_with_persisted_schema(schema, value_catalog, validation_jobs)?,
789 row_layout.current_layout_version().get(),
790 row_layout.history_floor().get(),
791 ))
792}
793
794#[expect(
799 clippy::too_many_arguments,
800 reason = "one final schema DTO assembly keeps every already-owned section explicit"
801)]
802fn describe_entity_model_from_description_rows(
803 entity_path: &str,
804 entity_name: &str,
805 primary_key: &str,
806 primary_key_fields: Vec<String>,
807 fields: Vec<EntityFieldDescription>,
808 indexes: Vec<EntityIndexDescription>,
809 relations: Vec<EntityRelationDescription>,
810 constraints: Vec<EntityConstraintDescription>,
811 row_layout_current: u32,
812 row_layout_history_floor: u32,
813) -> EntitySchemaDescription {
814 EntitySchemaDescription::new(
815 entity_path.to_string(),
816 entity_name.to_string(),
817 primary_key.to_string(),
818 primary_key_fields,
819 fields,
820 indexes,
821 relations,
822 constraints,
823 row_layout_current,
824 row_layout_history_floor,
825 )
826}
827
828fn describe_entity_constraints_from_model(model: &EntityModel) -> Vec<EntityConstraintDescription> {
829 let mut next_id = 1u32;
830 let mut constraints = vec![model_primary_key_constraint(model, next_id)];
831
832 for (slot, field) in model
833 .fields()
834 .iter()
835 .enumerate()
836 .filter(|(_, field)| !field.nullable())
837 {
838 next_id = next_id
839 .checked_add(1)
840 .expect("generated constraint count should fit current model bounds");
841 let field_id = FieldId::from_initial_slot(slot);
842 constraints.push(model_not_null_constraint(next_id, field_id, field.name()));
843 }
844
845 for index in model.indexes().iter().filter(|index| index.is_unique()) {
846 next_id = next_id
847 .checked_add(1)
848 .expect("generated constraint count should fit current model bounds");
849 constraints.push(model_unique_constraint(
850 next_id,
851 u32::from(index.ordinal()),
852 index.name(),
853 index
854 .fields()
855 .iter()
856 .map(|field| (*field).to_string())
857 .collect(),
858 ));
859 }
860
861 for (position, relation) in model.relations().iter().enumerate() {
862 next_id = next_id
863 .checked_add(1)
864 .expect("generated constraint count should fit current model bounds");
865 let relation_id = u32::try_from(position)
866 .ok()
867 .and_then(|position| position.checked_add(1))
868 .and_then(RelationId::new)
869 .expect("generated relation count should fit current model bounds");
870 constraints.push(model_relation_constraint(
871 next_id,
872 relation_id,
873 relation.name(),
874 relation
875 .local_fields()
876 .iter()
877 .map(|field| field.name().to_string())
878 .collect(),
879 relation.target_path(),
880 ));
881 }
882
883 constraints
884}
885
886fn model_primary_key_constraint(model: &EntityModel, id: u32) -> EntityConstraintDescription {
887 let mut description = accepted_constraint_description(
888 id,
889 primary_key_constraint_name(),
890 ConstraintOrigin::Generated,
891 );
892 description.kind = "primary_key".to_string();
893 description.fields = primary_key_field_names_from_model(model);
894 description.semantics = "primary_key_v1".to_string();
895 description
896}
897
898fn model_not_null_constraint(
899 id: u32,
900 field_id: FieldId,
901 field_name: &str,
902) -> EntityConstraintDescription {
903 let mut description = accepted_constraint_description(
904 id,
905 not_null_constraint_name(field_id).as_str(),
906 ConstraintOrigin::Generated,
907 );
908 description.kind = "not_null".to_string();
909 description.field_id = Some(field_id.get());
910 description.fields = vec![field_name.to_string()];
911 description.semantics = "not_null_v1".to_string();
912 description
913}
914
915fn model_unique_constraint(
916 id: u32,
917 index_id: u32,
918 index_name: &str,
919 fields: Vec<String>,
920) -> EntityConstraintDescription {
921 let mut description =
922 accepted_constraint_description(id, index_name, ConstraintOrigin::Generated);
923 description.kind = "unique".to_string();
924 description.index_id = Some(index_id);
925 description.fields = fields;
926 description.index = Some(index_name.to_string());
927 description.semantics = "unique_index_v1".to_string();
928 description
929}
930
931fn model_relation_constraint(
932 id: u32,
933 relation_id: RelationId,
934 relation_name: &str,
935 fields: Vec<String>,
936 target_path: &str,
937) -> EntityConstraintDescription {
938 let mut description =
939 accepted_constraint_description(id, relation_name, ConstraintOrigin::Generated);
940 description.kind = "relation".to_string();
941 description.relation_id = Some(relation_id.get());
942 description.fields = fields;
943 description.relation = Some(relation_name.to_string());
944 description.target_entity = Some(target_path.to_string());
945 description.action = Some("restrict".to_string());
946 description.semantics = "relation_pk_restrict_v1".to_string();
947 description
948}
949
950fn describe_entity_constraints_with_persisted_schema(
951 schema: &AcceptedSchemaSnapshot,
952 value_catalog: &AcceptedValueCatalogHandle,
953 validation_jobs: &[ConstraintValidationJob],
954) -> Result<Vec<EntityConstraintDescription>, InternalError> {
955 let snapshot = schema.persisted_snapshot();
956 let mut descriptions = snapshot
957 .constraints()
958 .iter()
959 .map(|constraint| describe_accepted_constraint(snapshot, value_catalog, constraint))
960 .collect::<Result<Vec<_>, InternalError>>()?;
961 descriptions.extend(
962 snapshot
963 .constraint_activations()
964 .iter()
965 .map(|activation| {
966 let job = validation_jobs
967 .iter()
968 .find(|job| job.constraint_id() == activation.id());
969 describe_constraint_activation(snapshot, value_catalog, activation, job)
970 })
971 .collect::<Result<Vec<_>, InternalError>>()?,
972 );
973 if validation_jobs.iter().any(|job| {
974 !snapshot
975 .constraint_activations()
976 .iter()
977 .any(|activation| activation.id() == job.constraint_id())
978 }) {
979 return Err(InternalError::store_invariant());
980 }
981 descriptions.sort_by_key(EntityConstraintDescription::id);
982 Ok(descriptions)
983}
984
985fn describe_accepted_constraint(
986 snapshot: &PersistedSchemaSnapshot,
987 value_catalog: &AcceptedValueCatalogHandle,
988 constraint: &crate::db::schema::AcceptedConstraintSnapshot,
989) -> Result<EntityConstraintDescription, InternalError> {
990 let mut description = accepted_constraint_description(
991 constraint.id().get(),
992 constraint.name(),
993 constraint.origin(),
994 );
995 match constraint.kind() {
996 AcceptedConstraintKind::PrimaryKey => {
997 description.kind = "primary_key".to_string();
998 description.fields = snapshot
999 .primary_key_field_ids()
1000 .iter()
1001 .map(|field_id| accepted_field_name(snapshot, *field_id))
1002 .collect::<Result<Vec<_>, _>>()?;
1003 description.semantics = "primary_key_v1".to_string();
1004 }
1005 AcceptedConstraintKind::NotNull { field_id } => {
1006 description.kind = "not_null".to_string();
1007 description.field_id = Some(field_id.get());
1008 description.fields = vec![accepted_field_name(snapshot, *field_id)?];
1009 description.semantics = "not_null_v1".to_string();
1010 }
1011 AcceptedConstraintKind::Unique { index_id } => {
1012 let index = snapshot
1013 .indexes()
1014 .iter()
1015 .find(|index| index.schema_id() == *index_id)
1016 .ok_or_else(InternalError::store_invariant)?;
1017 description.kind = "unique".to_string();
1018 description.index_id = Some(index_id.get());
1019 description.fields = describe_persisted_index_fields(index.key());
1020 description.index = Some(index.name().to_string());
1021 description.semantics = "unique_index_v1".to_string();
1022 }
1023 AcceptedConstraintKind::Relation { relation_id } => {
1024 let relation = snapshot
1025 .relations()
1026 .iter()
1027 .find(|relation| relation.id() == *relation_id)
1028 .ok_or_else(InternalError::store_invariant)?;
1029 description.kind = "relation".to_string();
1030 description.relation_id = Some(relation_id.get());
1031 description.fields = relation
1032 .local_field_ids()
1033 .iter()
1034 .map(|field_id| accepted_field_name(snapshot, *field_id))
1035 .collect::<Result<Vec<_>, _>>()?;
1036 description.relation = Some(relation.name().to_string());
1037 description.target_entity = Some(relation.target_path().to_string());
1038 description.action = Some("restrict".to_string());
1039 description.semantics = "relation_pk_restrict_v1".to_string();
1040 }
1041 AcceptedConstraintKind::Check { expression } => {
1042 description.kind = "check".to_string();
1043 description.fields = expression
1044 .dependencies()
1045 .into_iter()
1046 .map(|field_id| accepted_field_name(snapshot, field_id))
1047 .collect::<Result<Vec<_>, _>>()?;
1048 description.semantics = "check_expr_v1".to_string();
1049 description.check_sql = Some(render_accepted_check_expr_sql(
1050 expression,
1051 snapshot,
1052 value_catalog,
1053 )?);
1054 }
1055 }
1056 Ok(description)
1057}
1058
1059fn describe_constraint_activation(
1060 snapshot: &PersistedSchemaSnapshot,
1061 value_catalog: &AcceptedValueCatalogHandle,
1062 activation: &ConstraintActivationSnapshot,
1063 validation_job: Option<&ConstraintValidationJob>,
1064) -> Result<EntityConstraintDescription, InternalError> {
1065 let mut description = accepted_constraint_description(
1066 activation.id().get(),
1067 activation.name(),
1068 activation.origin(),
1069 );
1070 match activation.state() {
1071 ConstraintActivationState::EnforcingNewWrites if validation_job.is_none() => {
1072 description.validation_state = "enforcing_new_writes".to_string();
1073 }
1074 ConstraintActivationState::Validating => {
1075 let job = validation_job.ok_or_else(InternalError::store_invariant)?;
1076 job.validate(Some(activation))?;
1077 description.validation_state = "validating".to_string();
1078 description.validation_progress =
1079 Some(ConstraintValidationProgressDescription::from_job(job));
1080 }
1081 ConstraintActivationState::EnforcingNewWrites => {
1082 return Err(InternalError::store_invariant());
1083 }
1084 }
1085 match activation.kind() {
1086 ConstraintActivationKind::NotNull { field_id } => {
1087 description.kind = "not_null".to_string();
1088 description.field_id = Some(field_id.get());
1089 description.fields = vec![accepted_field_name(snapshot, *field_id)?];
1090 description.semantics = "not_null_v1".to_string();
1091 }
1092 ConstraintActivationKind::Unique { index_id } => {
1093 let index = snapshot
1094 .candidate_indexes()
1095 .iter()
1096 .find(|index| index.schema_id() == *index_id)
1097 .ok_or_else(InternalError::store_invariant)?;
1098 description.kind = "unique".to_string();
1099 description.index_id = Some(index_id.get());
1100 description.fields = describe_persisted_index_fields(index.key());
1101 description.index = Some(index.name().to_string());
1102 description.semantics = "unique_index_v1".to_string();
1103 }
1104 ConstraintActivationKind::Relation { relation_id } => {
1105 let relation = snapshot
1106 .candidate_relations()
1107 .iter()
1108 .find(|relation| relation.id() == *relation_id)
1109 .ok_or_else(InternalError::store_invariant)?;
1110 description.kind = "relation".to_string();
1111 description.relation_id = Some(relation_id.get());
1112 description.fields = relation
1113 .local_field_ids()
1114 .iter()
1115 .map(|field_id| accepted_field_name(snapshot, *field_id))
1116 .collect::<Result<Vec<_>, _>>()?;
1117 description.relation = Some(relation.name().to_string());
1118 description.target_entity = Some(relation.target_path().to_string());
1119 description.action = Some("restrict".to_string());
1120 description.semantics = "relation_pk_restrict_v1".to_string();
1121 }
1122 ConstraintActivationKind::Check { expression } => {
1123 description.kind = "check".to_string();
1124 description.fields = expression
1125 .dependencies()
1126 .into_iter()
1127 .map(|field_id| accepted_field_name(snapshot, field_id))
1128 .collect::<Result<Vec<_>, _>>()?;
1129 description.semantics = "check_expr_v1".to_string();
1130 description.check_sql = Some(render_accepted_check_expr_sql(
1131 expression,
1132 snapshot,
1133 value_catalog,
1134 )?);
1135 }
1136 }
1137 Ok(description)
1138}
1139
1140fn accepted_constraint_description(
1141 id: u32,
1142 name: &str,
1143 origin: ConstraintOrigin,
1144) -> EntityConstraintDescription {
1145 EntityConstraintDescription {
1146 id,
1147 name: name.to_string(),
1148 kind: String::new(),
1149 origin: accepted_constraint_origin_label(origin).to_string(),
1150 validation_state: "validated".to_string(),
1151 validation_progress: None,
1152 field_id: None,
1153 index_id: None,
1154 relation_id: None,
1155 fields: Vec::new(),
1156 index: None,
1157 relation: None,
1158 target_entity: None,
1159 action: None,
1160 semantics: String::new(),
1161 check_sql: None,
1162 }
1163}
1164
1165const fn accepted_constraint_origin_label(origin: ConstraintOrigin) -> &'static str {
1166 match origin {
1167 ConstraintOrigin::Generated => "generated",
1168 ConstraintOrigin::SqlDdl => "sql_ddl",
1169 }
1170}
1171
1172fn accepted_field_name(
1173 snapshot: &crate::db::schema::PersistedSchemaSnapshot,
1174 field_id: FieldId,
1175) -> Result<String, InternalError> {
1176 snapshot
1177 .fields()
1178 .iter()
1179 .find(|field| field.id() == field_id)
1180 .map(|field| field.name().to_string())
1181 .ok_or_else(InternalError::store_invariant)
1182}
1183
1184fn describe_entity_relations_from_model(model: &EntityModel) -> Vec<EntityRelationDescription> {
1185 relation_field_metadata_for_model_iter(model)
1186 .map(relation_description_from_metadata)
1187 .collect()
1188}
1189
1190fn primary_key_field_names_from_model(model: &EntityModel) -> Vec<String> {
1191 model
1192 .primary_key_model()
1193 .fields()
1194 .iter()
1195 .map(|field| field.name.to_string())
1196 .collect()
1197}
1198
1199fn render_primary_key_fields(fields: &[String]) -> String {
1200 fields.join(", ")
1201}
1202
1203fn describe_entity_indexes_from_model(model: &EntityModel) -> Vec<EntityIndexDescription> {
1204 let mut indexes = Vec::with_capacity(model.indexes.len());
1205 for index in model.indexes {
1206 indexes.push(EntityIndexDescription::new(
1207 index.name().to_string(),
1208 index.is_unique(),
1209 index
1210 .fields()
1211 .iter()
1212 .map(|field| (*field).to_string())
1213 .collect(),
1214 "generated".to_string(),
1215 ));
1216 }
1217
1218 indexes
1219}
1220
1221fn describe_entity_indexes_with_persisted_schema(
1222 schema: &AcceptedSchemaSnapshot,
1223) -> Vec<EntityIndexDescription> {
1224 schema
1225 .persisted_snapshot()
1226 .indexes()
1227 .iter()
1228 .map(|index| {
1229 EntityIndexDescription::new(
1230 index.name().to_string(),
1231 index.unique(),
1232 describe_persisted_index_fields(index.key()),
1233 if index.generated() {
1234 "generated".to_string()
1235 } else {
1236 "ddl".to_string()
1237 },
1238 )
1239 })
1240 .collect()
1241}
1242
1243fn describe_persisted_index_fields(key: &PersistedIndexKeySnapshot) -> Vec<String> {
1244 match key {
1245 PersistedIndexKeySnapshot::FieldPath(paths) => paths
1246 .iter()
1247 .map(|field_path| field_path.path().join("."))
1248 .collect(),
1249 PersistedIndexKeySnapshot::Items(items) => items
1250 .iter()
1251 .map(|item| match item {
1252 PersistedIndexKeyItemSnapshot::FieldPath(field_path) => field_path.path().join("."),
1253 PersistedIndexKeyItemSnapshot::Expression(expression) => {
1254 expression.canonical_text().to_string()
1255 }
1256 })
1257 .collect(),
1258 }
1259}
1260
1261#[must_use]
1265pub(in crate::db) fn describe_entity_fields(model: &EntityModel) -> Vec<EntityFieldDescription> {
1266 describe_entity_fields_with_slot_lookup(model, |slot, _field| {
1267 Some(u16::try_from(slot).expect("generated field slot should fit in u16"))
1268 })
1269}
1270
1271#[cfg_attr(
1272 doc,
1273 doc = "Build field descriptors using accepted persisted schema slot metadata."
1274)]
1275pub(in crate::db) fn describe_entity_fields_with_persisted_schema(
1276 schema: &AcceptedSchemaSnapshot,
1277 value_catalog: &AcceptedValueCatalogHandle,
1278) -> Result<Vec<EntityFieldDescription>, InternalError> {
1279 let row_layout = AcceptedRowLayoutRuntimeContract::from_accepted_schema(schema)?;
1280 describe_entity_fields_with_runtime_contract(schema, &row_layout, value_catalog)
1281}
1282
1283fn describe_entity_fields_with_runtime_contract(
1284 schema: &AcceptedSchemaSnapshot,
1285 row_layout: &AcceptedRowLayoutRuntimeContract<'_>,
1286 value_catalog: &AcceptedValueCatalogHandle,
1287) -> Result<Vec<EntityFieldDescription>, InternalError> {
1288 let snapshot = schema.persisted_snapshot();
1289 if snapshot.fields().len() != row_layout.fields().len() {
1290 return Err(InternalError::store_invariant());
1291 }
1292 let mut fields = Vec::with_capacity(snapshot.fields().len());
1293
1294 for (field, runtime_field) in snapshot.fields().iter().zip(row_layout.fields()) {
1297 if field.id() != runtime_field.field_id() {
1298 return Err(InternalError::store_invariant());
1299 }
1300 let primary_key = snapshot.primary_key_field_ids().contains(&field.id());
1301 let slot = Some(runtime_field.slot().get());
1302 let metadata = DescribeFieldMetadata::new(
1303 summarize_persisted_field_kind(field.kind(), value_catalog)?,
1304 field.nullable(),
1305 field_type_from_persisted_kind(field.kind())
1306 .value_kind()
1307 .is_queryable(),
1308 field_origin_label(field.generated()),
1309 );
1310 let temporal = accepted_field_temporal_facts(runtime_field, value_catalog)?;
1311
1312 push_described_field_row(
1313 &mut fields,
1314 field.name(),
1315 slot,
1316 primary_key,
1317 None,
1318 metadata,
1319 temporal,
1320 );
1321
1322 if !field.nested_leaves().is_empty() {
1323 describe_persisted_nested_leaves(
1324 &mut fields,
1325 field.nested_leaves(),
1326 field_origin_label(field.generated()),
1327 value_catalog,
1328 )?;
1329 }
1330 }
1331
1332 Ok(fields)
1333}
1334
1335fn describe_entity_fields_with_slot_lookup(
1338 model: &EntityModel,
1339 mut slot_for_field: impl FnMut(usize, &FieldModel) -> Option<u16>,
1340) -> Vec<EntityFieldDescription> {
1341 let mut fields = Vec::with_capacity(model.fields.len());
1342 let primary_key_fields = primary_key_field_names_from_model(model);
1343
1344 for (slot, field) in model.fields.iter().enumerate() {
1345 let primary_key = primary_key_fields
1346 .iter()
1347 .any(|primary_key_field| primary_key_field == field.name);
1348 describe_field_recursive(
1349 &mut fields,
1350 field.name,
1351 slot_for_field(slot, field),
1352 field,
1353 primary_key,
1354 None,
1355 None,
1356 );
1357 }
1358
1359 fields
1360}
1361
1362struct DescribeFieldMetadata {
1369 kind: String,
1370 nullable: bool,
1371 queryable: bool,
1372 origin: String,
1373}
1374
1375impl DescribeFieldMetadata {
1376 const fn new(kind: String, nullable: bool, queryable: bool, origin: String) -> Self {
1378 Self {
1379 kind,
1380 nullable,
1381 queryable,
1382 origin,
1383 }
1384 }
1385}
1386
1387fn describe_field_recursive(
1390 fields: &mut Vec<EntityFieldDescription>,
1391 name: &str,
1392 slot: Option<u16>,
1393 field: &FieldModel,
1394 primary_key: bool,
1395 tree_prefix: Option<&'static str>,
1396 metadata_override: Option<DescribeFieldMetadata>,
1397) {
1398 let temporal = if slot.is_some() {
1399 EntityFieldTemporalFacts::generated(field)
1400 } else {
1401 EntityFieldTemporalFacts::nested()
1402 };
1403 let metadata = metadata_override.unwrap_or_else(|| {
1404 DescribeFieldMetadata::new(
1405 summarize_field_kind(&field.kind),
1406 field.nullable(),
1407 field.kind.value_kind().is_queryable(),
1408 "generated".to_string(),
1409 )
1410 });
1411
1412 push_described_field_row(
1413 fields,
1414 name,
1415 slot,
1416 primary_key,
1417 tree_prefix,
1418 metadata,
1419 temporal,
1420 );
1421 describe_generated_nested_fields(fields, field.nested_fields());
1422}
1423
1424fn push_described_field_row(
1427 fields: &mut Vec<EntityFieldDescription>,
1428 name: &str,
1429 slot: Option<u16>,
1430 primary_key: bool,
1431 tree_prefix: Option<&'static str>,
1432 metadata: DescribeFieldMetadata,
1433 temporal: EntityFieldTemporalFacts,
1434) {
1435 let display_name = if let Some(prefix) = tree_prefix {
1438 format!("{prefix}{name}")
1439 } else {
1440 name.to_string()
1441 };
1442
1443 fields.push(EntityFieldDescription::new_with_temporal_facts(
1444 display_name,
1445 slot,
1446 primary_key,
1447 metadata,
1448 temporal,
1449 ));
1450}
1451
1452fn describe_generated_nested_fields(
1455 fields: &mut Vec<EntityFieldDescription>,
1456 nested_fields: &[FieldModel],
1457) {
1458 for (index, nested) in nested_fields.iter().enumerate() {
1459 let prefix = if index + 1 == nested_fields.len() {
1460 "└─ "
1461 } else {
1462 "├─ "
1463 };
1464 describe_field_recursive(
1465 fields,
1466 nested.name(),
1467 None,
1468 nested,
1469 false,
1470 Some(prefix),
1471 None,
1472 );
1473 }
1474}
1475
1476fn describe_persisted_nested_leaves(
1479 fields: &mut Vec<EntityFieldDescription>,
1480 nested_leaves: &[PersistedNestedLeafSnapshot],
1481 origin: String,
1482 value_catalog: &AcceptedValueCatalogHandle,
1483) -> Result<(), InternalError> {
1484 for (index, leaf) in nested_leaves.iter().enumerate() {
1485 let prefix = if index + 1 == nested_leaves.len() {
1486 "└─ "
1487 } else {
1488 "├─ "
1489 };
1490 let name = leaf.path().last().map_or("", String::as_str);
1491 let metadata = DescribeFieldMetadata::new(
1492 summarize_persisted_field_kind(leaf.kind(), value_catalog)?,
1493 leaf.nullable(),
1494 field_type_from_persisted_kind(leaf.kind())
1495 .value_kind()
1496 .is_queryable(),
1497 origin.clone(),
1498 );
1499
1500 push_described_field_row(
1501 fields,
1502 name,
1503 None,
1504 false,
1505 Some(prefix),
1506 metadata,
1507 EntityFieldTemporalFacts::nested(),
1508 );
1509 }
1510
1511 Ok(())
1512}
1513
1514fn field_origin_label(generated: bool) -> String {
1515 if generated {
1516 "generated".to_string()
1517 } else {
1518 "ddl".to_string()
1519 }
1520}
1521
1522fn describe_entity_relations_with_persisted_schema(
1523 schema: &AcceptedSchemaSnapshot,
1524) -> Vec<EntityRelationDescription> {
1525 schema
1526 .persisted_snapshot()
1527 .fields()
1528 .iter()
1529 .filter_map(relation_description_from_persisted_field)
1530 .collect()
1531}
1532
1533fn relation_description_from_persisted_field(
1534 field: &crate::db::schema::PersistedFieldSnapshot,
1535) -> Option<EntityRelationDescription> {
1536 let relation = persisted_relation_description_metadata(field.kind())?;
1537
1538 Some(EntityRelationDescription::new(
1539 field.name().to_string(),
1540 relation.target_path.to_string(),
1541 relation.target_entity_name.to_string(),
1542 relation.target_store_path.to_string(),
1543 relation.cardinality,
1544 ))
1545}
1546
1547struct PersistedRelationDescriptionMetadata<'a> {
1548 target_path: &'a str,
1549 target_entity_name: &'a str,
1550 target_store_path: &'a str,
1551 cardinality: EntityRelationCardinality,
1552}
1553
1554fn persisted_relation_description_metadata(
1555 kind: &AcceptedFieldKind,
1556) -> Option<PersistedRelationDescriptionMetadata<'_>> {
1557 const fn from_relation_kind(
1558 kind: &AcceptedFieldKind,
1559 cardinality: EntityRelationCardinality,
1560 ) -> Option<PersistedRelationDescriptionMetadata<'_>> {
1561 let AcceptedFieldKind::Relation {
1562 target_path,
1563 target_entity_name,
1564 target_store_path,
1565 ..
1566 } = kind
1567 else {
1568 return None;
1569 };
1570
1571 Some(PersistedRelationDescriptionMetadata {
1572 target_path: target_path.as_str(),
1573 target_entity_name: target_entity_name.as_str(),
1574 target_store_path: target_store_path.as_str(),
1575 cardinality,
1576 })
1577 }
1578
1579 match kind {
1580 AcceptedFieldKind::Relation { .. } => {
1581 from_relation_kind(kind, EntityRelationCardinality::Single)
1582 }
1583 AcceptedFieldKind::List(inner) => {
1584 from_relation_kind(inner, EntityRelationCardinality::List)
1585 }
1586 AcceptedFieldKind::Set(inner) => from_relation_kind(inner, EntityRelationCardinality::Set),
1587 _ => None,
1588 }
1589}
1590
1591fn relation_description_from_metadata(
1593 metadata: RelationFieldMetadata,
1594) -> EntityRelationDescription {
1595 let cardinality = match metadata.cardinality() {
1596 RelationFieldCardinality::Single => EntityRelationCardinality::Single,
1597 RelationFieldCardinality::List => EntityRelationCardinality::List,
1598 RelationFieldCardinality::Set => EntityRelationCardinality::Set,
1599 };
1600
1601 EntityRelationDescription::new(
1602 metadata.field_name().to_string(),
1603 metadata.target_path().to_string(),
1604 metadata.target_entity_name().to_string(),
1605 metadata.target_store_path().to_string(),
1606 cardinality,
1607 )
1608}
1609
1610#[cfg_attr(doc, doc = "Render one stable field-kind label for describe output.")]
1611fn summarize_field_kind(kind: &FieldKind) -> String {
1612 let mut out = String::new();
1613 write_field_kind_summary(&mut out, kind);
1614
1615 out
1616}
1617
1618fn write_field_kind_summary(out: &mut String, kind: &FieldKind) {
1621 if let Some(name) = kind.describe_kind_name() {
1622 out.push_str(name);
1623 return;
1624 }
1625
1626 match kind {
1627 FieldKind::Blob { max_len } => {
1628 write_length_bounded_field_kind_summary(out, "blob", *max_len);
1629 }
1630 FieldKind::Decimal { scale } => {
1631 let _ = write!(out, "decimal(scale={scale})");
1632 }
1633 FieldKind::IntBig { max_bytes } => {
1634 write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
1635 }
1636 FieldKind::Enum { path, .. } => {
1637 out.push_str("enum(");
1638 out.push_str(path);
1639 out.push(')');
1640 }
1641 FieldKind::Text { max_len } => {
1642 write_length_bounded_field_kind_summary(out, "text", *max_len);
1643 }
1644 FieldKind::Relation {
1645 target_entity_name,
1646 key_kind,
1647 ..
1648 } => {
1649 out.push_str("relation(target=");
1650 out.push_str(target_entity_name);
1651 out.push_str(", key=");
1652 write_field_kind_summary(out, key_kind);
1653 out.push(')');
1654 }
1655 FieldKind::List(inner) => {
1656 out.push_str("list<");
1657 write_field_kind_summary(out, inner);
1658 out.push('>');
1659 }
1660 FieldKind::Set(inner) => {
1661 out.push_str("set<");
1662 write_field_kind_summary(out, inner);
1663 out.push('>');
1664 }
1665 FieldKind::Map { key, value } => {
1666 out.push_str("map<");
1667 write_field_kind_summary(out, key);
1668 out.push_str(", ");
1669 write_field_kind_summary(out, value);
1670 out.push('>');
1671 }
1672 FieldKind::Composite { path, codec, shape } => {
1673 out.push_str("composite(path=");
1674 out.push_str(path);
1675 out.push_str(", codec=");
1676 write_composite_codec_summary(out, *codec);
1677 out.push_str(", shape=");
1678 write_generated_composite_shape_summary(out, shape);
1679 out.push(')');
1680 }
1681 FieldKind::Account
1682 | FieldKind::Bool
1683 | FieldKind::Date
1684 | FieldKind::Duration
1685 | FieldKind::Float32
1686 | FieldKind::Float64
1687 | FieldKind::Int8
1688 | FieldKind::Int16
1689 | FieldKind::Int32
1690 | FieldKind::Int64
1691 | FieldKind::Int128
1692 | FieldKind::Principal
1693 | FieldKind::Subaccount
1694 | FieldKind::Timestamp
1695 | FieldKind::Nat8
1696 | FieldKind::Nat16
1697 | FieldKind::Nat32
1698 | FieldKind::Nat64
1699 | FieldKind::Nat128
1700 | FieldKind::Ulid
1701 | FieldKind::Unit => unreachable!("schema describe invariant"),
1702 FieldKind::NatBig { max_bytes } => {
1703 write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
1704 }
1705 }
1706}
1707
1708fn write_composite_codec_summary(out: &mut String, codec: CompositeCodec) {
1709 match codec {
1710 CompositeCodec::StructuralV1 => out.push_str("structural_v1"),
1711 }
1712}
1713
1714fn write_generated_composite_shape_summary(out: &mut String, shape: &CompositeShapeModel) {
1715 match shape {
1716 CompositeShapeModel::Record(fields) => {
1717 out.push_str("record{");
1718 for (index, field) in fields.iter().enumerate() {
1719 if index > 0 {
1720 out.push_str(", ");
1721 }
1722 out.push_str(field.name());
1723 out.push(':');
1724 write_field_kind_summary(out, &field.kind());
1725 write_composite_nullability_summary(out, field.nullable());
1726 }
1727 out.push('}');
1728 }
1729 CompositeShapeModel::Tuple(elements) => {
1730 out.push_str("tuple<");
1731 for (index, element) in elements.iter().enumerate() {
1732 if index > 0 {
1733 out.push_str(", ");
1734 }
1735 write_generated_composite_element_summary(out, element);
1736 }
1737 out.push('>');
1738 }
1739 CompositeShapeModel::Newtype(inner) => {
1740 out.push_str("newtype<");
1741 write_generated_composite_element_summary(out, inner);
1742 out.push('>');
1743 }
1744 }
1745}
1746
1747fn write_generated_composite_element_summary(out: &mut String, element: &CompositeElementModel) {
1748 write_field_kind_summary(out, &element.kind());
1749 write_composite_nullability_summary(out, element.nullable());
1750}
1751
1752fn write_accepted_composite_shape_summary(
1753 out: &mut String,
1754 shape: &AcceptedCompositeShape,
1755 value_catalog: &AcceptedValueCatalogHandle,
1756) -> Result<(), InternalError> {
1757 match shape {
1758 AcceptedCompositeShape::Record(fields) => {
1759 out.push_str("record{");
1760 for (index, field) in fields.iter().enumerate() {
1761 if index > 0 {
1762 out.push_str(", ");
1763 }
1764 out.push_str(field.name());
1765 out.push(':');
1766 write_accepted_composite_element_summary(out, field.contract(), value_catalog)?;
1767 }
1768 out.push('}');
1769 }
1770 AcceptedCompositeShape::Tuple(elements) => {
1771 out.push_str("tuple<");
1772 for (index, element) in elements.iter().enumerate() {
1773 if index > 0 {
1774 out.push_str(", ");
1775 }
1776 write_accepted_composite_element_summary(out, element, value_catalog)?;
1777 }
1778 out.push('>');
1779 }
1780 AcceptedCompositeShape::Newtype(inner) => {
1781 out.push_str("newtype<");
1782 write_accepted_composite_element_summary(out, inner, value_catalog)?;
1783 out.push('>');
1784 }
1785 }
1786
1787 Ok(())
1788}
1789
1790fn write_accepted_composite_element_summary(
1791 out: &mut String,
1792 element: &AcceptedCompositeElement,
1793 value_catalog: &AcceptedValueCatalogHandle,
1794) -> Result<(), InternalError> {
1795 write_persisted_field_kind_summary(out, element.kind(), value_catalog)?;
1796 write_composite_nullability_summary(out, element.nullable());
1797 Ok(())
1798}
1799
1800fn write_composite_nullability_summary(out: &mut String, nullable: bool) {
1801 if nullable {
1802 out.push('?');
1803 }
1804}
1805
1806trait DescribeKindName {
1807 fn describe_kind_name(&self) -> Option<&'static str>;
1808}
1809
1810impl DescribeKindName for FieldKind {
1811 fn describe_kind_name(&self) -> Option<&'static str> {
1812 Some(match self {
1813 Self::Account => "account",
1814 Self::Bool => "bool",
1815 Self::Date => "date",
1816 Self::Duration => "duration",
1817 Self::Float32 => "float32",
1818 Self::Float64 => "float64",
1819 Self::Int8 => "int8",
1820 Self::Int16 => "int16",
1821 Self::Int32 => "int32",
1822 Self::Int64 => "int64",
1823 Self::Int128 => "int128",
1824 Self::Principal => "principal",
1825 Self::Subaccount => "subaccount",
1826 Self::Timestamp => "timestamp",
1827 Self::Nat8 => "nat8",
1828 Self::Nat16 => "nat16",
1829 Self::Nat32 => "nat32",
1830 Self::Nat64 => "nat64",
1831 Self::Nat128 => "nat128",
1832 Self::Ulid => "ulid",
1833 Self::Unit => "unit",
1834 Self::Blob { .. }
1835 | Self::Decimal { .. }
1836 | Self::Enum { .. }
1837 | Self::IntBig { .. }
1838 | Self::NatBig { .. }
1839 | Self::Text { .. }
1840 | Self::Relation { .. }
1841 | Self::List(_)
1842 | Self::Set(_)
1843 | Self::Map { .. }
1844 | Self::Composite { .. } => return None,
1845 })
1846 }
1847}
1848
1849fn write_length_bounded_field_kind_summary(
1853 out: &mut String,
1854 kind_name: &str,
1855 max_len: Option<u32>,
1856) {
1857 out.push_str(kind_name);
1858 if let Some(max_len) = max_len {
1859 out.push_str("(max_len=");
1860 out.push_str(&max_len.to_string());
1861 out.push(')');
1862 } else {
1863 out.push_str("(unbounded)");
1864 }
1865}
1866
1867fn write_byte_bounded_field_kind_summary(out: &mut String, kind_name: &str, max_bytes: u32) {
1868 out.push_str(kind_name);
1869 out.push_str("(max_bytes=");
1870 out.push_str(&max_bytes.to_string());
1871 out.push(')');
1872}
1873
1874fn generated_insert_default_facts(
1878 default: FieldDatabaseDefault,
1879) -> (Option<String>, Option<u32>, Option<String>) {
1880 match default {
1881 FieldDatabaseDefault::None => (None, None, None),
1882 FieldDatabaseDefault::EncodedSlotPayload(payload) => {
1883 let bytes = u32::try_from(payload.len()).ok();
1884 let hash = short_default_payload_fingerprint(payload);
1885 (
1886 Some(format!(
1887 "slot_payload(bytes={}, sha256={hash})",
1888 payload.len()
1889 )),
1890 bytes,
1891 Some(hash),
1892 )
1893 }
1894 FieldDatabaseDefault::AuthoredEnumUnit { enum_path, variant } => {
1895 (Some(format!("{enum_path}::{variant}")), None, None)
1896 }
1897 }
1898}
1899
1900struct RenderedTemporalPayload {
1908 value: String,
1909 bytes: u32,
1910 hash: String,
1911}
1912
1913fn accepted_field_temporal_facts(
1914 field: &AcceptedRowLayoutRuntimeField<'_>,
1915 value_catalog: &AcceptedValueCatalogHandle,
1916) -> Result<EntityFieldTemporalFacts, InternalError> {
1917 let write_policy = field.write_policy();
1918 let insert_omission = if write_policy.insert_generation().is_some() {
1919 "generated"
1920 } else if write_policy.write_management().is_some() {
1921 "managed"
1922 } else {
1923 match field.insert_omission_policy() {
1924 AcceptedInsertOmissionPolicy::NullIfMissing => "null",
1925 AcceptedInsertOmissionPolicy::DefaultIfMissing => "default",
1926 AcceptedInsertOmissionPolicy::Required => "required",
1927 }
1928 };
1929 let insert_default = field
1930 .insert_default()
1931 .slot_payload()
1932 .map(|payload| accepted_payload_facts(field, value_catalog, payload))
1933 .transpose()?;
1934 let (insert_default, insert_default_bytes, insert_default_hash) = match insert_default {
1935 Some(payload) => (Some(payload.value), Some(payload.bytes), Some(payload.hash)),
1936 None => (None, None, None),
1937 };
1938 let (historical_fill, historical_fill_bytes, historical_fill_hash) =
1939 match field.historical_fill() {
1940 SchemaHistoricalFill::Reject => (Some("reject".to_string()), None, None),
1941 SchemaHistoricalFill::Null => (Some("null".to_string()), None, None),
1942 SchemaHistoricalFill::SlotPayload(payload) => {
1943 let rendered = accepted_payload_facts(field, value_catalog, payload.as_slice())?;
1944 (
1945 Some(rendered.value),
1946 Some(rendered.bytes),
1947 Some(rendered.hash),
1948 )
1949 }
1950 };
1951
1952 Ok(EntityFieldTemporalFacts {
1953 insert_omission: Some(insert_omission.to_string()),
1954 insert_default,
1955 insert_default_bytes,
1956 insert_default_hash,
1957 introduced_in_layout: Some(field.introduced_in_layout().get()),
1958 historical_fill,
1959 historical_fill_bytes,
1960 historical_fill_hash,
1961 })
1962}
1963
1964fn accepted_payload_facts(
1965 field: &AcceptedRowLayoutRuntimeField<'_>,
1966 value_catalog: &AcceptedValueCatalogHandle,
1967 payload: &[u8],
1968) -> Result<RenderedTemporalPayload, InternalError> {
1969 let persistence = AcceptedFieldPersistenceContract::new(value_catalog, field.decode_contract())
1970 .map_err(|_| InternalError::store_invariant())?;
1971 let admitted = decode_admitted_value_from_accepted_field_contract(persistence, payload)?;
1972 let output = output_value_from_runtime(value_catalog.enum_catalog(), admitted.value())
1973 .map_err(|_| InternalError::store_invariant())?;
1974 let hash = short_default_payload_fingerprint(payload);
1975 let rendered = bounded_schema_value_rendering(&output, payload, hash.as_str());
1976 let bytes = u32::try_from(payload.len()).map_err(|_| InternalError::store_invariant())?;
1977
1978 Ok(RenderedTemporalPayload {
1979 value: rendered,
1980 bytes,
1981 hash,
1982 })
1983}
1984
1985fn bounded_schema_value_rendering(value: &OutputValue, payload: &[u8], hash: &str) -> String {
1986 let rendered = match value {
1987 OutputValue::Text(value) => format!("'{}'", value.escape_default()),
1988 _ => render_output_value_text(value),
1989 };
1990 if rendered.len() <= MAX_SCHEMA_VALUE_RENDER_CHARS {
1991 return rendered;
1992 }
1993
1994 format!(
1995 "{}(bytes={}, sha256={})",
1996 output_value_kind_label(value),
1997 payload.len(),
1998 hash,
1999 )
2000}
2001
2002const fn output_value_kind_label(value: &OutputValue) -> &'static str {
2003 match value {
2004 OutputValue::Account(_) => "account",
2005 OutputValue::Blob(_) => "blob",
2006 OutputValue::Bool(_) => "bool",
2007 OutputValue::Date(_) => "date",
2008 OutputValue::Decimal(_) => "decimal",
2009 OutputValue::Duration(_) => "duration",
2010 OutputValue::Enum(_) => "enum",
2011 OutputValue::Float32(_) => "float32",
2012 OutputValue::Float64(_) => "float64",
2013 OutputValue::Int64(_) => "int64",
2014 OutputValue::Int128(_) => "int128",
2015 OutputValue::IntBig(_) => "int_big",
2016 OutputValue::List(_) => "list",
2017 OutputValue::Map(_) => "map",
2018 OutputValue::Null => "null",
2019 OutputValue::Principal(_) => "principal",
2020 OutputValue::Subaccount(_) => "subaccount",
2021 OutputValue::Text(_) => "text",
2022 OutputValue::Timestamp(_) => "timestamp",
2023 OutputValue::Nat64(_) => "nat64",
2024 OutputValue::Nat128(_) => "nat128",
2025 OutputValue::NatBig(_) => "nat_big",
2026 OutputValue::Ulid(_) => "ulid",
2027 OutputValue::Unit => "unit",
2028 }
2029}
2030
2031fn short_default_payload_fingerprint(payload: &[u8]) -> String {
2032 let digest = Sha256::digest(payload);
2033 let mut out = String::with_capacity(16);
2034 for byte in &digest[..8] {
2035 let _ = write!(out, "{byte:02x}");
2036 }
2037 out
2038}
2039
2040#[cfg_attr(
2041 doc,
2042 doc = "Render one stable field-kind label from accepted persisted schema metadata."
2043)]
2044fn summarize_persisted_field_kind(
2045 kind: &AcceptedFieldKind,
2046 value_catalog: &AcceptedValueCatalogHandle,
2047) -> Result<String, InternalError> {
2048 let mut out = String::new();
2049 write_persisted_field_kind_summary(&mut out, kind, value_catalog)?;
2050
2051 Ok(out)
2052}
2053
2054fn write_persisted_field_kind_summary(
2058 out: &mut String,
2059 kind: &AcceptedFieldKind,
2060 value_catalog: &AcceptedValueCatalogHandle,
2061) -> Result<(), InternalError> {
2062 if let Some(name) = kind.describe_kind_name() {
2063 out.push_str(name);
2064 return Ok(());
2065 }
2066
2067 match kind {
2068 AcceptedFieldKind::Blob { max_len } => {
2069 write_length_bounded_field_kind_summary(out, "blob", *max_len);
2070 }
2071 AcceptedFieldKind::Decimal { scale } => {
2072 let _ = write!(out, "decimal(scale={scale})");
2073 }
2074 AcceptedFieldKind::IntBig { max_bytes } => {
2075 write_byte_bounded_field_kind_summary(out, "int_big", *max_bytes);
2076 }
2077 AcceptedFieldKind::Enum { type_id } => {
2078 let definition = value_catalog
2079 .enum_catalog()
2080 .enum_type(*type_id)
2081 .ok_or_else(InternalError::store_invariant)?;
2082 out.push_str("enum(");
2083 out.push_str(definition.path());
2084 out.push(')');
2085 }
2086 AcceptedFieldKind::Text { max_len } => {
2087 write_length_bounded_field_kind_summary(out, "text", *max_len);
2088 }
2089 AcceptedFieldKind::Relation {
2090 target_entity_name,
2091 key_kind,
2092 ..
2093 } => {
2094 out.push_str("relation(target=");
2095 out.push_str(target_entity_name);
2096 out.push_str(", key=");
2097 write_persisted_field_kind_summary(out, key_kind, value_catalog)?;
2098 out.push(')');
2099 }
2100 AcceptedFieldKind::List(inner) => {
2101 out.push_str("list<");
2102 write_persisted_field_kind_summary(out, inner, value_catalog)?;
2103 out.push('>');
2104 }
2105 AcceptedFieldKind::Set(inner) => {
2106 out.push_str("set<");
2107 write_persisted_field_kind_summary(out, inner, value_catalog)?;
2108 out.push('>');
2109 }
2110 AcceptedFieldKind::Map { key, value } => {
2111 out.push_str("map<");
2112 write_persisted_field_kind_summary(out, key, value_catalog)?;
2113 out.push_str(", ");
2114 write_persisted_field_kind_summary(out, value, value_catalog)?;
2115 out.push('>');
2116 }
2117 AcceptedFieldKind::Composite { type_id } => {
2118 let composite_catalog = value_catalog.composite_catalog();
2119 let definition = composite_catalog
2120 .composite_type(*type_id)
2121 .ok_or_else(InternalError::store_invariant)?;
2122 out.push_str("composite(path=");
2123 out.push_str(definition.path());
2124 out.push_str(", codec=");
2125 write_composite_codec_summary(out, definition.codec());
2126 out.push_str(", shape=");
2127 write_accepted_composite_shape_summary(out, definition.shape(), value_catalog)?;
2128 out.push(')');
2129 }
2130 AcceptedFieldKind::Account
2131 | AcceptedFieldKind::Bool
2132 | AcceptedFieldKind::Date
2133 | AcceptedFieldKind::Duration
2134 | AcceptedFieldKind::Float32
2135 | AcceptedFieldKind::Float64
2136 | AcceptedFieldKind::Int8
2137 | AcceptedFieldKind::Int16
2138 | AcceptedFieldKind::Int32
2139 | AcceptedFieldKind::Int64
2140 | AcceptedFieldKind::Int128
2141 | AcceptedFieldKind::Principal
2142 | AcceptedFieldKind::Subaccount
2143 | AcceptedFieldKind::Timestamp
2144 | AcceptedFieldKind::Nat8
2145 | AcceptedFieldKind::Nat16
2146 | AcceptedFieldKind::Nat32
2147 | AcceptedFieldKind::Nat64
2148 | AcceptedFieldKind::Nat128
2149 | AcceptedFieldKind::Ulid
2150 | AcceptedFieldKind::Unit => unreachable!("schema describe invariant"),
2151 AcceptedFieldKind::NatBig { max_bytes } => {
2152 write_byte_bounded_field_kind_summary(out, "nat_big", *max_bytes);
2153 }
2154 }
2155
2156 Ok(())
2157}
2158
2159impl DescribeKindName for AcceptedFieldKind {
2160 fn describe_kind_name(&self) -> Option<&'static str> {
2161 Some(match self {
2162 Self::Account => "account",
2163 Self::Bool => "bool",
2164 Self::Date => "date",
2165 Self::Duration => "duration",
2166 Self::Float32 => "float32",
2167 Self::Float64 => "float64",
2168 Self::Int8 => "int8",
2169 Self::Int16 => "int16",
2170 Self::Int32 => "int32",
2171 Self::Int64 => "int64",
2172 Self::Int128 => "int128",
2173 Self::Principal => "principal",
2174 Self::Subaccount => "subaccount",
2175 Self::Timestamp => "timestamp",
2176 Self::Nat8 => "nat8",
2177 Self::Nat16 => "nat16",
2178 Self::Nat32 => "nat32",
2179 Self::Nat64 => "nat64",
2180 Self::Nat128 => "nat128",
2181 Self::Ulid => "ulid",
2182 Self::Unit => "unit",
2183 Self::Blob { .. }
2184 | Self::Decimal { .. }
2185 | Self::Enum { .. }
2186 | Self::IntBig { .. }
2187 | Self::NatBig { .. }
2188 | Self::Text { .. }
2189 | Self::Relation { .. }
2190 | Self::List(_)
2191 | Self::Set(_)
2192 | Self::Map { .. }
2193 | Self::Composite { .. } => return None,
2194 })
2195 }
2196}
2197
2198#[cfg(test)]
2203mod tests;