use std::collections::HashMap;
use boltr::error::BoltError;
use boltr::types::{
BoltDate, BoltDateTime, BoltDict, BoltDuration, BoltLocalDateTime, BoltLocalTime, BoltNode,
BoltPath, BoltPoint2D, BoltRelationship, BoltTime, 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.to_string(), 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::Timestamp(dt) => {
let utc = dt.and_utc();
if utc.timestamp_subsec_nanos() >= 1_000_000_000 {
return Err(BoltError::Backend(
"Timestamp leap seconds cannot be represented as Bolt LocalDateTime".into(),
));
}
Ok(BoltValue::LocalDateTime(BoltLocalDateTime {
seconds: utc.timestamp(),
nanoseconds: i64::from(utc.timestamp_subsec_nanos()),
}))
}
Value::ZonedDateTime {
utc,
offset_secs,
zone,
} => {
let (seconds, nanoseconds) = kglite::api::zoned_datetime::epoch_parts(*utc);
Ok(match zone {
None => BoltValue::DateTime(BoltDateTime {
seconds,
nanoseconds: i64::from(nanoseconds),
tz_offset_seconds: i64::from(*offset_secs),
}),
Some(zone) => BoltValue::DateTimeZoneId(boltr::types::BoltDateTimeZoneId {
seconds,
nanoseconds: i64::from(nanoseconds),
tz_id: zone.name().to_string(),
}),
})
}
Value::LocalTime(nanos) => Ok(BoltValue::LocalTime(BoltLocalTime {
nanoseconds: *nanos as i64,
})),
Value::Time { nanos, offset_secs } => Ok(BoltValue::Time(BoltTime {
nanoseconds: *nanos as i64,
tz_offset_seconds: i64::from(*offset_secs),
})),
Value::Point { lat, lon } => Ok(BoltValue::Point2D(BoltPoint2D {
srid: 4326,
x: *lon,
y: *lat,
})),
Value::Duration {
months,
days,
seconds,
nanos,
} => Ok(BoltValue::Duration(BoltDuration {
months: i64::from(*months),
days: i64::from(*days),
seconds: *seconds,
nanoseconds: i64::from(*nanos),
})),
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(),
)),
}
}
pub fn to_bolt_owned(value: Value) -> Result<BoltValue, BoltError> {
match value {
Value::String(s) => Ok(BoltValue::String(s)),
Value::List(items) => items
.into_iter()
.map(to_bolt_owned)
.collect::<Result<Vec<_>, _>>()
.map(BoltValue::List),
Value::Map(entries) => props_into_bolt_dict(entries).map(BoltValue::Dict),
Value::Node(node) => {
let kglite::api::NodeValue {
id,
labels,
properties,
} = *node;
Ok(BoltValue::Node(BoltNode {
id: i64::from(id),
labels,
properties: props_into_bolt_dict(properties)?,
element_id: id.to_string(),
}))
}
Value::Relationship(rel) => {
let rel = *rel;
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),
element_id: rel.id.to_string(),
start_element_id: rel.start_id.to_string(),
end_element_id: rel.end_id.to_string(),
rel_type: rel.rel_type,
properties: props_into_bolt_dict(rel.properties)?,
}))
}
Value::Path(path) => path_into_bolt_path(*path).map(BoltValue::Path),
other => to_bolt(&other),
}
}
fn props_into_bolt_dict(props: kglite::datatypes::PropMap) -> Result<BoltDict, BoltError> {
let pairs = props.into_pairs();
let mut dict = HashMap::with_capacity(pairs.len());
for (key, value) in pairs {
dict.insert(key.to_string(), to_bolt_owned(value)?);
}
Ok(dict)
}
fn props_to_bolt_dict(props: &kglite::datatypes::PropMap) -> Result<BoltDict, BoltError> {
props
.iter()
.map(|(k, v)| to_bolt(v).map(|bv| (k.to_string(), bv)))
.collect::<Result<HashMap<_, _>, _>>()
}
fn path_to_bolt_path(p: &kglite::api::PathValue) -> Result<BoltPath, BoltError> {
let indices = path_indices(p)?;
let nodes: Vec<BoltNode> = p
.nodes
.iter()
.map(|nv| {
Ok::<BoltNode, BoltError>(BoltNode {
id: i64::from(nv.id),
labels: nv.labels.clone(),
properties: props_to_bolt_dict(&nv.properties)?,
element_id: nv.id.to_string(),
})
})
.collect::<Result<Vec<_>, _>>()?;
let rels: Vec<BoltUnboundRelationship> = p
.rels
.iter()
.map(|rv| {
Ok::<BoltUnboundRelationship, BoltError>(BoltUnboundRelationship {
id: i64::from(rv.id),
rel_type: rv.rel_type.clone(),
properties: props_to_bolt_dict(&rv.properties)?,
element_id: rv.id.to_string(),
})
})
.collect::<Result<Vec<_>, _>>()?;
Ok(BoltPath {
nodes,
rels,
indices,
})
}
fn path_into_bolt_path(p: kglite::api::PathValue) -> Result<BoltPath, BoltError> {
let indices = path_indices(&p)?;
let nodes = p
.nodes
.into_iter()
.map(|nv| {
Ok::<BoltNode, BoltError>(BoltNode {
id: i64::from(nv.id),
element_id: nv.id.to_string(),
labels: nv.labels,
properties: props_into_bolt_dict(nv.properties)?,
})
})
.collect::<Result<Vec<_>, _>>()?;
let rels = p
.rels
.into_iter()
.map(|rv| {
Ok::<BoltUnboundRelationship, BoltError>(BoltUnboundRelationship {
id: i64::from(rv.id),
element_id: rv.id.to_string(),
rel_type: rv.rel_type,
properties: props_into_bolt_dict(rv.properties)?,
})
})
.collect::<Result<Vec<_>, _>>()?;
Ok(BoltPath {
nodes,
rels,
indices,
})
}
fn path_indices(p: &kglite::api::PathValue) -> Result<Vec<i64>, 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 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 {
return Err(BoltError::Backend(format!(
"kglite PathValue relationship {} ({} -> {}) does not connect path nodes {} -> {}",
rel.id, rel.start_id, rel.end_id, node_before.id, node_after.id
)));
};
indices.push(signed_rel);
indices.push((i + 1) as i64); }
Ok(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) => 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.as_str(), kv)))
.collect::<Result<kglite::datatypes::PropMap, _>>()
.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) => {
let (bolt_seconds, bolt_nanos) = normalise_bolt_seconds(d.seconds, d.nanoseconds)?;
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: bolt_seconds,
nanos: bolt_nanos,
})
}
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::LocalDateTime(dt) => {
let stamp = u32::try_from(dt.nanoseconds)
.ok()
.filter(|nanos| *nanos < 1_000_000_000)
.and_then(|nanos| chrono::DateTime::from_timestamp(dt.seconds, nanos))
.ok_or_else(|| {
BoltError::Protocol(format!(
"Bolt LocalDateTime out of range: seconds={}, nanoseconds={}",
dt.seconds, dt.nanoseconds
))
})?;
Ok(Value::Timestamp(stamp.naive_utc()))
}
BoltValue::LocalTime(t) => {
let nanos = u64::try_from(t.nanoseconds)
.ok()
.filter(|n| kglite::api::time_of_day::is_valid_nanos(*n))
.ok_or_else(|| {
BoltError::Protocol(format!(
"Bolt LocalTime out of range: nanoseconds={}",
t.nanoseconds
))
})?;
Ok(Value::LocalTime(nanos))
}
BoltValue::Time(t) => {
let nanos = u64::try_from(t.nanoseconds)
.ok()
.filter(|n| kglite::api::time_of_day::is_valid_nanos(*n))
.ok_or_else(|| {
BoltError::Protocol(format!(
"Bolt Time out of range: nanoseconds={}",
t.nanoseconds
))
})?;
let offset_secs = i32::try_from(t.tz_offset_seconds)
.ok()
.filter(|o| o.abs() <= kglite::api::time_of_day::MAX_OFFSET_SECS)
.ok_or_else(|| {
BoltError::Protocol(format!(
"Bolt Time offset out of range: tz_offset_seconds={}",
t.tz_offset_seconds
))
})?;
Ok(Value::Time { nanos, offset_secs })
}
BoltValue::DateTime(dt) => {
let utc = u32::try_from(dt.nanoseconds)
.ok()
.filter(|nanos| *nanos < 1_000_000_000)
.and_then(|nanos| kglite::api::zoned_datetime::from_epoch(dt.seconds, nanos))
.ok_or_else(|| {
BoltError::Protocol(format!(
"Bolt DateTime out of range: seconds={}, nanoseconds={}",
dt.seconds, dt.nanoseconds
))
})?;
let offset_secs = i32::try_from(dt.tz_offset_seconds)
.ok()
.filter(|o| o.abs() <= kglite::api::time_of_day::MAX_OFFSET_SECS)
.ok_or_else(|| {
BoltError::Protocol(format!(
"Bolt DateTime offset out of range: tz_offset_seconds={}",
dt.tz_offset_seconds
))
})?;
Ok(Value::zoned(utc, offset_secs))
}
BoltValue::DateTimeZoneId(dt) => {
let utc = u32::try_from(dt.nanoseconds)
.ok()
.filter(|nanos| *nanos < 1_000_000_000)
.and_then(|nanos| kglite::api::zoned_datetime::from_epoch(dt.seconds, nanos))
.ok_or_else(|| {
BoltError::Protocol(format!(
"Bolt DateTimeZoneId out of range: seconds={}, nanoseconds={}",
dt.seconds, dt.nanoseconds
))
})?;
let zone = kglite::api::zoned_datetime::zone_by_name(&dt.tz_id)
.map_err(|e| BoltError::Protocol(format!("Bolt DateTimeZoneId: {e}")))?;
Ok(Value::zoned_in(utc, zone))
}
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(),
)),
}
}
fn normalise_bolt_seconds(seconds: i64, nanoseconds: i64) -> Result<(i64, i32), BoltError> {
const NANOS: i128 = 1_000_000_000;
let total = i128::from(seconds) * NANOS + i128::from(nanoseconds);
let whole = i64::try_from(total.div_euclid(NANOS)).map_err(|_| {
BoltError::Protocol(format!(
"Bolt Duration seconds out of i64 range: {seconds}s + {nanoseconds}ns"
))
})?;
Ok((whole, total.rem_euclid(NANOS) as i32))
}
#[cfg(test)]
mod tests {
use kglite::api::{NodeValue, PathValue, RelValue};
use super::*;
fn node(id: u32) -> NodeValue {
NodeValue {
id,
labels: vec!["N".into()],
properties: kglite::datatypes::PropMap::new(),
}
}
fn rel(id: u32, start_id: u32, end_id: u32) -> RelValue {
RelValue {
incarnation: None,
id,
start_id,
end_id,
rel_type: "R".into(),
properties: kglite::datatypes::PropMap::new(),
}
}
fn bolt_duration(seconds: i64, nanoseconds: i64) -> BoltValue {
BoltValue::Duration(BoltDuration {
months: 1,
days: 2,
seconds,
nanoseconds,
})
}
#[test]
fn duration_nanoseconds_cross_the_wire_both_ways() {
let fine = Value::Duration {
months: 1,
days: 2,
seconds: -2,
nanos: 500_000_000,
};
assert_eq!(to_bolt(&fine).unwrap(), bolt_duration(-2, 500_000_000));
assert_eq!(
to_bolt_owned(fine.clone()).unwrap(),
bolt_duration(-2, 500_000_000)
);
assert_eq!(from_bolt(&bolt_duration(-2, 500_000_000)).unwrap(), fine);
}
#[test]
fn inbound_duration_nanoseconds_are_normalised() {
for ((seconds, nanoseconds), expected) in [
((0, -1), (-1, 999_999_999)),
((1, -500_000_000), (0, 500_000_000)),
((-1, -500_000_000), (-2, 500_000_000)),
((0, 2_500_000_000), (2, 500_000_000)),
((3, 7), (3, 7)),
] {
let Ok(Value::Duration {
seconds: s,
nanos: n,
..
}) = from_bolt(&bolt_duration(seconds, nanoseconds))
else {
panic!("({seconds}, {nanoseconds}) must bind");
};
assert_eq!((s, n), expected, "({seconds}, {nanoseconds})");
}
assert!(from_bolt(&bolt_duration(i64::MAX, 1_000_000_000)).is_err());
}
#[test]
fn non_finite_float_parameters_bind_unchanged() {
for f in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -0.0] {
let Ok(Value::Float64(bound)) = from_bolt(&BoltValue::Float(f)) else {
panic!("{f} must bind as a Float64 parameter");
};
assert_eq!(bound.to_bits(), f.to_bits());
}
}
#[test]
fn path_adapter_preserves_reverse_traversal_direction() {
let path = PathValue {
nodes: vec![node(2), node(1)],
rels: vec![rel(7, 1, 2)],
};
let encoded = path_to_bolt_path(&path).expect("valid reverse path");
assert_eq!(encoded.indices, vec![-1, 1]);
}
#[test]
fn path_adapter_rejects_relationship_disconnected_from_segment() {
let path = PathValue {
nodes: vec![node(1), node(2)],
rels: vec![rel(7, 3, 4)],
};
let error = path_to_bolt_path(&path).expect_err("disconnected relationship must fail");
assert!(error
.to_string()
.contains("does not connect path nodes 1 -> 2"));
}
#[test]
fn path_adapter_rejects_non_linear_shape() {
let path = PathValue {
nodes: vec![node(1)],
rels: vec![rel(7, 1, 2)],
};
let error =
path_to_bolt_path(&path).expect_err("invalid node/relationship count must fail");
assert!(error.to_string().contains("invariant violated"));
}
}
#[cfg(test)]
mod fractional_timestamp_contract_tests {
use super::*;
#[test]
fn local_datetime_roundtrip_preserves_fraction_and_negative_epoch_floor() {
for (text, seconds, nanos) in [
("1970-01-01T00:00:00", 0, 0),
("1970-01-01T00:00:00.123456789", 0, 123456789),
("1969-12-31T23:59:59.500", -1, 500000000),
] {
let value = Value::Timestamp(
chrono::NaiveDateTime::parse_from_str(text, "%Y-%m-%dT%H:%M:%S%.f").unwrap(),
);
let bolt = BoltValue::LocalDateTime(BoltLocalDateTime {
seconds,
nanoseconds: nanos,
});
assert_eq!(to_bolt(&value).unwrap(), bolt);
assert_eq!(from_bolt(&bolt).unwrap(), value);
let nested = Value::List(vec![value]);
assert_eq!(from_bolt(&to_bolt(&nested).unwrap()).unwrap(), nested);
}
}
}
#[cfg(test)]
mod time_of_day_tests {
use super::*;
#[test]
fn local_time_and_time_round_trip_with_their_nanoseconds_and_offset() {
let local = Value::LocalTime(36_000_500_000_001);
let zoned = Value::Time {
nanos: 86_399_999_999_999,
offset_secs: -19_800,
};
assert_eq!(
to_bolt(&local).unwrap(),
BoltValue::LocalTime(BoltLocalTime {
nanoseconds: 36_000_500_000_001
})
);
assert_eq!(
to_bolt(&zoned).unwrap(),
BoltValue::Time(BoltTime {
nanoseconds: 86_399_999_999_999,
tz_offset_seconds: -19_800
})
);
for value in [local, zoned] {
let back = from_bolt(&to_bolt(&value).unwrap()).unwrap();
assert_eq!(format!("{back:?}"), format!("{value:?}"));
assert_eq!(
to_bolt_owned(value.clone()).unwrap(),
to_bolt(&value).unwrap()
);
}
let nested = Value::List(vec![Value::Time {
nanos: 1,
offset_secs: 3600,
}]);
assert_eq!(
format!("{:?}", from_bolt(&to_bolt(&nested).unwrap()).unwrap()),
format!("{nested:?}")
);
}
#[test]
fn out_of_range_inbound_times_are_protocol_errors() {
for bad in [
BoltValue::LocalTime(BoltLocalTime { nanoseconds: -1 }),
BoltValue::LocalTime(BoltLocalTime {
nanoseconds: 86_400_000_000_000,
}),
BoltValue::Time(BoltTime {
nanoseconds: 86_400_000_000_000,
tz_offset_seconds: 0,
}),
BoltValue::Time(BoltTime {
nanoseconds: 0,
tz_offset_seconds: 64_801,
}),
BoltValue::Time(BoltTime {
nanoseconds: 0,
tz_offset_seconds: i64::MAX,
}),
] {
assert!(
matches!(from_bolt(&bad), Err(BoltError::Protocol(_))),
"{bad:?}"
);
}
}
#[test]
fn zoned_datetimes_round_trip_keeping_the_instant_and_the_offset() {
let utc = chrono::DateTime::from_timestamp(1_437_511_232, 142_000_000)
.unwrap()
.naive_utc();
let value = Value::zoned(utc, 3600);
let bolt = BoltValue::DateTime(boltr::types::BoltDateTime {
seconds: 1_437_511_232,
nanoseconds: 142_000_000,
tz_offset_seconds: 3600,
});
assert_eq!(to_bolt(&value).unwrap(), bolt);
assert_eq!(to_bolt_owned(value.clone()).unwrap(), bolt);
assert_eq!(
format!("{:?}", from_bolt(&bolt).unwrap()),
format!("{value:?}")
);
let before_epoch = Value::zoned(
chrono::DateTime::from_timestamp(-1, 999_999_999)
.unwrap()
.naive_utc(),
-64_800,
);
let nested = Value::List(vec![value, before_epoch]);
assert_eq!(
format!("{:?}", from_bolt(&to_bolt(&nested).unwrap()).unwrap()),
format!("{nested:?}")
);
}
#[test]
fn named_zones_travel_as_date_time_zone_id() {
let utc = chrono::DateTime::from_timestamp(1_774_747_800, 0)
.unwrap()
.naive_utc();
let zone = kglite::api::time_zone::ZoneId::from_name("Europe/Oslo").unwrap();
let value = Value::zoned_in(utc, zone);
let bolt = BoltValue::DateTimeZoneId(boltr::types::BoltDateTimeZoneId {
seconds: 1_774_747_800,
nanoseconds: 0,
tz_id: "Europe/Oslo".into(),
});
assert_eq!(to_bolt(&value).unwrap(), bolt);
assert_eq!(to_bolt_owned(value.clone()).unwrap(), bolt);
let back = from_bolt(&bolt).unwrap();
assert_eq!(format!("{back:?}"), format!("{value:?}"));
let Value::ZonedDateTime {
offset_secs,
zone: Some(back_zone),
..
} = back
else {
panic!("DateTimeZoneId decodes a named zoned datetime");
};
assert_eq!((offset_secs, back_zone.name()), (7200, "Europe/Oslo"));
}
#[test]
fn out_of_range_inbound_datetimes_and_named_zones_are_protocol_errors() {
for bad in [
BoltValue::DateTime(boltr::types::BoltDateTime {
seconds: 0,
nanoseconds: 1_000_000_000,
tz_offset_seconds: 0,
}),
BoltValue::DateTime(boltr::types::BoltDateTime {
seconds: 0,
nanoseconds: -1,
tz_offset_seconds: 0,
}),
BoltValue::DateTime(boltr::types::BoltDateTime {
seconds: i64::MAX,
nanoseconds: 0,
tz_offset_seconds: 0,
}),
BoltValue::DateTime(boltr::types::BoltDateTime {
seconds: 0,
nanoseconds: 0,
tz_offset_seconds: 64_801,
}),
BoltValue::DateTimeZoneId(boltr::types::BoltDateTimeZoneId {
seconds: 0,
nanoseconds: 0,
tz_id: "Mars/Base".into(),
}),
BoltValue::DateTimeZoneId(boltr::types::BoltDateTimeZoneId {
seconds: 0,
nanoseconds: 1_000_000_000,
tz_id: "UTC".into(),
}),
] {
assert!(
matches!(from_bolt(&bad), Err(BoltError::Protocol(_))),
"{bad:?}"
);
}
}
}
#[cfg(test)]
mod timestamp_representability_tests {
use super::*;
#[test]
fn chrono_leap_second_is_refused_without_silent_normalization() {
let leap = chrono::NaiveDate::from_ymd_opt(2016, 12, 31)
.unwrap()
.and_hms_nano_opt(23, 59, 59, 1_500_000_000)
.unwrap();
let value = Value::Timestamp(leap);
assert!(
matches!(to_bolt(&value), Err(BoltError::Backend(message)) if message.contains("leap seconds"))
);
assert!(matches!(
to_bolt(&Value::List(vec![value])),
Err(BoltError::Backend(_))
));
let incompatible = BoltValue::LocalDateTime(BoltLocalDateTime {
seconds: leap.and_utc().timestamp(),
nanoseconds: 1_500_000_000,
});
assert!(matches!(
from_bolt(&incompatible),
Err(BoltError::Protocol(_))
));
}
}
#[cfg(test)]
mod owned_conversion_tests {
use kglite::api::{NodeValue, PathValue, RelValue};
use kglite::datatypes::PropMap;
use super::*;
fn props() -> PropMap {
PropMap::from_pairs(vec![
("name".into(), Value::String("Ada".into())),
(
"tags".into(),
Value::List(vec![Value::Int64(1), Value::Null]),
),
(
"nested".into(),
Value::Map(PropMap::from_pairs(vec![(
"k".into(),
Value::Boolean(true),
)])),
),
])
}
fn node(id: u32) -> NodeValue {
NodeValue {
id,
labels: vec!["Person".into(), "Admin".into()],
properties: props(),
}
}
fn rel(id: u32, start_id: u32, end_id: u32) -> RelValue {
RelValue {
incarnation: None,
id,
start_id,
end_id,
rel_type: "KNOWS".into(),
properties: props(),
}
}
#[test]
fn owned_conversion_matches_borrowed_conversion() {
let values = vec![
Value::Null,
Value::Boolean(true),
Value::Int64(-7),
Value::UniqueId(9),
Value::Float64(1.5),
Value::String("text".into()),
Value::List(vec![
Value::String("a".into()),
Value::List(vec![Value::Int64(1)]),
]),
Value::Map(props()),
Value::DateTime(chrono::NaiveDate::from_ymd_opt(2024, 2, 29).unwrap()),
Value::Timestamp(
chrono::NaiveDate::from_ymd_opt(2024, 2, 29)
.unwrap()
.and_hms_nano_opt(1, 2, 3, 456)
.unwrap(),
),
Value::Point { lat: 1.0, lon: 2.0 },
Value::Duration {
months: 1,
days: 2,
seconds: 3,
nanos: 0,
},
Value::Node(Box::new(node(4))),
Value::Relationship(Box::new(rel(5, 4, 6))),
Value::Path(Box::new(PathValue {
nodes: vec![node(2), node(1), node(3)],
rels: vec![rel(7, 1, 2), rel(8, 1, 3)],
})),
];
for value in values {
assert_eq!(
to_bolt_owned(value.clone()).unwrap(),
to_bolt(&value).unwrap(),
"{value:?}"
);
}
}
#[test]
fn owned_conversion_refuses_what_borrowed_conversion_refuses() {
let leap = chrono::NaiveDate::from_ymd_opt(2016, 12, 31)
.unwrap()
.and_hms_nano_opt(23, 59, 59, 1_500_000_000)
.unwrap();
for value in [
Value::NodeRef(1),
Value::List(vec![Value::Timestamp(leap)]),
Value::Path(Box::new(PathValue {
nodes: vec![node(1)],
rels: vec![rel(7, 1, 2)],
})),
Value::Path(Box::new(PathValue {
nodes: vec![node(1), node(2)],
rels: vec![rel(7, 3, 4)],
})),
] {
assert!(to_bolt(&value).is_err(), "{value:?}");
assert!(to_bolt_owned(value.clone()).is_err(), "{value:?}");
}
}
}