use crate::SchemaType;
#[derive(thiserror::Error, Debug, PartialEq)]
#[non_exhaustive]
pub enum FalkorDBError {
#[error("A required Id for parsing was not found in the schema")]
MissingSchemaId(SchemaType),
#[error(
"Could not connect to Redis Sentinel, or a critical Sentinel operation has failed: {0}"
)]
SentinelConnection(String),
#[error("Received unsupported number of sentinel masters in list, there can be only one")]
SentinelMastersCount,
#[error("This requested returned a connection error, however, we may be able to create a new connection to the server, this operation should probably be retried in a bit.")]
ConnectionDown,
#[error("An error occurred while sending the request to Redis: {0}")]
RedisError(String),
#[error("An error occurred while parsing the Redis response: {0}")]
RedisParsingError(String),
#[error("Could not parse the provided connection info: {0}")]
InvalidConnectionInfo(String),
#[error("The connection returned invalid data for this command")]
InvalidDataReceived,
#[error("The provided URL scheme points at a database provider that is currently unavailable, make sure the correct feature is enabled")]
UnavailableProvider,
#[error(
"An error occurred when dealing with reference counts or RefCells, perhaps mutual borrows?"
)]
RefCountBooBoo,
#[error("The execution plan did not adhere to usual structure, and could not be parsed")]
CorruptExecutionPlan,
#[error("Could not connect to the server with the provided address")]
NoConnection,
#[error("Attempting to use an empty connection object")]
EmptyConnection,
#[error("General parsing error: {0}")]
ParsingError(String),
#[error("Could not parse header: {0}")]
ParsingHeader(&'static str),
#[error("The id received for this label/property/relationship was unknown")]
ParsingCompactIdUnknown,
#[error("Unknown type")]
ParsingUnknownType,
#[error("Element was not of type Bool")]
ParsingBool,
#[error("Could not parse into config value, was not one of the supported types")]
ParsingConfigValue,
#[error("Element was not of type I64")]
ParsingI64,
#[error("Element was not of type F64")]
ParsingF64,
#[error("Element was not of type F32")]
ParsingF32,
#[error("Element was not of type Vec32: {0}")]
ParsingVec32(String),
#[error("Element was not of type Array")]
ParsingArray,
#[error("Element was not of type String")]
ParsingString,
#[error("Element was not of type FEdge")]
ParsingFEdge,
#[error("Element was not of type FNode")]
ParsingFNode,
#[error("Element was not of type Path")]
ParsingPath,
#[error("Element was not of type Map")]
ParsingMap,
#[error("Element was not of type FPoint")]
ParsingFPoint,
#[error("Key id was not of type i64")]
ParsingKeyIdTypeMismatch,
#[error("Type marker was not of type i64")]
ParsingTypeMarkerTypeMismatch,
#[error("Both key id and type marker were not of type i64")]
ParsingKTVTypes,
#[error("Attempting to parse an Array into a struct, but the array doesn't have the expected element count: {0}")]
ParsingArrayToStructElementCount(&'static str),
#[error("Invalid enum string variant was encountered when parsing: {0}")]
InvalidEnumType(String),
#[error("Running in a single-threaded tokio runtime! Running async operations in a blocking context will cause a panic, aborting operation")]
SingleThreadedRuntime,
#[error("No runtime detected, you are trying to run an async operation from a sync context")]
NoRuntime,
#[error("Embedded server error: {0}")]
EmbeddedServerError(String),
#[error("Timed out after {timeout:?} waiting for {operation}")]
Timeout {
operation: crate::WaitOperation,
timeout: std::time::Duration,
},
#[error(
"{constraint_type} constraint on label '{label}' properties {properties:?} failed to be enforced"
)]
ConstraintFailed {
label: String,
properties: Vec<String>,
constraint_type: crate::ConstraintType,
},
#[cfg(feature = "serde")]
#[error("Failed to deserialize via serde: {0}")]
SerdeError(String),
#[error("invalid query parameter{}: {message}", .parameter.as_deref().map(|p| format!(" '{p}'")).unwrap_or_default())]
ParamEncoding {
parameter: Option<String>,
message: String,
},
#[error("invalid index option key '{key}': {message}")]
InvalidIndexOption {
key: String,
message: String,
},
#[error("result row has no column named '{name}'")]
MissingColumn {
name: String,
},
#[error("column index {index} is out of bounds for a row with {len} column(s)")]
ColumnIndexOutOfBounds {
index: usize,
len: usize,
},
#[error("result row shape mismatch: header has {header_len} column(s) but the row has {value_len} value(s)")]
RowShapeMismatch {
header_len: usize,
value_len: usize,
},
#[error("expected a value of type {expected}, but got {got}")]
TypeError {
expected: &'static str,
got: &'static str,
},
}
impl FalkorDBError {
#[must_use]
pub fn mitigation_hint(&self) -> Option<&'static str> {
match self {
Self::SingleThreadedRuntime => Some(
"run async operations on a multi-thread Tokio runtime, e.g. \
`#[tokio::main(flavor = \"multi_thread\")]`, not the current-thread runtime",
),
Self::NoRuntime => Some(
"no Tokio runtime is running — call async APIs from inside a runtime, or use the \
synchronous client instead",
),
Self::ConnectionDown => Some(
"the connection dropped — retry the operation; the client swaps in a fresh \
connection for the next attempt",
),
Self::MissingSchemaId(_) => Some(
"the local schema cache is stale; it normally self-heals on refresh, so retry the \
query",
),
Self::UnavailableProvider => Some(
"the requested provider or read-replica route isn't available — enable the matching \
cargo feature (for example `tokio` for async, or `rustls` / `native-tls` for TLS), \
and for read-only queries make sure a read replica is configured",
),
Self::RedisError(message) | Self::EmbeddedServerError(message) => {
server_message_hint(message)
}
_ => None,
}
}
}
fn server_message_hint(message: &str) -> Option<&'static str> {
let message = message.to_ascii_lowercase();
if message.contains("invalid graph operation on empty key")
|| message.contains("key doesn't contains a graph")
|| message.contains("key doesn't contain a graph")
{
Some(
"the graph key is missing or isn't a graph — create the graph with a write query (e.g. \
`CREATE`) first, or pick a name that doesn't collide with an existing non-graph key",
)
} else if message.contains("errmsg:")
&& message.contains("line:")
&& message.contains("column:")
{
Some(
"Cypher syntax error — check the query near the reported line/column, and pass values \
with `with_param` instead of formatting them into the query string",
)
} else if message.contains("query timed out") {
Some(
"the query exceeded its timeout — raise it with `QueryBuilder::with_timeout(ms)` (or the \
batch query's `with_timeout(ms)`)",
)
} else if message.contains("wrong number of arguments for 'graph")
|| message.contains("unknown command 'graph")
{
Some(
"the server didn't accept this graph command — make sure it is FalkorDB (not plain \
Redis) and recent enough",
)
} else {
None
}
}
#[cfg(feature = "serde")]
impl serde::de::Error for FalkorDBError {
fn custom<T: std::fmt::Display>(msg: T) -> Self {
FalkorDBError::SerdeError(msg.to_string())
}
}
impl From<strum::ParseError> for FalkorDBError {
fn from(value: strum::ParseError) -> Self {
FalkorDBError::InvalidEnumType(value.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_embedded_server_error_display() {
let error = FalkorDBError::EmbeddedServerError("test error".to_string());
assert_eq!(error.to_string(), "Embedded server error: test error");
}
#[test]
fn test_embedded_server_error_debug() {
let error = FalkorDBError::EmbeddedServerError("debug test".to_string());
let debug_str = format!("{:?}", error);
assert!(debug_str.contains("EmbeddedServerError"));
assert!(debug_str.contains("debug test"));
}
#[test]
fn test_embedded_server_error_equality() {
let error1 = FalkorDBError::EmbeddedServerError("same".to_string());
let error2 = FalkorDBError::EmbeddedServerError("same".to_string());
let error3 = FalkorDBError::EmbeddedServerError("different".to_string());
assert_eq!(error1, error2);
assert_ne!(error1, error3);
}
#[test]
fn test_invalid_connection_info_error() {
let error = FalkorDBError::InvalidConnectionInfo("bad connection".to_string());
assert!(error.to_string().contains("bad connection"));
}
#[test]
fn test_redis_error() {
let error = FalkorDBError::RedisError("connection failed".to_string());
assert!(error.to_string().contains("connection failed"));
}
#[test]
fn test_error_from_strum() {
let parse_error = strum::ParseError::VariantNotFound;
let falkor_error: FalkorDBError = parse_error.into();
assert!(matches!(falkor_error, FalkorDBError::InvalidEnumType(_)));
}
#[test]
fn test_unavailable_provider_error() {
let error = FalkorDBError::UnavailableProvider;
assert!(error.to_string().contains("unavailable"));
}
#[test]
fn test_no_connection_error() {
let error = FalkorDBError::NoConnection;
assert!(error.to_string().contains("Could not connect"));
}
#[test]
fn mitigation_hint_for_recognized_variants() {
assert!(FalkorDBError::SingleThreadedRuntime
.mitigation_hint()
.unwrap()
.contains("multi-thread"));
assert!(FalkorDBError::NoRuntime
.mitigation_hint()
.unwrap()
.contains("runtime"));
assert!(FalkorDBError::ConnectionDown
.mitigation_hint()
.unwrap()
.contains("retry"));
assert!(FalkorDBError::MissingSchemaId(SchemaType::Labels)
.mitigation_hint()
.unwrap()
.contains("retry"));
assert!(FalkorDBError::UnavailableProvider
.mitigation_hint()
.unwrap()
.contains("feature"));
}
#[test]
fn mitigation_hint_recognizes_server_messages() {
let cases = [
("Invalid graph operation on empty key", "create the graph"),
("key doesn't contains a graph", "create the graph"),
("key doesn't contain a graph", "create the graph"),
(
"errMsg: syntax error line: 1, column: 5, offset: 4",
"Cypher syntax",
),
("Query timed out", "timeout"),
(
"ERR wrong number of arguments for 'GRAPH.QUERY' command",
"FalkorDB",
),
("ERR unknown command 'GRAPH.QUERY'", "FalkorDB"),
];
for (message, needle) in cases {
let hint = FalkorDBError::RedisError(message.to_string()).mitigation_hint();
assert!(
hint.is_some_and(|h| h.contains(needle)),
"{message:?} -> {hint:?} should contain {needle:?}"
);
}
assert!(FalkorDBError::EmbeddedServerError("Query timed out".into())
.mitigation_hint()
.is_some());
}
#[test]
fn mitigation_hint_is_none_for_unrecognized_errors() {
assert_eq!(
FalkorDBError::RedisError("READONLY You can't write against a replica.".into())
.mitigation_hint(),
None
);
assert_eq!(FalkorDBError::InvalidDataReceived.mitigation_hint(), None);
assert_eq!(
FalkorDBError::RedisError("ERR wrong number of arguments for 'AUTH' command".into())
.mitigation_hint(),
None
);
assert_eq!(
FalkorDBError::RedisError("errMsg: something else entirely".into()).mitigation_hint(),
None
);
}
use proptest::prelude::*;
proptest! {
#[test]
fn mitigation_hint_never_panics(message in ".*") {
let _ = FalkorDBError::RedisError(message.clone()).mitigation_hint();
let _ = FalkorDBError::EmbeddedServerError(message).mitigation_hint();
}
}
}