use crate::{
db::{
codec::cursor::CursorDecodeError,
cursor::{ContinuationSignature, TokenWireError},
},
error::InternalError,
value::Value,
};
use thiserror::Error as ThisError;
#[derive(Debug, ThisError)]
pub enum CursorPlanError {
#[error("invalid continuation cursor: {reason}")]
InvalidContinuationCursor { reason: CursorDecodeError },
#[error("invalid continuation cursor: {reason}")]
InvalidContinuationCursorPayload { reason: String },
#[error("{reason}")]
ContinuationCursorInvariantViolation { reason: String },
#[error(
"continuation cursor does not match query plan signature for '{entity_path}': expected={expected}, actual={actual}"
)]
ContinuationCursorSignatureMismatch {
entity_path: &'static str,
expected: String,
actual: String,
},
#[error("continuation cursor boundary arity mismatch: expected {expected}, found {found}")]
ContinuationCursorBoundaryArityMismatch { expected: usize, found: usize },
#[error(
"continuation cursor offset mismatch: expected {expected_offset}, found {actual_offset}"
)]
ContinuationCursorWindowMismatch {
expected_offset: u32,
actual_offset: u32,
},
#[error(
"continuation cursor boundary type mismatch for field '{field}': expected {expected}, found {value:?}"
)]
ContinuationCursorBoundaryTypeMismatch {
field: String,
expected: String,
value: Value,
},
#[error(
"continuation cursor primary key type mismatch for '{field}': expected {expected}, found {value:?}"
)]
ContinuationCursorPrimaryKeyTypeMismatch {
field: String,
expected: String,
value: Option<Value>,
},
}
impl CursorPlanError {
pub(crate) const fn cursor_requires_order_message() -> &'static str {
"cursor pagination requires an explicit ordering"
}
pub(crate) const fn cursor_requires_explicit_or_grouped_ordering_message() -> &'static str {
"cursor pagination requires explicit or grouped ordering"
}
pub(crate) const fn cursor_requires_limit_message() -> &'static str {
"cursor pagination requires a limit"
}
pub(crate) const fn cursor_requires_non_empty_order_message() -> &'static str {
"cursor pagination requires non-empty ordering"
}
pub(in crate::db) const fn invalid_continuation_cursor(reason: CursorDecodeError) -> Self {
Self::InvalidContinuationCursor { reason }
}
pub(in crate::db) fn invalid_continuation_cursor_payload(reason: impl Into<String>) -> Self {
Self::InvalidContinuationCursorPayload {
reason: reason.into(),
}
}
pub(in crate::db) fn continuation_cursor_direction_mismatch() -> Self {
Self::invalid_continuation_cursor_payload(
"continuation cursor direction does not match executable plan direction",
)
}
pub(in crate::db) fn grouped_continuation_cursor_direction_mismatch() -> Self {
Self::invalid_continuation_cursor_payload(
"grouped continuation cursor direction does not match executable plan direction",
)
}
pub(in crate::db) fn continuation_cursor_unknown_order_field(field: &str) -> Self {
Self::invalid_continuation_cursor_payload(format!("unknown order field '{field}'"))
}
pub(in crate::db) fn continuation_cursor_primary_key_tie_break_required(
pk_field: &str,
) -> Self {
Self::invalid_continuation_cursor_payload(format!(
"order specification must end with primary key '{pk_field}' as deterministic tie-break"
))
}
pub(in crate::db) fn index_range_anchor_decode_failed(reason: impl Into<String>) -> Self {
Self::invalid_continuation_cursor_payload(format!(
"index-range continuation anchor decode failed: {}",
reason.into(),
))
}
pub(in crate::db) fn index_range_anchor_canonical_encoding_mismatch() -> Self {
Self::invalid_continuation_cursor_payload(
"index-range continuation anchor canonical encoding mismatch",
)
}
pub(in crate::db) fn index_range_anchor_index_id_mismatch() -> Self {
Self::invalid_continuation_cursor_payload(
"index-range continuation anchor index id mismatch",
)
}
pub(in crate::db) fn index_range_anchor_key_namespace_mismatch() -> Self {
Self::invalid_continuation_cursor_payload(
"index-range continuation anchor key namespace mismatch",
)
}
pub(in crate::db) fn index_range_anchor_component_arity_mismatch() -> Self {
Self::invalid_continuation_cursor_payload(
"index-range continuation anchor component arity mismatch",
)
}
pub(in crate::db) fn index_range_anchor_outside_envelope() -> Self {
Self::invalid_continuation_cursor_payload(
"index-range continuation anchor is outside the original range envelope",
)
}
pub(in crate::db) fn unexpected_index_range_anchor_for_composite_plan() -> Self {
Self::invalid_continuation_cursor_payload(
"unexpected index-range continuation anchor for composite access plan",
)
}
pub(in crate::db) fn index_range_anchor_semantic_bounds_required() -> Self {
Self::invalid_continuation_cursor_payload(
"index-range continuation validation is missing semantic bounds payload",
)
}
pub(in crate::db) fn index_range_anchor_required() -> Self {
Self::invalid_continuation_cursor_payload(
"index-range continuation cursor is missing a raw-key anchor",
)
}
pub(in crate::db) fn unexpected_index_range_anchor_for_non_range_path() -> Self {
Self::invalid_continuation_cursor_payload(
"unexpected index-range continuation anchor for non-index-range access path",
)
}
pub(in crate::db) fn index_range_anchor_primary_key_decode_failed(
reason: impl std::fmt::Display,
) -> Self {
Self::invalid_continuation_cursor_payload(format!(
"index-range continuation anchor primary key decode failed: {reason}",
))
}
pub(in crate::db) fn index_range_boundary_primary_key_decode_failed(
reason: impl std::fmt::Display,
) -> Self {
Self::invalid_continuation_cursor_payload(format!(
"index-range continuation boundary primary key decode failed: {reason}",
))
}
pub(in crate::db) fn index_range_boundary_anchor_mismatch() -> Self {
Self::invalid_continuation_cursor_payload(
"index-range continuation boundary/anchor mismatch",
)
}
pub(in crate::db) fn continuation_cursor_invariant(reason: impl Into<String>) -> Self {
Self::ContinuationCursorInvariantViolation {
reason: reason.into(),
}
}
pub(in crate::db) fn cursor_requires_order() -> Self {
Self::continuation_cursor_invariant(Self::cursor_requires_order_message())
}
#[cfg(test)]
pub(in crate::db) fn cursor_requires_explicit_or_grouped_ordering() -> Self {
Self::continuation_cursor_invariant(
Self::cursor_requires_explicit_or_grouped_ordering_message(),
)
}
pub(in crate::db) fn cursor_requires_non_empty_order() -> Self {
Self::continuation_cursor_invariant(Self::cursor_requires_non_empty_order_message())
}
pub(in crate::db) fn continuation_cursor_signature_mismatch(
entity_path: &'static str,
expected: &ContinuationSignature,
actual: &ContinuationSignature,
) -> Self {
Self::ContinuationCursorSignatureMismatch {
entity_path,
expected: expected.to_string(),
actual: actual.to_string(),
}
}
pub(in crate::db) const fn continuation_cursor_boundary_arity_mismatch(
expected: usize,
found: usize,
) -> Self {
Self::ContinuationCursorBoundaryArityMismatch { expected, found }
}
pub(in crate::db) const fn continuation_cursor_window_mismatch(
expected_offset: u32,
actual_offset: u32,
) -> Self {
Self::ContinuationCursorWindowMismatch {
expected_offset,
actual_offset,
}
}
pub(in crate::db) fn continuation_cursor_boundary_type_mismatch(
field: impl Into<String>,
expected: impl Into<String>,
value: Value,
) -> Self {
Self::ContinuationCursorBoundaryTypeMismatch {
field: field.into(),
expected: expected.into(),
value,
}
}
pub(in crate::db) fn continuation_cursor_primary_key_type_mismatch(
field: impl Into<String>,
expected: impl Into<String>,
value: Option<Value>,
) -> Self {
Self::ContinuationCursorPrimaryKeyTypeMismatch {
field: field.into(),
expected: expected.into(),
value,
}
}
pub(in crate::db) fn from_token_wire_error(err: TokenWireError) -> Self {
match err {
TokenWireError::Encode(message) | TokenWireError::Decode(message) => {
Self::invalid_continuation_cursor_payload(message)
}
}
}
#[cfg(test)]
pub(in crate::db) fn into_pk_cursor_decode_internal_error(self) -> InternalError {
match self {
Self::InvalidContinuationCursor { reason } => InternalError::cursor_executor_invariant(
format!("pk cursor decode rejected invalid continuation cursor: {reason}"),
),
Self::InvalidContinuationCursorPayload { reason } => {
InternalError::cursor_executor_invariant(format!(
"pk cursor decode rejected invalid continuation payload: {reason}"
))
}
Self::ContinuationCursorSignatureMismatch { .. } => {
InternalError::cursor_executor_invariant(
"pk cursor decode encountered continuation signature mismatch",
)
}
Self::ContinuationCursorBoundaryArityMismatch { expected, found } => {
InternalError::cursor_executor_invariant(format!(
"pk cursor boundary arity mismatch: expected {expected}, found {found}"
))
}
Self::ContinuationCursorWindowMismatch {
expected_offset,
actual_offset,
} => InternalError::cursor_executor_invariant(format!(
"pk cursor window mismatch: expected_offset={expected_offset}, actual_offset={actual_offset}"
)),
Self::ContinuationCursorBoundaryTypeMismatch { field, .. } => {
InternalError::cursor_executor_invariant(format!(
"pk cursor boundary type mismatch on field '{field}'"
))
}
Self::ContinuationCursorPrimaryKeyTypeMismatch { value: None, .. } => {
InternalError::cursor_executor_invariant("pk cursor slot must be present")
}
Self::ContinuationCursorPrimaryKeyTypeMismatch { value: Some(_), .. } => {
InternalError::cursor_executor_invariant("pk cursor slot type mismatch")
}
Self::ContinuationCursorInvariantViolation { reason } => {
InternalError::cursor_executor_invariant(reason)
}
}
}
pub(crate) fn into_internal_error(self) -> InternalError {
match self {
Self::ContinuationCursorInvariantViolation { reason } => {
InternalError::cursor_executor_invariant(reason)
}
Self::InvalidContinuationCursor { .. }
| Self::InvalidContinuationCursorPayload { .. }
| Self::ContinuationCursorSignatureMismatch { .. }
| Self::ContinuationCursorBoundaryArityMismatch { .. }
| Self::ContinuationCursorWindowMismatch { .. }
| Self::ContinuationCursorBoundaryTypeMismatch { .. }
| Self::ContinuationCursorPrimaryKeyTypeMismatch { .. } => {
InternalError::cursor_unsupported(self.to_string())
}
}
}
}