use super::epistemic::EpistemicError;
use super::filter::FilterError;
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum StoreError {
LeaseExpired {
store: String,
resource: String,
lease: String,
detail: String,
},
EmptyConnection,
EmptyEnvVarName,
MissingEnvVar { var: String },
PoolInit { dsn_masked: String, source: String },
InvalidIdentifier { kind: &'static str, name: String },
EmptyData { op: &'static str },
Filter(FilterError),
Epistemic(EpistemicError),
Connect { source: String },
Query { op: &'static str, source: String },
UnsupportedColumnType { column: String, pg_type: String },
Decode { column: String, pg_type: String, source: String },
TableNotResolved { table: String },
AmbiguousTable { table: String, schemas: Vec<String> },
SchemaDrift { op: &'static str, sqlstate: String, source: String },
MissingPerTenantSchemaEnv { store: String, var: String },
DeclaredVsLiveDrift { store: String, drift: String },
}
impl fmt::Display for StoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StoreError::LeaseExpired {
store,
resource,
lease,
detail,
} => write!(
f,
"CT-2 ANCHOR BREACH — axonstore `{store}` was used, but the `lease {lease}` over \
its resource `{resource}` is no longer held: {detail}. A lease is a τ-decaying \
affine capability: using the resource after expiry is the breach, and this is \
the moment it fires. (Until v2.67.0 a flow could not USE a resource at all, \
so this guarantee was structurally impossible to violate — and therefore \
structurally impossible to keep.)"
),
StoreError::EmptyConnection => write!(
f,
"axonstore `connection` is empty — expected a DSN or an \
`env:VARNAME` reference"
),
StoreError::EmptyEnvVarName => write!(
f,
"axonstore `connection` is the bare prefix `env:` with no \
variable name"
),
StoreError::MissingEnvVar { var } => write!(
f,
"axonstore `connection: \"env:{var}\"` — environment \
variable `{var}` is not set (or not valid UTF-8)"
),
StoreError::PoolInit { dsn_masked, source } => write!(
f,
"axonstore connection pool could not be initialised for \
`{dsn_masked}`: {source}"
),
StoreError::InvalidIdentifier { kind, name } => write!(
f,
"unsafe {kind} identifier `{name}` — must match \
[A-Za-z_][A-Za-z0-9_]* and be ≤ 63 bytes"
),
StoreError::EmptyData { op } => write!(
f,
"axonstore `{op}` was given no column data"
),
StoreError::Filter(e) => write!(f, "where-expression: {e}"),
StoreError::Epistemic(e) => write!(f, "{e}"),
StoreError::Connect { source } => {
write!(f, "axonstore could not reach the database: {source}")
}
StoreError::Query { op, source } => {
write!(f, "axonstore `{op}` SQL failed: {source}")
}
StoreError::UnsupportedColumnType { column, pg_type } => write!(
f,
"column `{column}` has Postgres type `{pg_type}`, outside \
the v1.30.0 supported catalog"
),
StoreError::Decode { column, pg_type, source } => write!(
f,
"column `{column}` (`{pg_type}`) failed to decode: {source}"
),
StoreError::TableNotResolved { table } => write!(
f,
"axonstore could not resolve table `{table}` to a \
relation in any schema of the database — verify the \
table exists in the target database (a deploy-time \
migration is the usual remedy) and that the configured \
credentials can SELECT from it; the introspection scans \
`pg_catalog` independent of `search_path`, so the table \
is genuinely absent on every schema this role can see"
),
StoreError::AmbiguousTable { table, schemas } => write!(
f,
"axonstore table `{table}` is ambiguous — it exists in \
{} schemas ({}) and the connection's `search_path` does \
not disambiguate it; either narrow the role's \
`search_path` so exactly one of the resolving schemas \
is visible, or declare the target schema explicitly on \
the `axonstore` (the `schema:` declaration, \
incl. `schema: env:VAR` per-tenant)",
schemas.len(),
schemas.join(", "),
),
StoreError::SchemaDrift { op, sqlstate, source } => write!(
f,
"axonstore `{op}` hit live schema drift (SQLSTATE \
{sqlstate}) — the cached schema is stale: {source}"
),
StoreError::MissingPerTenantSchemaEnv { store, var } => write!(
f,
"axon-T806 axonstore `{store}` declares `schema: env:{var}` \
but environment variable `{var}` is not set at deploy \
time. The per-tenant schema namespace is required to \
resolve the store's column manifest entry. Either \
export `{var}` with the SQL schema name (e.g. \
`tenant_42`), or declare the schema differently \
(inline `schema {{ … }}` block, or manifest reference \
`schema: \"qualified.name\"`). Never a silent fallback."
),
StoreError::DeclaredVsLiveDrift { store, drift } => write!(
f,
"axon-T807 axonstore `{store}` declared column schema \
disagrees with the live database: {drift}. The deploy \
fails fail-closed (D8 strengthening). Remedy: run `axon \
store introspect {store}` to refresh the manifest, run \
the missing migration on the database, or fix the \
declared `schema:` block to match the live shape."
),
}
}
}
impl std::error::Error for StoreError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
StoreError::Filter(e) => Some(e),
StoreError::Epistemic(e) => Some(e),
_ => None,
}
}
}
impl StoreError {
pub fn is_schema_drift(&self) -> bool {
matches!(self, StoreError::SchemaDrift { .. })
}
}
impl From<FilterError> for StoreError {
fn from(e: FilterError) -> Self {
StoreError::Filter(e)
}
}
impl From<EpistemicError> for StoreError {
fn from(e: EpistemicError) -> Self {
StoreError::Epistemic(e)
}
}