use std::collections::{BTreeMap, HashMap};
use std::fmt;
use std::hash::{Hash, Hasher};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NodeId(pub u64);
impl fmt::Display for NodeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "NodeId({})", self.0)
}
}
impl NodeId {
pub fn to_be_bytes(self) -> [u8; 8] {
self.0.to_be_bytes()
}
pub fn from_be_bytes(bytes: [u8; 8]) -> Self {
Self(u64::from_be_bytes(bytes))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(missing_docs)]
pub enum Value {
Null,
Bool(bool),
I64(i64),
F64(f64),
String(String),
List(Vec<Value>),
Node(Node),
Edge(Edge),
Path(PathValue),
Map(BTreeMap<String, Value>),
Date(crate::temporal::CypherDate),
LocalTime(crate::temporal::CypherLocalTime),
Time(crate::temporal::CypherTime),
LocalDateTime(crate::temporal::CypherLocalDateTime),
DateTime(crate::temporal::CypherDateTime),
Duration(crate::temporal::CypherDuration),
}
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Value::Null, Value::Null) => true,
(Value::Bool(a), Value::Bool(b)) => a == b,
(Value::I64(a), Value::I64(b)) => a == b,
(Value::F64(a), Value::F64(b)) => a.to_bits() == b.to_bits(),
(Value::String(a), Value::String(b)) => a == b,
(Value::List(a), Value::List(b)) => a == b,
(Value::Node(a), Value::Node(b)) => a == b,
(Value::Edge(a), Value::Edge(b)) => a == b,
(Value::Path(a), Value::Path(b)) => a == b,
(Value::Map(a), Value::Map(b)) => a == b,
(Value::Date(a), Value::Date(b)) => a == b,
(Value::LocalTime(a), Value::LocalTime(b)) => a == b,
(Value::Time(a), Value::Time(b)) => a == b,
(Value::LocalDateTime(a), Value::LocalDateTime(b)) => a == b,
(Value::DateTime(a), Value::DateTime(b)) => a == b,
(Value::Duration(a), Value::Duration(b)) => a == b,
_ => false,
}
}
}
impl Eq for Value {}
fn hash_properties<H: Hasher>(props: &Properties, state: &mut H) {
let mut entries: Vec<(&String, &Value)> = props.iter().collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
entries.len().hash(state);
for (k, v) in entries {
k.hash(state);
v.hash(state);
}
}
fn hash_node<H: Hasher>(n: &Node, state: &mut H) {
n.id.hash(state);
n.labels.hash(state);
hash_properties(&n.properties, state);
}
fn hash_edge<H: Hasher>(e: &Edge, state: &mut H) {
e.src.hash(state);
e.dst.hash(state);
e.label.hash(state);
hash_properties(&e.properties, state);
}
impl Hash for Value {
fn hash<H: Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
match self {
Value::Null => {}
Value::Bool(b) => b.hash(state),
Value::I64(n) => n.hash(state),
Value::F64(f) => f.to_bits().hash(state),
Value::String(s) => s.hash(state),
Value::List(items) => items.hash(state),
Value::Node(n) => hash_node(n, state),
Value::Edge(e) => hash_edge(e, state),
Value::Path(p) => {
p.nodes.len().hash(state);
for n in &p.nodes {
hash_node(n, state);
}
p.edges.len().hash(state);
for e in &p.edges {
hash_edge(e, state);
}
}
Value::Map(map) => {
for (k, v) in map {
k.hash(state);
v.hash(state);
}
}
Value::Date(d) => d.hash(state),
Value::LocalTime(t) => t.hash(state),
Value::Time(t) => t.hash(state),
Value::LocalDateTime(dt) => dt.hash(state),
Value::DateTime(dt) => dt.hash(state),
Value::Duration(d) => d.hash(state),
}
}
}
fn fmt_properties(props: &Properties, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut entries: Vec<(&String, &Value)> = props.iter().collect();
entries.sort_by(|a, b| a.0.cmp(b.0));
for (i, (k, v)) in entries.into_iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{k}: {v}")?;
}
Ok(())
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::Null => write!(f, "null"),
Value::Bool(b) => write!(f, "{b}"),
Value::I64(n) => write!(f, "{n}"),
Value::F64(n) => write!(f, "{n}"),
Value::String(s) => write!(f, "\"{s}\""),
Value::List(items) => {
write!(f, "[")?;
for (i, v) in items.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{v}")?;
}
write!(f, "]")
}
Value::Node(n) => {
write!(f, "(")?;
for lbl in &n.labels {
write!(f, ":{lbl}")?;
}
if !n.properties.is_empty() {
write!(f, " {{")?;
fmt_properties(&n.properties, f)?;
write!(f, "}}")?;
}
write!(f, ")")
}
Value::Edge(e) => {
write!(f, "[:{} {{", e.label)?;
fmt_properties(&e.properties, f)?;
write!(f, "}}]")
}
Value::Path(p) => {
write!(f, "<")?;
if let Some(first) = p.nodes.first() {
write!(f, "(")?;
for lbl in &first.labels {
write!(f, ":{lbl}")?;
}
if !first.properties.is_empty() {
if first.labels.is_empty() {
write!(f, "{{")?;
} else {
write!(f, " {{")?;
}
fmt_properties(&first.properties, f)?;
write!(f, "}}")?;
}
write!(f, ")")?;
}
for (i, (edge, node)) in p.edges.iter().zip(p.nodes.iter().skip(1)).enumerate() {
let prev_node = &p.nodes[i];
let forward = prev_node.id == edge.src;
if forward {
write!(f, "-[:{}", edge.label)?;
} else {
write!(f, "<-[:{}", edge.label)?;
}
if !edge.properties.is_empty() {
write!(f, " {{")?;
fmt_properties(&edge.properties, f)?;
write!(f, "}}")?;
}
if forward {
write!(f, "]->(")?;
} else {
write!(f, "]-(")?;
}
for lbl in &node.labels {
write!(f, ":{lbl}")?;
}
if !node.properties.is_empty() {
if node.labels.is_empty() {
write!(f, "{{")?;
} else {
write!(f, " {{")?;
}
fmt_properties(&node.properties, f)?;
write!(f, "}}")?;
}
write!(f, ")")?;
}
write!(f, ">")
}
Value::Map(map) => {
write!(f, "{{")?;
for (i, (k, v)) in map.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{k}: {v}")?;
}
write!(f, "}}")
}
Value::Date(d) => write!(f, "{d}"),
Value::LocalTime(t) => write!(f, "{t}"),
Value::Time(t) => write!(f, "{t}"),
Value::LocalDateTime(dt) => write!(f, "{dt}"),
Value::DateTime(dt) => write!(f, "{dt}"),
Value::Duration(d) => write!(f, "{d}"),
}
}
}
pub type Properties = HashMap<String, Value>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[allow(missing_docs)]
pub struct Node {
pub id: NodeId,
#[serde(default, alias = "label", deserialize_with = "deserialize_labels")]
pub labels: Vec<String>,
pub properties: Properties,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[allow(missing_docs)]
pub struct Edge {
pub src: NodeId,
pub dst: NodeId,
pub label: String,
pub properties: Properties,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[allow(missing_docs)]
pub struct PathValue {
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
}
impl PathValue {
pub fn single(node: Node) -> Self {
Self {
nodes: vec![node],
edges: Vec::new(),
}
}
pub fn len(&self) -> usize {
self.edges.len()
}
pub fn is_empty(&self) -> bool {
self.edges.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum Direction {
Outgoing,
Incoming,
Both,
}
#[derive(Serialize, Deserialize)]
pub(crate) struct NodeRecord {
#[serde(default, alias = "label", deserialize_with = "deserialize_labels")]
pub labels: Vec<String>,
pub properties: Properties,
}
fn deserialize_labels<'de, D>(deserializer: D) -> std::result::Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de;
struct LabelsVisitor;
impl<'de> de::Visitor<'de> for LabelsVisitor {
type Value = Vec<String>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a string or list of strings")
}
fn visit_str<E: de::Error>(self, v: &str) -> std::result::Result<Vec<String>, E> {
if v.is_empty() {
Ok(Vec::new())
} else {
Ok(vec![v.to_string()])
}
}
fn visit_string<E: de::Error>(self, v: String) -> std::result::Result<Vec<String>, E> {
if v.is_empty() {
Ok(Vec::new())
} else {
Ok(vec![v])
}
}
fn visit_seq<A: de::SeqAccess<'de>>(
self,
mut seq: A,
) -> std::result::Result<Vec<String>, A::Error> {
let mut labels = Vec::new();
while let Some(s) = seq.next_element()? {
labels.push(s);
}
Ok(labels)
}
}
deserializer.deserialize_any(LabelsVisitor)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueryPhase {
Parse,
SemanticAnalysis,
Runtime,
}
impl fmt::Display for QueryPhase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
QueryPhase::Parse => write!(f, "parse"),
QueryPhase::SemanticAnalysis => write!(f, "semantic analysis"),
QueryPhase::Runtime => write!(f, "runtime"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(missing_docs)]
pub struct Span {
pub start: usize,
pub end: usize,
pub line: u32,
pub col: u32,
}
impl Span {
pub const fn synthetic() -> Self {
Self {
start: 0,
end: 0,
line: 0,
col: 0,
}
}
pub fn is_synthetic(&self) -> bool {
self.line == 0 && self.col == 0 && self.start == 0 && self.end == 0
}
}
impl fmt::Display for Span {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "line {}:{}", self.line, self.col)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum ErrorCode {
Other,
AmbiguousAggregationExpression,
CreatingVarLength,
DeleteConnectedNode,
DeletedEntityAccess,
DifferentColumnsInUnion,
InvalidAggregation,
InvalidArgumentPassingMode,
InvalidArgumentType,
InvalidArgumentValue,
InvalidClauseComposition,
InvalidDelete,
InvalidNumberOfArguments,
InvalidPropertyType,
InvalidUnicodeLiteral,
MapElementAccessByNonString,
MergeReadOwnWrites,
MissingParameter,
NegativeIntegerArgument,
NoExpressionAlias,
NoSingleRelationshipType,
NonConstantExpression,
NumberOutOfRange,
ProcedureNotFound,
RequiresDirectedRelationship,
UndefinedVariable,
UnknownFunction,
UnexpectedSyntax,
VariableAlreadyBound,
VariableTypeConflict,
}
impl ErrorCode {
pub fn as_str(&self) -> &'static str {
match self {
ErrorCode::Other => "Other",
ErrorCode::AmbiguousAggregationExpression => "AmbiguousAggregationExpression",
ErrorCode::CreatingVarLength => "CreatingVarLength",
ErrorCode::DeleteConnectedNode => "DeleteConnectedNode",
ErrorCode::DeletedEntityAccess => "DeletedEntityAccess",
ErrorCode::DifferentColumnsInUnion => "DifferentColumnsInUnion",
ErrorCode::InvalidAggregation => "InvalidAggregation",
ErrorCode::InvalidArgumentPassingMode => "InvalidArgumentPassingMode",
ErrorCode::InvalidArgumentType => "InvalidArgumentType",
ErrorCode::InvalidArgumentValue => "InvalidArgumentValue",
ErrorCode::InvalidClauseComposition => "InvalidClauseComposition",
ErrorCode::InvalidDelete => "InvalidDelete",
ErrorCode::InvalidNumberOfArguments => "InvalidNumberOfArguments",
ErrorCode::InvalidPropertyType => "InvalidPropertyType",
ErrorCode::InvalidUnicodeLiteral => "InvalidUnicodeLiteral",
ErrorCode::MapElementAccessByNonString => "MapElementAccessByNonString",
ErrorCode::MergeReadOwnWrites => "MergeReadOwnWrites",
ErrorCode::MissingParameter => "MissingParameter",
ErrorCode::NegativeIntegerArgument => "NegativeIntegerArgument",
ErrorCode::NoExpressionAlias => "NoExpressionAlias",
ErrorCode::NoSingleRelationshipType => "NoSingleRelationshipType",
ErrorCode::NonConstantExpression => "NonConstantExpression",
ErrorCode::NumberOutOfRange => "NumberOutOfRange",
ErrorCode::ProcedureNotFound => "ProcedureNotFound",
ErrorCode::RequiresDirectedRelationship => "RequiresDirectedRelationship",
ErrorCode::UndefinedVariable => "UndefinedVariable",
ErrorCode::UnknownFunction => "UnknownFunction",
ErrorCode::UnexpectedSyntax => "UnexpectedSyntax",
ErrorCode::VariableAlreadyBound => "VariableAlreadyBound",
ErrorCode::VariableTypeConflict => "VariableTypeConflict",
}
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum QueryError {
SyntaxError {
phase: QueryPhase,
code: ErrorCode,
message: String,
hint: Option<String>,
span: Option<Span>,
},
TypeError {
phase: QueryPhase,
code: ErrorCode,
message: String,
hint: Option<String>,
span: Option<Span>,
},
SemanticError {
phase: QueryPhase,
code: ErrorCode,
message: String,
hint: Option<String>,
span: Option<Span>,
},
EntityNotFound {
phase: QueryPhase,
code: ErrorCode,
message: String,
hint: Option<String>,
span: Option<Span>,
},
ArgumentError {
phase: QueryPhase,
code: ErrorCode,
message: String,
hint: Option<String>,
span: Option<Span>,
},
ArithmeticError {
phase: QueryPhase,
code: ErrorCode,
message: String,
hint: Option<String>,
span: Option<Span>,
},
ConstraintViolation {
phase: QueryPhase,
code: ErrorCode,
message: String,
hint: Option<String>,
span: Option<Span>,
},
ProcedureError {
phase: QueryPhase,
code: ErrorCode,
message: String,
hint: Option<String>,
span: Option<Span>,
},
}
macro_rules! query_error_fields {
($self:expr) => {
match $self {
QueryError::SyntaxError {
phase,
code,
message,
hint,
span,
}
| QueryError::TypeError {
phase,
code,
message,
hint,
span,
}
| QueryError::SemanticError {
phase,
code,
message,
hint,
span,
}
| QueryError::EntityNotFound {
phase,
code,
message,
hint,
span,
}
| QueryError::ArgumentError {
phase,
code,
message,
hint,
span,
}
| QueryError::ArithmeticError {
phase,
code,
message,
hint,
span,
}
| QueryError::ConstraintViolation {
phase,
code,
message,
hint,
span,
}
| QueryError::ProcedureError {
phase,
code,
message,
hint,
span,
} => (phase, code, message, hint, span),
}
};
}
impl QueryError {
pub fn phase(&self) -> QueryPhase {
let (phase, _, _, _, _) = query_error_fields!(self);
*phase
}
pub fn kind(&self) -> &'static str {
match self {
QueryError::SyntaxError { .. } => "SyntaxError",
QueryError::TypeError { .. } => "TypeError",
QueryError::SemanticError { .. } => "SemanticError",
QueryError::EntityNotFound { .. } => "EntityNotFound",
QueryError::ArgumentError { .. } => "ArgumentError",
QueryError::ArithmeticError { .. } => "ArithmeticError",
QueryError::ConstraintViolation { .. } => "ConstraintViolation",
QueryError::ProcedureError { .. } => "ProcedureError",
}
}
pub fn code(&self) -> ErrorCode {
let (_, code, _, _, _) = query_error_fields!(self);
*code
}
pub fn message(&self) -> &str {
let (_, _, message, _, _) = query_error_fields!(self);
message.as_str()
}
pub fn hint(&self) -> Option<&str> {
let (_, _, _, hint, _) = query_error_fields!(self);
hint.as_deref()
}
pub fn span(&self) -> Option<Span> {
let (_, _, _, _, span) = query_error_fields!(self);
*span
}
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
let (_, _, _, hint_field, _) = query_error_fields!(&mut self);
*hint_field = Some(hint.into());
self
}
pub fn with_span(mut self, new_span: Span) -> Self {
if new_span.is_synthetic() {
return self;
}
let (_, _, _, _, span_field) = query_error_fields!(&mut self);
*span_field = Some(new_span);
self
}
pub fn with_code(mut self, new_code: ErrorCode) -> Self {
let (_, code_field, _, _, _) = query_error_fields!(&mut self);
*code_field = new_code;
self
}
}
impl fmt::Display for QueryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (phase, code, message, hint, span) = query_error_fields!(self);
match span {
Some(s) => write!(f, "{}({}) at {}: {}", self.kind(), code, s, message)?,
None => write!(f, "{}({}) at {}: {}", self.kind(), code, phase, message)?,
}
if let Some(h) = hint {
write!(f, "\n hint: {h}")?;
}
Ok(())
}
}
impl std::error::Error for QueryError {}
#[derive(Debug)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum GraphError {
Storage {
source: rusqlite::Error,
hint: Option<String>,
},
Serialization {
context: String,
source: String,
hint: Option<String>,
},
Query(QueryError),
NodeNotFound {
id: NodeId,
hint: Option<String>,
},
EdgeNotFound {
src: NodeId,
label: String,
dst: NodeId,
hint: Option<String>,
},
HasEdges {
id: NodeId,
hint: Option<String>,
},
Transaction {
message: String,
hint: Option<String>,
},
IndexAlreadyExists {
label: String,
properties: Vec<String>,
hint: Option<String>,
},
IndexNotFound {
label: String,
properties: Vec<String>,
hint: Option<String>,
},
InvalidIndexDefinition {
reason: String,
hint: Option<String>,
},
InvalidName {
name: String,
hint: Option<String>,
},
SizeLimit {
what: String,
limit: usize,
actual: usize,
hint: Option<String>,
},
SchemaMismatch {
found: u64,
supported: u64,
hint: Option<String>,
},
}
impl fmt::Display for GraphError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GraphError::Storage { source, hint } => {
write!(f, "storage error: {source}")?;
fmt_hint(f, hint.as_deref())
}
GraphError::Serialization {
context,
source,
hint,
} => {
write!(f, "serialization error ({context}): {source}")?;
fmt_hint(f, hint.as_deref())
}
GraphError::Query(q) => q.fmt(f),
GraphError::NodeNotFound { id, hint } => {
write!(f, "node not found: {id}")?;
fmt_hint(f, hint.as_deref())
}
GraphError::EdgeNotFound {
src,
label,
dst,
hint,
} => {
write!(f, "edge not found: {src} -[:{label}]-> {dst}")?;
fmt_hint(f, hint.as_deref())
}
GraphError::HasEdges { id, hint } => {
write!(
f,
"cannot delete node {id} because it still has edges; use DETACH DELETE to remove edges too"
)?;
fmt_hint(f, hint.as_deref())
}
GraphError::Transaction { message, hint } => {
write!(f, "transaction error: {message}")?;
fmt_hint(f, hint.as_deref())
}
GraphError::IndexAlreadyExists {
label,
properties,
hint,
} => {
write!(
f,
"index already exists: :{label}({})",
properties.join(", ")
)?;
fmt_hint(f, hint.as_deref())
}
GraphError::IndexNotFound {
label,
properties,
hint,
} => {
write!(f, "index not found: :{label}({})", properties.join(", "))?;
fmt_hint(f, hint.as_deref())
}
GraphError::InvalidIndexDefinition { reason, hint } => {
write!(f, "invalid index definition: {reason}")?;
fmt_hint(f, hint.as_deref())
}
GraphError::InvalidName { name, hint } => {
write!(
f,
"invalid name '{name}': must contain only ASCII letters, digits, or underscores"
)?;
fmt_hint(f, hint.as_deref())
}
GraphError::SizeLimit {
what,
limit,
actual,
hint,
} => {
write!(
f,
"{what} ({actual} bytes) exceeds maximum of {limit} bytes"
)?;
fmt_hint(f, hint.as_deref())
}
GraphError::SchemaMismatch {
found,
supported,
hint,
} => {
write!(
f,
"schema version mismatch: database is v{found}, this library supports up to v{supported}"
)?;
fmt_hint(f, hint.as_deref())
}
}
}
}
fn fmt_hint(f: &mut fmt::Formatter<'_>, hint: Option<&str>) -> fmt::Result {
if let Some(h) = hint {
write!(f, "\n hint: {h}")?;
}
Ok(())
}
impl std::error::Error for GraphError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
GraphError::Storage { source, .. } => Some(source),
GraphError::Query(q) => Some(q),
_ => None,
}
}
}
impl From<rusqlite::Error> for GraphError {
fn from(source: rusqlite::Error) -> Self {
GraphError::Storage { source, hint: None }
}
}
impl From<QueryError> for GraphError {
fn from(error: QueryError) -> Self {
GraphError::Query(error)
}
}
impl GraphError {
pub fn syntax(message: impl Into<String>) -> Self {
GraphError::Query(QueryError::SyntaxError {
phase: QueryPhase::Parse,
code: ErrorCode::Other,
message: message.into(),
hint: None,
span: None,
})
}
pub fn semantic(message: impl Into<String>) -> Self {
GraphError::Query(QueryError::SemanticError {
phase: QueryPhase::SemanticAnalysis,
code: ErrorCode::Other,
message: message.into(),
hint: None,
span: None,
})
}
pub fn constraint(message: impl Into<String>) -> Self {
GraphError::Query(QueryError::ConstraintViolation {
phase: QueryPhase::Runtime,
code: ErrorCode::Other,
message: message.into(),
hint: None,
span: None,
})
}
pub fn type_error(phase: QueryPhase, message: impl Into<String>) -> Self {
GraphError::Query(QueryError::TypeError {
phase,
code: ErrorCode::Other,
message: message.into(),
hint: None,
span: None,
})
}
pub fn argument(phase: QueryPhase, message: impl Into<String>) -> Self {
GraphError::Query(QueryError::ArgumentError {
phase,
code: ErrorCode::Other,
message: message.into(),
hint: None,
span: None,
})
}
pub fn undefined_variable(name: impl fmt::Display) -> Self {
GraphError::Query(QueryError::SemanticError {
phase: QueryPhase::SemanticAnalysis,
code: ErrorCode::UndefinedVariable,
message: format!("variable `{name}` is not defined"),
hint: None,
span: None,
})
}
pub fn procedure_not_found(name: impl fmt::Display) -> Self {
GraphError::Query(QueryError::ProcedureError {
phase: QueryPhase::SemanticAnalysis,
code: ErrorCode::ProcedureNotFound,
message: format!("unknown procedure `{name}`"),
hint: None,
span: None,
})
}
pub fn invalid_argument_type(
phase: QueryPhase,
function: impl fmt::Display,
got: impl fmt::Display,
) -> Self {
GraphError::Query(QueryError::TypeError {
phase,
code: ErrorCode::InvalidArgumentType,
message: format!("{got} is not a valid argument type for {function}"),
hint: None,
span: None,
})
}
pub fn invalid_argument_value(
phase: QueryPhase,
function: impl fmt::Display,
message: impl fmt::Display,
) -> Self {
GraphError::Query(QueryError::ArgumentError {
phase,
code: ErrorCode::InvalidArgumentValue,
message: format!("{function}: {message}"),
hint: None,
span: None,
})
}
pub fn number_out_of_range(phase: QueryPhase, message: impl Into<String>) -> Self {
GraphError::Query(QueryError::ArithmeticError {
phase,
code: ErrorCode::NumberOutOfRange,
message: message.into(),
hint: None,
span: None,
})
}
pub fn query(phase: QueryPhase, code: ErrorCode, message: impl Into<String>) -> Self {
let message = message.into();
let mk = |kind: fn(QueryPhase, ErrorCode, String) -> QueryError| -> Self {
GraphError::Query(kind(phase, code, message))
};
match code {
ErrorCode::InvalidUnicodeLiteral
| ErrorCode::InvalidClauseComposition
| ErrorCode::UnexpectedSyntax => mk(QueryError::syntax_with),
ErrorCode::InvalidArgumentType
| ErrorCode::InvalidPropertyType
| ErrorCode::MapElementAccessByNonString
| ErrorCode::DeletedEntityAccess => mk(QueryError::type_with),
ErrorCode::UndefinedVariable
| ErrorCode::VariableAlreadyBound
| ErrorCode::VariableTypeConflict
| ErrorCode::AmbiguousAggregationExpression
| ErrorCode::CreatingVarLength
| ErrorCode::DifferentColumnsInUnion
| ErrorCode::InvalidAggregation
| ErrorCode::InvalidArgumentPassingMode
| ErrorCode::InvalidArgumentValue
| ErrorCode::InvalidDelete
| ErrorCode::InvalidNumberOfArguments
| ErrorCode::NegativeIntegerArgument
| ErrorCode::NoExpressionAlias
| ErrorCode::NoSingleRelationshipType
| ErrorCode::NonConstantExpression
| ErrorCode::RequiresDirectedRelationship => mk(QueryError::semantic_with),
ErrorCode::MissingParameter => mk(QueryError::argument_with),
ErrorCode::NumberOutOfRange => mk(QueryError::arithmetic_with),
ErrorCode::DeleteConnectedNode | ErrorCode::MergeReadOwnWrites => {
mk(QueryError::constraint_with)
}
ErrorCode::ProcedureNotFound => mk(QueryError::procedure_with),
ErrorCode::UnknownFunction => mk(QueryError::syntax_with),
ErrorCode::Other => mk(QueryError::semantic_with),
}
}
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
match &mut self {
GraphError::Query(q) => {
let (_, _, _, h, _) = query_error_fields!(q);
*h = Some(hint.into());
}
GraphError::Storage { hint: h, .. }
| GraphError::Serialization { hint: h, .. }
| GraphError::NodeNotFound { hint: h, .. }
| GraphError::EdgeNotFound { hint: h, .. }
| GraphError::HasEdges { hint: h, .. }
| GraphError::Transaction { hint: h, .. }
| GraphError::IndexAlreadyExists { hint: h, .. }
| GraphError::IndexNotFound { hint: h, .. }
| GraphError::InvalidIndexDefinition { hint: h, .. }
| GraphError::InvalidName { hint: h, .. }
| GraphError::SizeLimit { hint: h, .. }
| GraphError::SchemaMismatch { hint: h, .. } => *h = Some(hint.into()),
}
self
}
pub fn with_code(mut self, code: ErrorCode) -> Self {
if let GraphError::Query(q) = &mut self {
let (_, c, _, _, _) = query_error_fields!(q);
*c = code;
}
self
}
pub fn with_span(self, span: Span) -> Self {
if span.is_synthetic() {
return self;
}
match self {
GraphError::Query(q) => GraphError::Query(q.with_span(span)),
other => other,
}
}
pub fn serialization(context: impl Into<String>, source: impl fmt::Display) -> Self {
GraphError::Serialization {
context: context.into(),
source: source.to_string(),
hint: None,
}
}
pub fn transaction(message: impl Into<String>) -> Self {
GraphError::Transaction {
message: message.into(),
hint: None,
}
}
}
impl QueryError {
fn syntax_with(phase: QueryPhase, code: ErrorCode, message: String) -> Self {
QueryError::SyntaxError {
phase,
code,
message,
hint: None,
span: None,
}
}
fn type_with(phase: QueryPhase, code: ErrorCode, message: String) -> Self {
QueryError::TypeError {
phase,
code,
message,
hint: None,
span: None,
}
}
fn semantic_with(phase: QueryPhase, code: ErrorCode, message: String) -> Self {
QueryError::SemanticError {
phase,
code,
message,
hint: None,
span: None,
}
}
fn argument_with(phase: QueryPhase, code: ErrorCode, message: String) -> Self {
QueryError::ArgumentError {
phase,
code,
message,
hint: None,
span: None,
}
}
fn arithmetic_with(phase: QueryPhase, code: ErrorCode, message: String) -> Self {
QueryError::ArithmeticError {
phase,
code,
message,
hint: None,
span: None,
}
}
fn constraint_with(phase: QueryPhase, code: ErrorCode, message: String) -> Self {
QueryError::ConstraintViolation {
phase,
code,
message,
hint: None,
span: None,
}
}
fn procedure_with(phase: QueryPhase, code: ErrorCode, message: String) -> Self {
QueryError::ProcedureError {
phase,
code,
message,
hint: None,
span: None,
}
}
}
pub type Result<T> = std::result::Result<T, GraphError>;
pub fn validate_name(name: &str) -> Result<()> {
if name.is_empty() || !name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
return Err(GraphError::InvalidName {
name: name.to_string(),
hint: None,
});
}
Ok(())
}
pub fn validate_name_length(name: &str, max_bytes: usize) -> Result<()> {
if name.len() > max_bytes {
return Err(GraphError::SizeLimit {
what: format!("name '{}...'", &name[..max_bytes.min(32)]),
limit: max_bytes,
actual: name.len(),
hint: None,
});
}
Ok(())
}
fn value_byte_size(val: &Value) -> usize {
match val {
Value::Null | Value::Bool(_) | Value::I64(_) | Value::F64(_) => 8,
Value::String(s) => s.len(),
Value::List(items) => items.iter().map(value_byte_size).sum(),
Value::Node(n) => node_byte_size(n),
Value::Edge(e) => edge_byte_size(e),
Value::Path(p) => {
p.nodes.iter().map(node_byte_size).sum::<usize>()
+ p.edges.iter().map(edge_byte_size).sum::<usize>()
}
Value::Map(map) => map.iter().map(|(k, v)| k.len() + value_byte_size(v)).sum(),
Value::Date(_) => 12,
Value::LocalTime(_) => 16,
Value::Time(_) => 20,
Value::LocalDateTime(_) => 20,
Value::DateTime(_) => 28,
Value::Duration(_) => 32,
}
}
fn node_byte_size(n: &Node) -> usize {
8 + n.labels.iter().map(|l| l.len()).sum::<usize>()
+ n.properties
.iter()
.map(|(k, v)| k.len() + value_byte_size(v))
.sum::<usize>()
}
fn edge_byte_size(e: &Edge) -> usize {
16 + e.label.len()
+ e.properties
.iter()
.map(|(k, v)| k.len() + value_byte_size(v))
.sum::<usize>()
}
pub fn validate_properties(
label: &str,
properties: &Properties,
max_name_bytes: usize,
max_value_bytes: usize,
) -> Result<()> {
validate_name_length(label, max_name_bytes)?;
for (key, val) in properties {
validate_name_length(key, max_name_bytes)?;
let size = value_byte_size(val);
if size > max_value_bytes {
return Err(GraphError::SizeLimit {
what: format!("property '{key}' value"),
limit: max_value_bytes,
actual: size,
hint: None,
});
}
}
Ok(())
}