use std::collections::HashMap;
use boltr::error::BoltError;
use boltr::types::{
BoltDate, BoltDict, BoltDuration, BoltNode, BoltPath, BoltPoint2D, BoltRelationship,
BoltUnboundRelationship, BoltValue,
};
use kglite::api::Value;
pub fn to_bolt(value: &Value) -> Result<BoltValue, BoltError> {
match value {
Value::Null => Ok(BoltValue::Null),
Value::Boolean(b) => Ok(BoltValue::Boolean(*b)),
Value::Int64(n) => Ok(BoltValue::Integer(*n)),
Value::UniqueId(n) => Ok(BoltValue::Integer(i64::from(*n))),
Value::Float64(f) => Ok(BoltValue::Float(*f)),
Value::String(s) => Ok(BoltValue::String(s.clone())),
Value::List(items) => items
.iter()
.map(to_bolt)
.collect::<Result<Vec<_>, _>>()
.map(BoltValue::List),
Value::Map(entries) => entries
.iter()
.map(|(k, v)| to_bolt(v).map(|bv| (k.clone(), bv)))
.collect::<Result<HashMap<_, _>, _>>()
.map(BoltValue::Dict),
Value::DateTime(date) => {
let epoch =
chrono::NaiveDate::from_ymd_opt(1970, 1, 1).expect("1970-01-01 is a valid date");
Ok(BoltValue::Date(BoltDate {
days: date.signed_duration_since(epoch).num_days(),
}))
}
Value::Point { lat, lon } => Ok(BoltValue::Point2D(BoltPoint2D {
srid: 4326,
x: *lon,
y: *lat,
})),
Value::Duration {
months,
days,
seconds,
} => Ok(BoltValue::Duration(BoltDuration {
months: i64::from(*months),
days: i64::from(*days),
seconds: *seconds,
nanoseconds: 0,
})),
Value::Node(node) => {
let properties = props_to_bolt_dict(&node.properties)?;
Ok(BoltValue::Node(BoltNode {
id: i64::from(node.id),
labels: node.labels.clone(),
properties,
element_id: node.id.to_string(),
}))
}
Value::Relationship(rel) => {
let properties = props_to_bolt_dict(&rel.properties)?;
Ok(BoltValue::Relationship(BoltRelationship {
id: i64::from(rel.id),
start_node_id: i64::from(rel.start_id),
end_node_id: i64::from(rel.end_id),
rel_type: rel.rel_type.clone(),
properties,
element_id: rel.id.to_string(),
start_element_id: rel.start_id.to_string(),
end_element_id: rel.end_id.to_string(),
}))
}
Value::Path(path) => path_to_bolt_path(path.as_ref()).map(BoltValue::Path),
Value::NodeRef(_) => Err(BoltError::Backend(
"internal Value::NodeRef leaked through projection — please file a \
bug against kglite (this variant is supposed to be materialized \
before reaching the Bolt boundary)"
.into(),
)),
}
}
fn props_to_bolt_dict(
props: &std::collections::BTreeMap<String, Value>,
) -> Result<BoltDict, BoltError> {
props
.iter()
.map(|(k, v)| to_bolt(v).map(|bv| (k.clone(), bv)))
.collect::<Result<HashMap<_, _>, _>>()
}
fn path_to_bolt_path(p: &kglite::api::PathValue) -> Result<BoltPath, BoltError> {
if p.nodes.len() != p.rels.len() + 1 {
return Err(BoltError::Backend(format!(
"kglite PathValue invariant violated: {} nodes vs {} rels (expected {} vs {})",
p.nodes.len(),
p.rels.len(),
p.rels.len() + 1,
p.rels.len()
)));
}
let nodes: Vec<BoltNode> = p
.nodes
.iter()
.map(|nv| {
let properties = props_to_bolt_dict(&nv.properties)?;
Ok::<BoltNode, BoltError>(BoltNode {
id: i64::from(nv.id),
labels: nv.labels.clone(),
properties,
element_id: nv.id.to_string(),
})
})
.collect::<Result<Vec<_>, _>>()?;
let rels: Vec<BoltUnboundRelationship> = p
.rels
.iter()
.map(|rv| {
let properties = props_to_bolt_dict(&rv.properties)?;
Ok::<BoltUnboundRelationship, BoltError>(BoltUnboundRelationship {
id: i64::from(rv.id),
rel_type: rv.rel_type.clone(),
properties,
element_id: rv.id.to_string(),
})
})
.collect::<Result<Vec<_>, _>>()?;
let mut indices: Vec<i64> = Vec::with_capacity(p.rels.len() * 2);
for (i, rel) in p.rels.iter().enumerate() {
let node_before = &p.nodes[i];
let node_after = &p.nodes[i + 1];
let rel_idx_1based = (i + 1) as i64;
let signed_rel: i64 = if rel.start_id == node_before.id && rel.end_id == node_after.id {
rel_idx_1based } else if rel.start_id == node_after.id && rel.end_id == node_before.id {
-rel_idx_1based } else {
tracing::warn!(
rel_id = rel.id,
rel_start = rel.start_id,
rel_end = rel.end_id,
path_node_before = node_before.id,
path_node_after = node_after.id,
"path rel doesn't connect surrounding nodes — defaulting to outgoing direction"
);
rel_idx_1based
};
indices.push(signed_rel);
indices.push((i + 1) as i64); }
Ok(BoltPath {
nodes,
rels,
indices,
})
}
pub fn from_bolt(value: &BoltValue) -> Result<Value, BoltError> {
match value {
BoltValue::Null => Ok(Value::Null),
BoltValue::Boolean(b) => Ok(Value::Boolean(*b)),
BoltValue::Integer(n) => Ok(Value::Int64(*n)),
BoltValue::Float(f) => {
if !f.is_finite() {
return Err(BoltError::Protocol(format!(
"non-finite Float parameter: {f} \
(NaN and ±Infinity not supported — typically indicates \
a client-side division-by-zero or sentinel-value bug; \
send NULL instead if the absence of a value is what \
you mean)"
)));
}
Ok(Value::Float64(*f))
}
BoltValue::String(s) => Ok(Value::String(s.clone())),
BoltValue::List(items) => items
.iter()
.map(from_bolt)
.collect::<Result<Vec<_>, _>>()
.map(Value::List),
BoltValue::Dict(entries) => entries
.iter()
.map(|(k, v)| from_bolt(v).map(|kv| (k.clone(), kv)))
.collect::<Result<std::collections::BTreeMap<_, _>, _>>()
.map(Value::Map),
BoltValue::Date(d) => {
let epoch =
chrono::NaiveDate::from_ymd_opt(1970, 1, 1).expect("1970-01-01 is a valid date");
let date = epoch
.checked_add_signed(chrono::Duration::days(d.days))
.ok_or_else(|| {
BoltError::Protocol(format!(
"Bolt Date out of range for kglite NaiveDate: days={}",
d.days
))
})?;
Ok(Value::DateTime(date))
}
BoltValue::Duration(d) => Ok(Value::Duration {
months: i32::try_from(d.months).map_err(|_| {
BoltError::Protocol(format!(
"Bolt Duration.months out of i32 range: {}",
d.months
))
})?,
days: i32::try_from(d.days).map_err(|_| {
BoltError::Protocol(format!("Bolt Duration.days out of i32 range: {}", d.days))
})?,
seconds: d.seconds,
}),
BoltValue::Point2D(p) => {
if p.srid != 4326 {
return Err(BoltError::Protocol(format!(
"Bolt Point2D with SRID {} not supported — kglite \
only represents WGS84 lat/lon (SRID 4326)",
p.srid
)));
}
Ok(Value::Point { lat: p.y, lon: p.x })
}
BoltValue::Bytes(_) => Err(BoltError::Protocol(
"Bolt Bytes parameter not supported — kglite has no byte-string Value variant".into(),
)),
BoltValue::Time(_)
| BoltValue::LocalTime(_)
| BoltValue::DateTime(_)
| BoltValue::DateTimeZoneId(_)
| BoltValue::LocalDateTime(_) => Err(BoltError::Protocol(
"Bolt time-of-day / timestamp parameters not yet supported — kglite's \
Value::DateTime is date-only (Phase A.1 deferred time precision)"
.into(),
)),
BoltValue::Point3D(_) => Err(BoltError::Protocol(
"Bolt Point3D parameter not supported — kglite represents only 2D points".into(),
)),
BoltValue::Node(_) | BoltValue::Relationship(_) | BoltValue::Path(_) => {
Err(BoltError::Protocol(
"Bolt Node/Relationship/Path is not a valid parameter type — \
drivers should serialize property values instead"
.into(),
))
}
BoltValue::UnboundRelationship(_) => Err(BoltError::Protocol(
"Bolt UnboundRelationship only appears inside Path structures — \
cannot be a standalone parameter"
.into(),
)),
}
}