use super::*;
use crate::db::{
access::AccessPlanError,
cursor::CursorPlanError,
query::plan::{
PlanError, PolicyPlanError,
validate::{GroupPlanError, OrderPlanError, PlanPolicyError, PlanUserError},
},
};
fn from_group_plan_error(err: PlanError) -> InternalError {
let message = match err {
PlanError::User(inner) => match *inner {
PlanUserError::Group(inner) => {
InternalError::invalid_logical_plan_message(inner.to_string())
}
other => {
format!("group-plan error conversion received non-group user variant: {other}")
}
},
PlanError::Policy(inner) => match *inner {
PlanPolicyError::Group(inner) => {
InternalError::invalid_logical_plan_message(inner.to_string())
}
PlanPolicyError::Policy(inner) => {
format!("group-plan error conversion received non-group policy variant: {inner}")
}
},
PlanError::Cursor(inner) => {
format!("group-plan error conversion received cursor variant: {inner}")
}
};
InternalError::planner_invariant(message)
}
fn plan_invariant_violation(err: PolicyPlanError) -> InternalError {
let reason = match err {
PolicyPlanError::EmptyOrderSpec => "order specification must include at least one field",
PolicyPlanError::DeletePlanWithGrouping => {
"delete plans must not include GROUP BY or HAVING"
}
PolicyPlanError::DeletePlanWithPagination => "delete plans must not include pagination",
PolicyPlanError::LoadPlanWithDeleteLimit => "load plans must not carry delete limits",
PolicyPlanError::DeleteWindowRequiresOrder => {
"delete limit or offset requires explicit ordering"
}
PolicyPlanError::UnorderedPagination => "pagination requires explicit ordering",
PolicyPlanError::ExpressionOrderRequiresIndexSatisfiedAccess => {
"expression ORDER BY requires matching index-backed access ordering"
}
};
InternalError::planner_invariant(InternalError::executor_invariant_message(reason))
}
#[test]
fn index_plan_index_corruption_uses_index_origin() {
let err = InternalError::index_plan_index_corruption("broken key payload");
assert_eq!(err.class, ErrorClass::Corruption);
assert_eq!(err.origin, ErrorOrigin::Index);
assert_eq!(
err.message,
"corruption detected (index): broken key payload"
);
}
#[test]
fn index_plan_store_corruption_uses_store_origin() {
let err = InternalError::index_plan_store_corruption("row/key mismatch");
assert_eq!(err.class, ErrorClass::Corruption);
assert_eq!(err.origin, ErrorOrigin::Store);
assert_eq!(err.message, "corruption detected (store): row/key mismatch");
}
#[test]
fn index_plan_serialize_corruption_uses_serialize_origin() {
let err = InternalError::index_plan_serialize_corruption("decode failed");
assert_eq!(err.class, ErrorClass::Corruption);
assert_eq!(err.origin, ErrorOrigin::Serialize);
assert_eq!(
err.message,
"corruption detected (serialize): decode failed"
);
}
#[test]
fn serialize_incompatible_persisted_format_uses_serialize_origin() {
let err = InternalError::serialize_incompatible_persisted_format("row format version 7");
assert_eq!(err.class, ErrorClass::IncompatiblePersistedFormat);
assert_eq!(err.origin, ErrorOrigin::Serialize);
assert_eq!(err.message, "row format version 7");
}
#[test]
fn index_plan_store_invariant_uses_store_origin() {
let err = InternalError::index_plan_store_invariant("row/key mismatch");
assert_eq!(err.class, ErrorClass::InvariantViolation);
assert_eq!(err.origin, ErrorOrigin::Store);
assert_eq!(
err.message,
"invariant violation detected (store): row/key mismatch"
);
}
#[test]
fn query_executor_invariant_uses_invariant_violation_class() {
let err = InternalError::query_executor_invariant("route contract mismatch");
assert_eq!(err.class, ErrorClass::InvariantViolation);
assert_eq!(err.origin, ErrorOrigin::Query);
}
#[test]
fn cursor_executor_invariant_uses_cursor_origin() {
let err = InternalError::cursor_executor_invariant("cursor contract mismatch");
assert_eq!(err.class, ErrorClass::InvariantViolation);
assert_eq!(err.origin, ErrorOrigin::Cursor);
}
#[test]
fn query_unsupported_uses_query_origin() {
let err = InternalError::query_unsupported("unsupported query shape");
assert_eq!(err.class, ErrorClass::Unsupported);
assert_eq!(err.origin, ErrorOrigin::Query);
assert_eq!(err.message, "unsupported query shape");
}
#[cfg(feature = "sql")]
#[test]
fn query_unsupported_sql_feature_preserves_query_detail_label() {
let err = InternalError::query_unsupported_sql_feature("JOIN");
assert_eq!(err.class, ErrorClass::Unsupported);
assert_eq!(err.origin, ErrorOrigin::Query);
assert!(
matches!(
err.detail(),
Some(ErrorDetail::Query(QueryErrorDetail::UnsupportedSqlFeature { feature }))
if feature == &"JOIN"
),
"query unsupported SQL feature helper should preserve structured feature label detail",
);
}
#[test]
fn executor_access_plan_error_mapping_stays_invariant_violation() {
let err = AccessPlanError::IndexPrefixEmpty.into_internal_error();
assert_eq!(err.class, ErrorClass::InvariantViolation);
assert_eq!(err.origin, ErrorOrigin::Query);
}
#[test]
fn plan_policy_error_mapping_uses_executor_invariant_prefix() {
let err = plan_invariant_violation(PolicyPlanError::DeleteWindowRequiresOrder);
assert_eq!(err.class, ErrorClass::InvariantViolation);
assert_eq!(err.origin, ErrorOrigin::Planner);
assert_eq!(
err.message,
"executor invariant violated: delete limit or offset requires explicit ordering",
);
}
#[test]
fn group_plan_error_mapping_uses_invalid_logical_plan_prefix() {
let err = from_group_plan_error(PlanError::from(GroupPlanError::UnknownGroupField {
field: "tenant".to_string(),
}));
assert_eq!(err.class, ErrorClass::InvariantViolation);
assert_eq!(err.origin, ErrorOrigin::Planner);
assert_eq!(
err.message,
"invalid logical plan: unknown group field 'tenant'",
);
}
#[test]
fn group_plan_error_mapping_rejects_non_group_user_variant() {
let err = from_group_plan_error(PlanError::from(PlanUserError::Order(Box::new(
OrderPlanError::UnknownField {
field: "tenant".to_string(),
},
))));
assert_eq!(err.class, ErrorClass::InvariantViolation);
assert_eq!(err.origin, ErrorOrigin::Planner);
assert!(
err.message
.contains("group-plan error conversion received non-group user variant"),
"non-group user variant mapping should fail closed with explicit domain message: {err:?}",
);
}
#[test]
fn group_plan_error_mapping_rejects_non_group_policy_variant() {
let err = from_group_plan_error(PlanError::from(PlanPolicyError::Policy(Box::new(
PolicyPlanError::UnorderedPagination,
))));
assert_eq!(err.class, ErrorClass::InvariantViolation);
assert_eq!(err.origin, ErrorOrigin::Planner);
assert!(
err.message
.contains("group-plan error conversion received non-group policy variant"),
"non-group policy variant mapping should fail closed with explicit domain message: {err:?}",
);
}
#[test]
fn group_plan_error_mapping_rejects_cursor_variant() {
let err = from_group_plan_error(PlanError::from(
CursorPlanError::ContinuationCursorWindowMismatch {
expected_offset: 8,
actual_offset: 3,
},
));
assert_eq!(err.class, ErrorClass::InvariantViolation);
assert_eq!(err.origin, ErrorOrigin::Planner);
assert!(
err.message
.contains("group-plan error conversion received cursor variant"),
"cursor variant mapping should fail closed with explicit domain message: {err:?}",
);
}
#[test]
fn cursor_plan_error_mapping_classifies_invalid_payload_as_unsupported() {
let err = CursorPlanError::InvalidContinuationCursorPayload {
reason: "bad payload".to_string(),
}
.into_internal_error();
assert_eq!(err.class, ErrorClass::Unsupported);
assert_eq!(err.origin, ErrorOrigin::Cursor);
assert!(err.message.contains("invalid continuation cursor"));
}
#[test]
fn cursor_plan_error_mapping_classifies_signature_mismatch_as_unsupported() {
let err = CursorPlanError::ContinuationCursorSignatureMismatch {
entity_path: "tests::Entity",
expected: "aa".to_string(),
actual: "bb".to_string(),
}
.into_internal_error();
assert_eq!(err.class, ErrorClass::Unsupported);
assert_eq!(err.origin, ErrorOrigin::Cursor);
}
#[test]
fn cursor_plan_error_mapping_keeps_invariant_violation_class() {
let err = CursorPlanError::ContinuationCursorInvariantViolation {
reason: "runtime cursor contract violated".to_string(),
}
.into_internal_error();
assert_eq!(err.class, ErrorClass::InvariantViolation);
assert_eq!(err.origin, ErrorOrigin::Cursor);
assert!(
err.message.starts_with("executor invariant violated:"),
"cursor invariant mapping must add the canonical executor prefix once",
);
assert!(err.message.contains("runtime cursor contract violated"));
}
#[test]
fn classification_integrity_helpers_preserve_error_class() {
let classes = [
ErrorClass::Corruption,
ErrorClass::IncompatiblePersistedFormat,
ErrorClass::NotFound,
ErrorClass::Internal,
ErrorClass::Conflict,
ErrorClass::Unsupported,
ErrorClass::InvariantViolation,
];
for class in classes {
let base = InternalError::classified(class, ErrorOrigin::Query, "base");
let relabeled_message = base.with_message("updated");
let reorigined = relabeled_message.with_origin(ErrorOrigin::Store);
assert_eq!(
reorigined.class, class,
"class must be preserved across helper relabeling operations",
);
}
}
#[test]
fn classification_integrity_cursor_conversion_matrix_is_restricted() {
fn expected_class_from_cursor_variant(err: &CursorPlanError) -> ErrorClass {
match err {
CursorPlanError::InvalidContinuationCursor { .. }
| CursorPlanError::InvalidContinuationCursorPayload { .. }
| CursorPlanError::ContinuationCursorSignatureMismatch { .. }
| CursorPlanError::ContinuationCursorBoundaryArityMismatch { .. }
| CursorPlanError::ContinuationCursorWindowMismatch { .. }
| CursorPlanError::ContinuationCursorBoundaryTypeMismatch { .. }
| CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch { .. } => {
ErrorClass::Unsupported
}
CursorPlanError::ContinuationCursorInvariantViolation { .. } => {
ErrorClass::InvariantViolation
}
}
}
let cases = vec![
CursorPlanError::InvalidContinuationCursorPayload {
reason: "payload".to_string(),
},
CursorPlanError::ContinuationCursorInvariantViolation {
reason: "invariant".to_string(),
},
CursorPlanError::ContinuationCursorSignatureMismatch {
entity_path: "tests::Entity",
expected: "aabb".to_string(),
actual: "ccdd".to_string(),
},
CursorPlanError::ContinuationCursorBoundaryArityMismatch {
expected: 2,
found: 1,
},
CursorPlanError::ContinuationCursorWindowMismatch {
expected_offset: 10,
actual_offset: 2,
},
CursorPlanError::ContinuationCursorBoundaryTypeMismatch {
field: "rank".to_string(),
expected: "u64".to_string(),
value: crate::value::Value::Text("x".to_string()),
},
CursorPlanError::ContinuationCursorPrimaryKeyTypeMismatch {
field: "id".to_string(),
expected: "Ulid".to_string(),
value: Some(crate::value::Value::Text("x".to_string())),
},
];
for cursor_err in cases {
let expected_class = expected_class_from_cursor_variant(&cursor_err);
let err = cursor_err.into_internal_error();
assert_eq!(err.origin, ErrorOrigin::Cursor);
assert_eq!(
err.class, expected_class,
"cursor conversion class must remain stable for each cursor variant: {err:?}",
);
}
}
#[test]
fn classification_integrity_access_plan_conversion_stays_invariant() {
let err = AccessPlanError::InvalidKeyRange.into_internal_error();
assert_eq!(err.class, ErrorClass::InvariantViolation);
assert_eq!(err.origin, ErrorOrigin::Query);
}
#[test]
fn classification_integrity_corruption_constructors_never_downgrade() {
let corruption_cases = [
InternalError::store_corruption("store"),
InternalError::index_corruption("index"),
InternalError::serialize_corruption("serialize"),
InternalError::identity_corruption("identity"),
InternalError::index_plan_index_corruption("index-plan-index"),
InternalError::index_plan_store_corruption("index-plan-store"),
InternalError::index_plan_serialize_corruption("index-plan-serialize"),
];
for err in corruption_cases {
assert_eq!(
err.class,
ErrorClass::Corruption,
"corruption constructors must remain corruption-classed",
);
assert!(
!matches!(err.class, ErrorClass::Unsupported),
"corruption constructors must never downgrade to unsupported",
);
}
}
#[test]
fn mutation_unknown_field_message_uses_public_wording() {
let err = InternalError::mutation_structural_field_unknown("tests::User", "missing_name");
assert_eq!(err.class, ErrorClass::InvariantViolation);
assert_eq!(err.origin, ErrorOrigin::Executor);
assert_eq!(
err.message,
"mutation field not found: tests::User field=missing_name",
);
}
#[test]
fn mutation_invalid_result_message_uses_public_wording() {
let err =
InternalError::mutation_structural_after_image_invalid("tests::User", "abc123", "detail");
assert_eq!(err.class, ErrorClass::InvariantViolation);
assert_eq!(err.origin, ErrorOrigin::Executor);
assert_eq!(
err.message,
"mutation result is invalid: tests::User key=abc123 (detail)",
);
}