1use std::collections::BTreeMap;
4use std::error::Error as StdError;
5use std::fmt;
6
7use type_bridge_contract::sdk_diagnostic::{
8 SdkDiagnosticCategory, SdkDiagnosticDetailValue, SdkDiagnosticPathSegment,
9 SdkExecutionDiagnostic, SdkProjectionEvidenceSlotPresence, SdkQueryDiagnosticCategory,
10 SdkQueryDiagnosticPathKind,
11};
12use type_bridge_orm::match_request::MatchError;
13use type_bridge_orm::{
14 ProjectedCrudCompatibilityCause, ProjectedCrudCompatibilityFailure,
15 ProjectedCrudCompatibilityStage,
16};
17
18use crate::hooks::{CrudOperation, ModelKind};
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22#[non_exhaustive]
23pub enum ErrorCategory {
24 Connection,
26 Schema,
28 ModelValidation,
30 Integrity,
32 QueryAuthoring,
34 QueryExecution,
36 Transaction,
38 Remote,
40 Capability,
42 ResourceLimit,
44 Cancelled,
46 NotFound,
48 Lifecycle,
50 Database,
52 Other,
54}
55
56impl ErrorCategory {
57 #[must_use]
59 pub const fn as_str(self) -> &'static str {
60 match self {
61 Self::Connection => "connection",
62 Self::Schema => "schema",
63 Self::ModelValidation => "model_validation",
64 Self::Integrity => "integrity",
65 Self::QueryAuthoring => "query_authoring",
66 Self::QueryExecution => "query_execution",
67 Self::Transaction => "transaction",
68 Self::Remote => "remote",
69 Self::Capability => "capability",
70 Self::ResourceLimit => "resource_limit",
71 Self::Cancelled => "cancelled",
72 Self::NotFound => "not_found",
73 Self::Lifecycle => "lifecycle",
74 Self::Database => "database",
75 Self::Other => "other",
76 }
77 }
78}
79
80impl fmt::Display for ErrorCategory {
81 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82 formatter.write_str(self.as_str())
83 }
84}
85
86#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88pub enum ModelValidationPhase {
89 Input,
91 Hydration,
93}
94
95#[derive(Clone, Debug, Eq, PartialEq)]
97#[non_exhaustive]
98pub enum ErrorDetail {
99 Text(String),
101 Long(i64),
103 Boolean(bool),
105 TextList(Vec<String>),
107 QueryCategory(QueryDiagnosticCategory),
109 QueryIdentity(String),
111 QueryIdentityList(Vec<String>),
113}
114
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117#[non_exhaustive]
118pub enum QueryDiagnosticCategory {
119 InvalidPlan,
121 Cardinality,
123 UnsupportedCapability,
125 StaleSchema,
127 ResourceLimit,
129 Cancelled,
131 Provider,
133 ResultDecode,
135}
136
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
139#[non_exhaustive]
140pub enum QueryDiagnosticPathKind {
141 Request,
143 Plan,
145 Operation,
147 Predicate,
149 Output,
151 ProviderEvidence,
153 Result,
155}
156
157#[derive(Clone, Debug, Eq, PartialEq)]
159#[non_exhaustive]
160pub enum ErrorPathSegment {
161 Argument(String),
163 Field(String),
165 Index(u64),
167 Identifier(String),
169 Query(QueryDiagnosticPathKind),
171 QueryBinding(u16),
173 QueryField {
175 owner: String,
177 name: String,
179 },
180 QueryRole {
182 owner: String,
184 name: String,
186 },
187 QueryRoleEdge(u16),
189 QueryOutputSlot(u64),
191 QueryOutputName(String),
193 ContractField(String),
195 ContractIdentity(String),
197}
198
199#[derive(Clone, Debug, Eq, PartialEq)]
202pub struct ErrorDiagnostic {
203 path: Vec<ErrorPathSegment>,
204 details: BTreeMap<String, ErrorDetail>,
205}
206
207impl ErrorDiagnostic {
208 #[must_use]
210 pub fn path(&self) -> &[ErrorPathSegment] {
211 &self.path
212 }
213
214 #[must_use]
216 pub fn details(&self) -> &BTreeMap<String, ErrorDetail> {
217 &self.details
218 }
219}
220
221#[derive(Debug, thiserror::Error)]
223#[non_exhaustive]
224pub enum Error {
225 #[error("Model validation failed during {phase:?}: {message}")]
227 ModelValidation {
228 phase: ModelValidationPhase,
230 code: String,
232 path: Vec<String>,
234 message: String,
236 #[source]
238 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
239 },
240
241 #[error("{category} error [{code}]: {message}")]
244 Classified {
245 category: ErrorCategory,
247 phase: Option<ModelValidationPhase>,
249 code: String,
251 path: Vec<String>,
253 diagnostic: Option<Box<ErrorDiagnostic>>,
255 message: String,
257 #[source]
259 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
260 },
261
262 #[error("Schema verification failed: {message}")]
264 SchemaVerification {
265 message: String,
267 #[source]
269 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
270 },
271
272 #[error("Connection error: {message}")]
274 Connection {
275 message: String,
277 #[source]
279 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
280 },
281
282 #[error("Query execution error: {message}")]
284 QueryExecution {
285 message: String,
287 #[source]
289 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
290 },
291
292 #[error("Transaction error: {message}")]
294 Transaction {
295 message: String,
297 #[source]
299 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
300 },
301
302 #[error("Entity not found: {message}")]
304 NotFound {
305 message: String,
307 #[source]
309 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
310 },
311
312 #[error("Database error: {message}")]
314 Database {
315 message: String,
317 #[source]
319 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
320 },
321
322 #[error("Client error: {message}")]
324 Other {
325 message: String,
327 #[source]
329 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
330 },
331}
332
333impl Error {
334 pub(crate) fn from_contract_diagnostic(
335 error: type_bridge_contract::diagnostic::Diagnostic,
336 ) -> Self {
337 use type_bridge_contract::diagnostic::DiagnosticCategory;
338
339 let category = match error.category() {
340 DiagnosticCategory::InvalidContract => ErrorCategory::Other,
341 DiagnosticCategory::UnsupportedCapability => ErrorCategory::Capability,
342 DiagnosticCategory::ResourceLimit => ErrorCategory::ResourceLimit,
343 DiagnosticCategory::Cancelled => ErrorCategory::Cancelled,
344 DiagnosticCategory::Integrity => ErrorCategory::Integrity,
345 };
346 let code = error.code().as_str().to_owned();
347 let message = error.to_string();
348 Self::classified_with_diagnostic(
349 category,
350 None,
351 code,
352 Vec::new(),
353 None,
354 message,
355 Some(Box::new(error)),
356 )
357 }
358
359 #[allow(dead_code)]
360 pub(crate) fn model_validation(
361 phase: ModelValidationPhase,
362 code: impl Into<String>,
363 path: Vec<String>,
364 message: impl Into<String>,
365 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
366 ) -> Self {
367 Self::ModelValidation {
368 phase,
369 code: code.into(),
370 path,
371 message: message.into(),
372 source,
373 }
374 }
375
376 pub(crate) fn classified(
377 category: ErrorCategory,
378 phase: Option<ModelValidationPhase>,
379 code: impl Into<String>,
380 path: Vec<String>,
381 message: impl Into<String>,
382 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
383 ) -> Self {
384 Self::classified_with_diagnostic(category, phase, code, path, None, message, source)
385 }
386
387 pub(crate) fn projection_evidence_rejection(
388 presence: SdkProjectionEvidenceSlotPresence,
389 ) -> Self {
390 let error = SdkExecutionDiagnostic::classify_detached_semantic_schema_fingerprint_rejection(
391 presence,
392 );
393 let code = error.code().as_str().to_owned();
394 let message = error.message().as_str().to_owned();
395 let path = error.path().iter().map(flatten_sdk_path).collect();
396 let diagnostic = ErrorDiagnostic {
397 path: error
398 .path()
399 .iter()
400 .map(|segment| match segment {
401 SdkDiagnosticPathSegment::Argument(value) => {
402 ErrorPathSegment::Argument(value.as_str().to_owned())
403 }
404 other => typed_sdk_path(other),
405 })
406 .collect(),
407 details: error
408 .details()
409 .iter()
410 .map(|(name, value)| (name.as_str().to_owned(), flatten_sdk_detail(value)))
411 .collect(),
412 };
413 Self::classified_with_diagnostic(
414 ErrorCategory::Integrity,
415 None,
416 code,
417 path,
418 Some(diagnostic),
419 message,
420 Some(Box::new(error)),
421 )
422 }
423
424 fn classified_with_diagnostic(
425 category: ErrorCategory,
426 phase: Option<ModelValidationPhase>,
427 code: impl Into<String>,
428 path: Vec<String>,
429 diagnostic: Option<ErrorDiagnostic>,
430 message: impl Into<String>,
431 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
432 ) -> Self {
433 Self::Classified {
434 category,
435 phase,
436 code: code.into(),
437 path,
438 diagnostic: diagnostic.map(Box::new),
439 message: message.into(),
440 source,
441 }
442 }
443
444 #[must_use]
449 pub fn remote(
450 code: impl Into<String>,
451 message: impl Into<String>,
452 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
453 ) -> Self {
454 Self::classified(
455 ErrorCategory::Remote,
456 None,
457 code,
458 Vec::new(),
459 message,
460 source,
461 )
462 }
463
464 pub(crate) fn from_match(error: MatchError, phase: ModelValidationPhase) -> Self {
465 Self::from_sdk_execution(type_bridge_orm::lower_match_error(&error), phase)
466 }
467
468 pub(crate) fn from_sdk_execution(
469 error: SdkExecutionDiagnostic,
470 model_phase: ModelValidationPhase,
471 ) -> Self {
472 let code = error.code().as_str().to_owned();
473 let message = error.message().as_str().to_owned();
474 let path = error.path().iter().map(flatten_sdk_path).collect();
475 let diagnostic_path = error.path().iter().map(typed_sdk_path).collect();
476 let details = error
477 .details()
478 .iter()
479 .map(|(name, value)| (name.as_str().to_owned(), flatten_sdk_detail(value)))
480 .collect();
481 let diagnostic = ErrorDiagnostic {
482 path: diagnostic_path,
483 details,
484 };
485 if let Some(query_category) = sdk_query_category(&error) {
486 let (category, phase) = match query_category {
487 SdkQueryDiagnosticCategory::InvalidPlan => (ErrorCategory::QueryAuthoring, None),
488 SdkQueryDiagnosticCategory::Cardinality
489 | SdkQueryDiagnosticCategory::ResultDecode => {
490 (ErrorCategory::ModelValidation, Some(model_phase))
491 }
492 SdkQueryDiagnosticCategory::UnsupportedCapability => {
493 (ErrorCategory::Capability, None)
494 }
495 SdkQueryDiagnosticCategory::StaleSchema => (ErrorCategory::Schema, None),
496 SdkQueryDiagnosticCategory::ResourceLimit => (ErrorCategory::ResourceLimit, None),
497 SdkQueryDiagnosticCategory::Cancelled => (ErrorCategory::Cancelled, None),
498 SdkQueryDiagnosticCategory::Provider => (ErrorCategory::QueryExecution, None),
499 _ => (ErrorCategory::Other, None),
500 };
501 return Self::classified_with_diagnostic(
502 category,
503 phase,
504 code,
505 path,
506 Some(diagnostic),
507 message,
508 Some(Box::new(error)),
509 );
510 }
511 match error.category() {
512 SdkDiagnosticCategory::InvalidInput | SdkDiagnosticCategory::Integrity => {
513 Self::ModelValidation {
514 phase: model_phase,
515 code,
516 path,
517 message,
518 source: Some(Box::new(error)),
519 }
520 }
521 SdkDiagnosticCategory::Provider => Self::QueryExecution {
522 message,
523 source: Some(Box::new(error)),
524 },
525 SdkDiagnosticCategory::Transaction => Self::Transaction {
526 message,
527 source: Some(Box::new(error)),
528 },
529 SdkDiagnosticCategory::UnsupportedCapability => Self::classified_with_diagnostic(
530 ErrorCategory::Capability,
531 None,
532 code,
533 path,
534 Some(diagnostic),
535 message,
536 Some(Box::new(error)),
537 ),
538 SdkDiagnosticCategory::ResourceLimit => Self::classified_with_diagnostic(
539 ErrorCategory::ResourceLimit,
540 None,
541 code,
542 path,
543 Some(diagnostic),
544 message,
545 Some(Box::new(error)),
546 ),
547 SdkDiagnosticCategory::Cancelled => Self::classified_with_diagnostic(
548 ErrorCategory::Cancelled,
549 None,
550 code,
551 path,
552 Some(diagnostic),
553 message,
554 Some(Box::new(error)),
555 ),
556 SdkDiagnosticCategory::Internal => Self::classified_with_diagnostic(
557 ErrorCategory::Other,
558 None,
559 code,
560 path,
561 Some(diagnostic),
562 message,
563 Some(Box::new(error)),
564 ),
565 _ => Self::Other {
566 message,
567 source: Some(Box::new(error)),
568 },
569 }
570 }
571
572 #[cfg(feature = "typedb")]
573 pub(crate) fn from_direct_connection(error: SdkExecutionDiagnostic) -> Self {
574 let category = match error.category() {
575 SdkDiagnosticCategory::InvalidInput | SdkDiagnosticCategory::Provider => {
576 ErrorCategory::Connection
577 }
578 SdkDiagnosticCategory::Integrity => ErrorCategory::Integrity,
579 SdkDiagnosticCategory::UnsupportedCapability => ErrorCategory::Capability,
580 SdkDiagnosticCategory::ResourceLimit => ErrorCategory::ResourceLimit,
581 SdkDiagnosticCategory::Cancelled => ErrorCategory::Cancelled,
582 SdkDiagnosticCategory::Transaction | SdkDiagnosticCategory::Internal => {
583 ErrorCategory::Other
584 }
585 _ => ErrorCategory::Other,
586 };
587 let code = error.code().as_str().to_owned();
588 let message = error.message().as_str().to_owned();
589 let path = error.path().iter().map(flatten_sdk_path).collect();
590 let diagnostic = ErrorDiagnostic {
591 path: error.path().iter().map(typed_sdk_path).collect(),
592 details: error
593 .details()
594 .iter()
595 .map(|(name, value)| (name.as_str().to_owned(), flatten_sdk_detail(value)))
596 .collect(),
597 };
598 Self::classified_with_diagnostic(
599 category,
600 None,
601 code,
602 path,
603 Some(diagnostic),
604 message,
605 Some(Box::new(error)),
606 )
607 }
608
609 pub(crate) fn from_projected_batch(
610 error: SdkExecutionDiagnostic,
611 model_phase: ModelValidationPhase,
612 ) -> Self {
613 let (category, phase) = match error.category() {
614 SdkDiagnosticCategory::InvalidInput => {
615 (ErrorCategory::ModelValidation, Some(model_phase))
616 }
617 SdkDiagnosticCategory::Integrity => (ErrorCategory::Integrity, Some(model_phase)),
618 SdkDiagnosticCategory::Provider => (ErrorCategory::QueryExecution, None),
619 SdkDiagnosticCategory::Transaction => (ErrorCategory::Transaction, None),
620 SdkDiagnosticCategory::UnsupportedCapability => (ErrorCategory::Capability, None),
621 SdkDiagnosticCategory::ResourceLimit => (ErrorCategory::ResourceLimit, None),
622 SdkDiagnosticCategory::Cancelled => (ErrorCategory::Cancelled, None),
623 SdkDiagnosticCategory::Internal => (ErrorCategory::Other, None),
624 _ => (ErrorCategory::Other, None),
625 };
626 let code = error.code().as_str().to_owned();
627 let message = error.message().as_str().to_owned();
628 let path = error.path().iter().map(flatten_sdk_path).collect();
629 let diagnostic = ErrorDiagnostic {
630 path: error
631 .path()
632 .iter()
633 .map(typed_projected_batch_path)
634 .collect(),
635 details: error
636 .details()
637 .iter()
638 .map(|(name, value)| (name.as_str().to_owned(), flatten_sdk_detail(value)))
639 .collect(),
640 };
641 Self::classified_with_diagnostic(
642 category,
643 phase,
644 code,
645 path,
646 Some(diagnostic),
647 message,
648 Some(Box::new(error)),
649 )
650 }
651
652 pub(crate) fn with_projected_batch_row(self, ordinal: u64) -> Self {
653 let row_path = [
654 ErrorPathSegment::Argument("rows".to_owned()),
655 ErrorPathSegment::Index(ordinal),
656 ];
657 match self {
658 Self::ModelValidation {
659 phase,
660 code,
661 mut path,
662 message,
663 source,
664 } => {
665 let mut typed_path = Vec::with_capacity(path.len().saturating_add(2));
666 typed_path.extend(row_path);
667 for segment in &path {
668 append_generated_path_segment(&mut typed_path, segment);
669 }
670 path.insert(0, format!("[{ordinal}]"));
671 path.insert(0, "rows".to_owned());
672 Self::classified_with_diagnostic(
673 ErrorCategory::ModelValidation,
674 Some(phase),
675 code,
676 path,
677 Some(ErrorDiagnostic {
678 path: typed_path,
679 details: BTreeMap::new(),
680 }),
681 message,
682 source,
683 )
684 }
685 Self::Classified {
686 category,
687 phase,
688 code,
689 mut path,
690 diagnostic,
691 message,
692 source,
693 } => {
694 path.insert(0, format!("[{ordinal}]"));
695 path.insert(0, "rows".to_owned());
696 let mut diagnostic = diagnostic.map_or_else(
697 || ErrorDiagnostic {
698 path: Vec::new(),
699 details: BTreeMap::new(),
700 },
701 |diagnostic| *diagnostic,
702 );
703 diagnostic.path.splice(0..0, row_path);
704 Self::classified_with_diagnostic(
705 category,
706 phase,
707 code,
708 path,
709 Some(diagnostic),
710 message,
711 source,
712 )
713 }
714 other => other,
715 }
716 }
717
718 pub(crate) fn from_projected_crud(
719 error: ProjectedCrudCompatibilityFailure,
720 kind: ModelKind,
721 operation: Option<CrudOperation>,
722 ) -> Self {
723 let (diagnostic, stage, cause) = error.into_parts();
724 let code = diagnostic.code().as_str();
725
726 if code == "mutation_rehydration_missing" {
727 let message = match (kind, operation) {
728 (ModelKind::Entity, Some(CrudOperation::Update)) => {
729 "updated entity was not returned"
730 }
731 (ModelKind::Entity, _) => "written entity was not returned",
732 (ModelKind::Relation, _) => "written relation was not returned",
733 };
734 return Self::model_validation(
735 ModelValidationPhase::Hydration,
736 "missing_post_write_row",
737 vec!["iid".into()],
738 message,
739 None,
740 );
741 }
742 if code == "relation_hydration_ambiguous" {
743 return Self::model_validation(
744 ModelValidationPhase::Hydration,
745 "ambiguous_provider_row",
746 vec!["iid".into()],
747 "provider returned multiple coalesced rows for one exact IID",
748 None,
749 );
750 }
751 if code == "hydrated_type_mismatch" {
752 let message = match kind {
753 ModelKind::Entity => "provider entity row has the wrong exact concrete type",
754 ModelKind::Relation => "provider relation row has the wrong exact concrete type",
755 };
756 return Self::model_validation(
757 ModelValidationPhase::Hydration,
758 "wrong_concrete_type",
759 vec!["type".into()],
760 message,
761 None,
762 );
763 }
764 if code == "hydrated_iid_missing" {
765 let message = match kind {
766 ModelKind::Entity => "provider entity row omitted its IID",
767 ModelKind::Relation => "provider relation row omitted its IID",
768 };
769 return Self::model_validation(
770 ModelValidationPhase::Hydration,
771 "missing_iid",
772 vec!["iid".into()],
773 message,
774 None,
775 );
776 }
777 if code == "hydrated_iid_mismatch" {
778 let message = match kind {
779 ModelKind::Entity => "provider entity row contains a noncanonical IID",
780 ModelKind::Relation => "provider relation row contains a noncanonical IID",
781 };
782 return Self::model_validation(
783 ModelValidationPhase::Hydration,
784 "noncanonical_iid",
785 vec!["iid".into()],
786 message,
787 None,
788 );
789 }
790 if kind == ModelKind::Relation
791 && code == "noncanonical_iid"
792 && diagnostic
793 .path()
794 .iter()
795 .any(|segment| matches!(segment, SdkDiagnosticPathSegment::Role(_)))
796 {
797 return Self::model_validation(
798 ModelValidationPhase::Input,
799 "noncanonical_player_iid",
800 projected_role_path(&diagnostic, "iid"),
801 "relation reference IID must be canonical",
802 None,
803 );
804 }
805 if kind == ModelKind::Relation
806 && diagnostic
807 .path()
808 .iter()
809 .any(|segment| matches!(segment, SdkDiagnosticPathSegment::Role(_)))
810 && matches!(
811 cause.as_ref(),
812 Some(ProjectedCrudCompatibilityCause::Orm(
813 type_bridge_orm::OrmError::Hydration { .. }
814 ))
815 )
816 {
817 return Self::model_validation(
818 ModelValidationPhase::Hydration,
819 "invalid_player_attributes",
820 projected_role_path(&diagnostic, "attributes"),
821 "provider role player attributes are outside the projected descriptor",
822 cause.and_then(projected_cause_source),
823 );
824 }
825 if kind == ModelKind::Relation
826 && code == "runtime_projection_mismatch"
827 && diagnostic
828 .path()
829 .iter()
830 .any(|segment| matches!(segment, SdkDiagnosticPathSegment::Role(_)))
831 {
832 return Self::model_validation(
833 ModelValidationPhase::Hydration,
834 "invalid_player_attributes",
835 projected_role_path(&diagnostic, "attributes"),
836 "provider role player attributes are outside the projected descriptor",
837 cause.and_then(projected_cause_source),
838 );
839 }
840 if kind == ModelKind::Relation {
841 let mapped = match code {
842 "hydrated_player_iid_missing" => Some((
843 "missing_player_iid",
844 "iid",
845 "provider role player omitted its IID",
846 )),
847 "hydrated_player_iid_invalid" => Some((
848 "noncanonical_player_iid",
849 "iid",
850 "provider role player contains a noncanonical IID",
851 )),
852 "hydrated_player_type_missing" => Some((
853 "missing_player_type",
854 "type",
855 "provider role player omitted its concrete type",
856 )),
857 "hydrated_role_player_not_accepted" => Some((
858 "player_not_allowed",
859 "type",
860 "provider role player type is outside the role",
861 )),
862 "projected_player_type_ambiguous" => Some((
863 "invalid_installed_projection",
864 "type",
865 "projected player authority is ambiguous",
866 )),
867 _ => None,
868 };
869 if let Some((legacy_code, suffix, message)) = mapped {
870 return Self::model_validation(
871 ModelValidationPhase::Hydration,
872 legacy_code,
873 projected_role_path(&diagnostic, suffix),
874 message,
875 None,
876 );
877 }
878 }
879
880 if let Some(cause) = cause {
881 return match cause {
882 ProjectedCrudCompatibilityCause::Orm(error) => {
883 if stage == ProjectedCrudCompatibilityStage::Hydration {
884 Self::from_orm_hydration(error)
885 } else {
886 Self::from_orm(error)
887 }
888 }
889 ProjectedCrudCompatibilityCause::Commit(error) => {
890 Self::from_orm(error.into_orm_error())
891 }
892 };
893 }
894 Self::from_sdk_execution(
895 diagnostic,
896 if stage == ProjectedCrudCompatibilityStage::Hydration {
897 ModelValidationPhase::Hydration
898 } else {
899 ModelValidationPhase::Input
900 },
901 )
902 }
903
904 pub(crate) fn from_hook(error: crate::hooks::HookError) -> Self {
905 let code = match error {
906 crate::hooks::HookError::Rejected { .. } => "lifecycle_hook_rejected",
907 crate::hooks::HookError::Internal { .. } => "lifecycle_hook_failed",
908 };
909 Self::classified(
910 ErrorCategory::Lifecycle,
911 None,
912 code,
913 Vec::new(),
914 error.to_string(),
915 Some(Box::new(error)),
916 )
917 }
918
919 #[allow(dead_code)]
920 pub(crate) fn from_orm(err: type_bridge_orm::OrmError) -> Self {
921 match err {
922 type_bridge_orm::OrmError::Match(error) => {
923 Self::from_match(error, ModelValidationPhase::Input)
924 }
925 error @ type_bridge_orm::OrmError::Connection(_) => Self::Connection {
926 message: error.to_string(),
927 source: Some(Box::new(error)),
928 },
929 error @ type_bridge_orm::OrmError::QueryExecution(_) => Self::QueryExecution {
930 message: error.to_string(),
931 source: Some(Box::new(error)),
932 },
933 error @ type_bridge_orm::OrmError::Transaction(_) => Self::Transaction {
934 message: error.to_string(),
935 source: Some(Box::new(error)),
936 },
937 error @ type_bridge_orm::OrmError::NotFound(_) => Self::NotFound {
938 message: error.to_string(),
939 source: Some(Box::new(error)),
940 },
941 error @ type_bridge_orm::OrmError::Hydration { .. } => Self::ModelValidation {
942 phase: ModelValidationPhase::Hydration,
943 code: "invalid_provider_evidence".into(),
944 path: vec![],
945 message: error.to_string(),
946 source: Some(Box::new(error)),
947 },
948 error => Self::Database {
949 message: error.to_string(),
950 source: Some(Box::new(error)),
951 },
952 }
953 }
954
955 pub(crate) fn from_orm_hydration(err: type_bridge_orm::OrmError) -> Self {
956 match err {
957 type_bridge_orm::OrmError::Match(error) => {
958 Self::from_match(error, ModelValidationPhase::Hydration)
959 }
960 error => Self::from_orm(error),
961 }
962 }
963
964 #[must_use]
966 pub const fn category(&self) -> ErrorCategory {
967 match self {
968 Self::ModelValidation { .. } => ErrorCategory::ModelValidation,
969 Self::Classified { category, .. } => *category,
970 Self::SchemaVerification { .. } => ErrorCategory::Schema,
971 Self::Connection { .. } => ErrorCategory::Connection,
972 Self::QueryExecution { .. } => ErrorCategory::QueryExecution,
973 Self::Transaction { .. } => ErrorCategory::Transaction,
974 Self::NotFound { .. } => ErrorCategory::NotFound,
975 Self::Database { .. } => ErrorCategory::Database,
976 Self::Other { .. } => ErrorCategory::Other,
977 }
978 }
979
980 #[must_use]
988 pub fn sdk_category(&self) -> Option<&'static str> {
989 StdError::source(self)
990 .and_then(|source| source.downcast_ref::<SdkExecutionDiagnostic>())
991 .map(|diagnostic| diagnostic.category().as_str())
992 }
993
994 #[must_use]
996 pub fn message(&self) -> &str {
997 match self {
998 Self::ModelValidation { message, .. }
999 | Self::Classified { message, .. }
1000 | Self::SchemaVerification { message, .. }
1001 | Self::Connection { message, .. }
1002 | Self::QueryExecution { message, .. }
1003 | Self::Transaction { message, .. }
1004 | Self::NotFound { message, .. }
1005 | Self::Database { message, .. }
1006 | Self::Other { message, .. } => message,
1007 }
1008 }
1009
1010 #[must_use]
1012 pub fn code(&self) -> Option<&str> {
1013 match self {
1014 Self::ModelValidation { code, .. } | Self::Classified { code, .. } => Some(code),
1015 _ => None,
1016 }
1017 }
1018
1019 #[must_use]
1021 pub fn path(&self) -> Option<&[String]> {
1022 match self {
1023 Self::ModelValidation { path, .. } | Self::Classified { path, .. } => Some(path),
1024 _ => None,
1025 }
1026 }
1027
1028 #[must_use]
1033 pub fn diagnostic_path(&self) -> Option<&[ErrorPathSegment]> {
1034 match self {
1035 Self::Classified { diagnostic, .. } => diagnostic.as_deref().map(ErrorDiagnostic::path),
1036 _ => None,
1037 }
1038 }
1039
1040 #[must_use]
1042 pub fn details(&self) -> Option<&BTreeMap<String, ErrorDetail>> {
1043 match self {
1044 Self::Classified { diagnostic, .. } => {
1045 diagnostic.as_deref().map(ErrorDiagnostic::details)
1046 }
1047 _ => None,
1048 }
1049 }
1050
1051 #[must_use]
1053 pub const fn model_validation_phase(&self) -> Option<ModelValidationPhase> {
1054 match self {
1055 Self::ModelValidation { phase, .. } => Some(*phase),
1056 Self::Classified { phase, .. } => *phase,
1057 _ => None,
1058 }
1059 }
1060}
1061
1062fn flatten_sdk_path(segment: &SdkDiagnosticPathSegment) -> String {
1063 match segment {
1064 SdkDiagnosticPathSegment::Argument(value) => value.as_str().to_owned(),
1065 SdkDiagnosticPathSegment::Index(value) => format!("[{value}]"),
1066 SdkDiagnosticPathSegment::Type(_) => "type".into(),
1067 SdkDiagnosticPathSegment::Field(value) => value.attribute().label().as_str().to_owned(),
1068 SdkDiagnosticPathSegment::Role(value) => value.label().as_str().to_owned(),
1069 SdkDiagnosticPathSegment::Query(value) => value.as_str().to_owned(),
1070 SdkDiagnosticPathSegment::QueryBinding(value) => format!("binding[{value}]"),
1071 SdkDiagnosticPathSegment::QueryField { owner, name } => {
1072 format!("{}.{}", owner.as_str(), name.as_str())
1073 }
1074 SdkDiagnosticPathSegment::QueryRole { owner, name } => {
1075 format!("{}.{}", owner.as_str(), name.as_str())
1076 }
1077 SdkDiagnosticPathSegment::QueryRoleEdge(value) => format!("role_edge[{value}]"),
1078 SdkDiagnosticPathSegment::QueryOutputSlot(value) => format!("output[{value}]"),
1079 SdkDiagnosticPathSegment::QueryOutputName(value)
1080 | SdkDiagnosticPathSegment::ContractField(value)
1081 | SdkDiagnosticPathSegment::ContractIdentity(value) => value.as_str().to_owned(),
1082 _ => "diagnostic".into(),
1083 }
1084}
1085
1086fn projected_role_path(diagnostic: &SdkExecutionDiagnostic, suffix: &'static str) -> Vec<String> {
1087 let role = diagnostic.path().iter().find_map(|segment| {
1088 let SdkDiagnosticPathSegment::Role(role) = segment else {
1089 return None;
1090 };
1091 Some(role.label().as_str())
1092 });
1093 let index = diagnostic.path().iter().find_map(|segment| {
1094 let SdkDiagnosticPathSegment::Index(index) = segment else {
1095 return None;
1096 };
1097 Some(*index)
1098 });
1099 let head = match (role, index) {
1100 (Some(role), Some(index)) => format!("{role}[{index}]"),
1101 (Some(role), None) => role.to_owned(),
1102 _ => "roles".to_owned(),
1103 };
1104 vec![head, suffix.to_owned()]
1105}
1106
1107fn projected_cause_source(
1108 cause: ProjectedCrudCompatibilityCause,
1109) -> Option<Box<dyn StdError + Send + Sync + 'static>> {
1110 match cause {
1111 ProjectedCrudCompatibilityCause::Orm(error) => Some(Box::new(error)),
1112 ProjectedCrudCompatibilityCause::Commit(error) => Some(Box::new(error)),
1113 }
1114}
1115
1116fn typed_sdk_path(segment: &SdkDiagnosticPathSegment) -> ErrorPathSegment {
1117 match segment {
1118 SdkDiagnosticPathSegment::Argument(value) => {
1119 ErrorPathSegment::Field(value.as_str().to_owned())
1120 }
1121 SdkDiagnosticPathSegment::Index(value) => ErrorPathSegment::Index(*value),
1122 SdkDiagnosticPathSegment::Type(value) => ErrorPathSegment::Identifier(format!(
1123 "{}:{}",
1124 match value.kind() {
1125 type_bridge_contract::id::TypeKind::Entity => "entity",
1126 type_bridge_contract::id::TypeKind::Relation => "relation",
1127 type_bridge_contract::id::TypeKind::Attribute => "attribute",
1128 type_bridge_contract::id::TypeKind::Struct => "struct",
1129 },
1130 value.label().as_str()
1131 )),
1132 SdkDiagnosticPathSegment::Field(value) => ErrorPathSegment::Identifier(format!(
1133 "{}:{}",
1134 value.owner().label().as_str(),
1135 value.attribute().label().as_str()
1136 )),
1137 SdkDiagnosticPathSegment::Role(value) => ErrorPathSegment::Identifier(format!(
1138 "{}:{}",
1139 value.declaring_relation().as_str(),
1140 value.label().as_str()
1141 )),
1142 SdkDiagnosticPathSegment::Query(value) => {
1143 ErrorPathSegment::Query(acceptance_path_kind(*value))
1144 }
1145 SdkDiagnosticPathSegment::QueryBinding(value) => ErrorPathSegment::QueryBinding(*value),
1146 SdkDiagnosticPathSegment::QueryField { owner, name } => ErrorPathSegment::QueryField {
1147 owner: owner.as_str().to_owned(),
1148 name: name.as_str().to_owned(),
1149 },
1150 SdkDiagnosticPathSegment::QueryRole { owner, name } => ErrorPathSegment::QueryRole {
1151 owner: owner.as_str().to_owned(),
1152 name: name.as_str().to_owned(),
1153 },
1154 SdkDiagnosticPathSegment::QueryRoleEdge(value) => ErrorPathSegment::QueryRoleEdge(*value),
1155 SdkDiagnosticPathSegment::QueryOutputSlot(value) => {
1156 ErrorPathSegment::QueryOutputSlot(*value)
1157 }
1158 SdkDiagnosticPathSegment::QueryOutputName(value) => {
1159 ErrorPathSegment::QueryOutputName(value.as_str().to_owned())
1160 }
1161 SdkDiagnosticPathSegment::ContractField(value) => {
1162 ErrorPathSegment::ContractField(value.as_str().to_owned())
1163 }
1164 SdkDiagnosticPathSegment::ContractIdentity(value) => {
1165 ErrorPathSegment::ContractIdentity(value.as_str().to_owned())
1166 }
1167 _ => ErrorPathSegment::Identifier("diagnostic".into()),
1168 }
1169}
1170
1171fn typed_projected_batch_path(segment: &SdkDiagnosticPathSegment) -> ErrorPathSegment {
1172 match segment {
1173 SdkDiagnosticPathSegment::Argument(value) => {
1174 ErrorPathSegment::Argument(value.as_str().to_owned())
1175 }
1176 other => typed_sdk_path(other),
1177 }
1178}
1179
1180fn append_generated_path_segment(path: &mut Vec<ErrorPathSegment>, segment: &str) {
1181 let Some(first_index) = segment.find('[') else {
1182 path.push(ErrorPathSegment::Field(segment.to_owned()));
1183 return;
1184 };
1185 let mut parsed = Vec::new();
1186 if first_index > 0 {
1187 parsed.push(ErrorPathSegment::Field(segment[..first_index].to_owned()));
1188 }
1189 let mut remainder = &segment[first_index..];
1190 while remainder.starts_with('[') {
1191 let Some(end) = remainder.find(']') else {
1192 path.push(ErrorPathSegment::Field(segment.to_owned()));
1193 return;
1194 };
1195 let Ok(index) = remainder[1..end].parse::<u64>() else {
1196 path.push(ErrorPathSegment::Field(segment.to_owned()));
1197 return;
1198 };
1199 parsed.push(ErrorPathSegment::Index(index));
1200 remainder = &remainder[end + 1..];
1201 }
1202 if !remainder.is_empty() || parsed.is_empty() {
1203 path.push(ErrorPathSegment::Field(segment.to_owned()));
1204 } else {
1205 path.extend(parsed);
1206 }
1207}
1208
1209fn flatten_sdk_detail(value: &SdkDiagnosticDetailValue) -> ErrorDetail {
1210 match value {
1211 SdkDiagnosticDetailValue::Boolean(value) => ErrorDetail::Boolean(*value),
1212 SdkDiagnosticDetailValue::Count(value) | SdkDiagnosticDetailValue::ByteCount(value) => {
1213 i64::try_from(*value)
1214 .map_or_else(|_| ErrorDetail::Text(value.to_string()), ErrorDetail::Long)
1215 }
1216 SdkDiagnosticDetailValue::Capability(value) => ErrorDetail::Text(value.as_str().to_owned()),
1217 SdkDiagnosticDetailValue::ValueType(value) => ErrorDetail::Text(value.as_str().to_owned()),
1218 SdkDiagnosticDetailValue::Type(value) => {
1219 ErrorDetail::Text(value.label().as_str().to_owned())
1220 }
1221 SdkDiagnosticDetailValue::Field(value) => {
1222 ErrorDetail::Text(value.attribute().label().as_str().to_owned())
1223 }
1224 SdkDiagnosticDetailValue::Role(value) => {
1225 ErrorDetail::Text(value.label().as_str().to_owned())
1226 }
1227 SdkDiagnosticDetailValue::Fingerprint(value) => ErrorDetail::Text(value.digest().to_hex()),
1228 SdkDiagnosticDetailValue::ProviderOperation(value) => {
1229 ErrorDetail::Text(value.as_str().to_owned())
1230 }
1231 SdkDiagnosticDetailValue::CommitOutcome(value) => {
1232 ErrorDetail::Text(value.as_str().to_owned())
1233 }
1234 SdkDiagnosticDetailValue::Signed(value) => ErrorDetail::Long(*value),
1235 SdkDiagnosticDetailValue::QueryCategory(value) => {
1236 ErrorDetail::QueryCategory(query_category(*value))
1237 }
1238 SdkDiagnosticDetailValue::QueryIdentity(value) => {
1239 ErrorDetail::QueryIdentity(value.as_str().to_owned())
1240 }
1241 SdkDiagnosticDetailValue::QueryIdentityList(values) => ErrorDetail::QueryIdentityList(
1242 values
1243 .iter()
1244 .map(|value| value.as_str().to_owned())
1245 .collect(),
1246 ),
1247 _ => ErrorDetail::Text("diagnostic".into()),
1248 }
1249}
1250
1251fn sdk_query_category(error: &SdkExecutionDiagnostic) -> Option<SdkQueryDiagnosticCategory> {
1252 error.details().iter().find_map(|(name, value)| {
1253 (name.as_str() == "query_category").then(|| {
1254 let SdkDiagnosticDetailValue::QueryCategory(category) = value else {
1255 return None;
1256 };
1257 Some(*category)
1258 })?
1259 })
1260}
1261
1262const fn query_category(value: SdkQueryDiagnosticCategory) -> QueryDiagnosticCategory {
1263 match value {
1264 SdkQueryDiagnosticCategory::InvalidPlan => QueryDiagnosticCategory::InvalidPlan,
1265 SdkQueryDiagnosticCategory::Cardinality => QueryDiagnosticCategory::Cardinality,
1266 SdkQueryDiagnosticCategory::UnsupportedCapability => {
1267 QueryDiagnosticCategory::UnsupportedCapability
1268 }
1269 SdkQueryDiagnosticCategory::StaleSchema => QueryDiagnosticCategory::StaleSchema,
1270 SdkQueryDiagnosticCategory::ResourceLimit => QueryDiagnosticCategory::ResourceLimit,
1271 SdkQueryDiagnosticCategory::Cancelled => QueryDiagnosticCategory::Cancelled,
1272 SdkQueryDiagnosticCategory::Provider => QueryDiagnosticCategory::Provider,
1273 SdkQueryDiagnosticCategory::ResultDecode => QueryDiagnosticCategory::ResultDecode,
1274 _ => QueryDiagnosticCategory::ResultDecode,
1275 }
1276}
1277
1278const fn acceptance_path_kind(value: SdkQueryDiagnosticPathKind) -> QueryDiagnosticPathKind {
1279 match value {
1280 SdkQueryDiagnosticPathKind::Request => QueryDiagnosticPathKind::Request,
1281 SdkQueryDiagnosticPathKind::Plan => QueryDiagnosticPathKind::Plan,
1282 SdkQueryDiagnosticPathKind::Operation => QueryDiagnosticPathKind::Operation,
1283 SdkQueryDiagnosticPathKind::Predicate => QueryDiagnosticPathKind::Predicate,
1284 SdkQueryDiagnosticPathKind::Output => QueryDiagnosticPathKind::Output,
1285 SdkQueryDiagnosticPathKind::ProviderEvidence => QueryDiagnosticPathKind::ProviderEvidence,
1286 SdkQueryDiagnosticPathKind::Result => QueryDiagnosticPathKind::Result,
1287 _ => QueryDiagnosticPathKind::Result,
1288 }
1289}
1290
1291pub type Result<T, E = Error> = std::result::Result<T, E>;
1293
1294#[cfg(test)]
1295mod tests {
1296 use super::{Error, ErrorCategory};
1297
1298 #[test]
1299 fn public_error_categories_and_remote_constructor_are_stable() {
1300 let categories = [
1301 (ErrorCategory::Connection, "connection"),
1302 (ErrorCategory::Schema, "schema"),
1303 (ErrorCategory::ModelValidation, "model_validation"),
1304 (ErrorCategory::Integrity, "integrity"),
1305 (ErrorCategory::QueryAuthoring, "query_authoring"),
1306 (ErrorCategory::QueryExecution, "query_execution"),
1307 (ErrorCategory::Transaction, "transaction"),
1308 (ErrorCategory::Remote, "remote"),
1309 (ErrorCategory::Capability, "capability"),
1310 (ErrorCategory::ResourceLimit, "resource_limit"),
1311 (ErrorCategory::NotFound, "not_found"),
1312 (ErrorCategory::Lifecycle, "lifecycle"),
1313 (ErrorCategory::Database, "database"),
1314 (ErrorCategory::Other, "other"),
1315 ];
1316 for (category, spelling) in categories {
1317 assert_eq!(category.as_str(), spelling);
1318 assert_eq!(category.to_string(), spelling);
1319 }
1320
1321 let error = Error::remote("remote_transport", "connection reset", None);
1322 assert_eq!(error.category(), ErrorCategory::Remote);
1323 assert_eq!(error.code(), Some("remote_transport"));
1324 assert_eq!(error.path(), Some(&[][..]));
1325 assert_eq!(error.message(), "connection reset");
1326 }
1327}