use std::collections::HashMap;
use serde::{Deserialize, Serialize};
pub const BACKUP_FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BackupFormat {
JsonLines,
}
impl Default for BackupFormat {
fn default() -> Self {
Self::JsonLines
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupMetadata {
pub version: u32,
pub format: BackupFormat,
pub created_at: u64,
pub sequence_number: u64,
pub is_incremental: bool,
pub previous_sequence: Option<u64>,
pub statistics: BackupStatistics,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub extra: HashMap<String, String>,
}
impl BackupMetadata {
pub fn new_full(sequence_number: u64) -> Self {
Self {
version: BACKUP_FORMAT_VERSION,
format: BackupFormat::default(),
created_at: current_timestamp(),
sequence_number,
is_incremental: false,
previous_sequence: None,
statistics: BackupStatistics::default(),
description: None,
extra: HashMap::new(),
}
}
pub fn new_incremental(sequence_number: u64, previous_sequence: u64) -> Self {
Self {
version: BACKUP_FORMAT_VERSION,
format: BackupFormat::default(),
created_at: current_timestamp(),
sequence_number,
is_incremental: true,
previous_sequence: Some(previous_sequence),
statistics: BackupStatistics::default(),
description: None,
extra: HashMap::new(),
}
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.extra.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BackupStatistics {
pub entity_count: u64,
pub edge_count: u64,
pub metadata_count: u64,
pub total_records: u64,
pub uncompressed_size: u64,
}
impl BackupStatistics {
pub fn add_entity(&mut self) {
self.entity_count += 1;
self.total_records += 1;
}
pub fn add_edge(&mut self) {
self.edge_count += 1;
self.total_records += 1;
}
pub fn add_metadata(&mut self) {
self.metadata_count += 1;
self.total_records += 1;
}
pub fn add_size(&mut self, size: u64) {
self.uncompressed_size += size;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BackupRecordType {
Metadata,
Entity,
Edge,
KeyValue,
EndOfBackup,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackupRecord {
#[serde(rename = "type")]
pub record_type: BackupRecordType,
pub data: RecordData,
#[serde(skip_serializing_if = "Option::is_none")]
pub table: Option<String>,
}
impl BackupRecord {
pub fn metadata(meta: BackupMetadata) -> Self {
Self {
record_type: BackupRecordType::Metadata,
data: RecordData::Metadata(meta),
table: None,
}
}
pub fn entity(entity: EntityRecord) -> Self {
Self {
record_type: BackupRecordType::Entity,
data: RecordData::Entity(entity),
table: None,
}
}
pub fn edge(edge: EdgeRecord) -> Self {
Self { record_type: BackupRecordType::Edge, data: RecordData::Edge(edge), table: None }
}
pub fn key_value(table: String, key: Vec<u8>, value: Vec<u8>) -> Self {
Self {
record_type: BackupRecordType::KeyValue,
data: RecordData::KeyValue(KeyValueRecord { key, value }),
table: Some(table),
}
}
pub fn end_of_backup(stats: BackupStatistics) -> Self {
Self {
record_type: BackupRecordType::EndOfBackup,
data: RecordData::EndOfBackup(stats),
table: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RecordData {
Metadata(BackupMetadata),
Entity(EntityRecord),
Edge(EdgeRecord),
KeyValue(KeyValueRecord),
EndOfBackup(BackupStatistics),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityRecord {
pub id: u64,
pub labels: Vec<String>,
pub properties: HashMap<String, serde_json::Value>,
}
impl EntityRecord {
pub fn from_entity(entity: &manifoldb_core::Entity) -> Self {
let properties =
entity.properties.iter().map(|(k, v)| (k.clone(), value_to_json(v))).collect();
Self {
id: entity.id.as_u64(),
labels: entity.labels.iter().map(|l| l.as_str().to_owned()).collect(),
properties,
}
}
pub fn to_entity(&self) -> manifoldb_core::Entity {
let mut entity = manifoldb_core::Entity::new(manifoldb_core::EntityId::new(self.id));
for label in &self.labels {
entity = entity.with_label(label.as_str());
}
for (key, value) in &self.properties {
entity = entity.with_property(key.as_str(), json_to_value(value));
}
entity
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeRecord {
pub id: u64,
pub source: u64,
pub target: u64,
pub edge_type: String,
pub properties: HashMap<String, serde_json::Value>,
}
impl EdgeRecord {
pub fn from_edge(edge: &manifoldb_core::Edge) -> Self {
let properties =
edge.properties.iter().map(|(k, v)| (k.clone(), value_to_json(v))).collect();
Self {
id: edge.id.as_u64(),
source: edge.source.as_u64(),
target: edge.target.as_u64(),
edge_type: edge.edge_type.as_str().to_owned(),
properties,
}
}
pub fn to_edge(&self) -> manifoldb_core::Edge {
let mut edge = manifoldb_core::Edge::new(
manifoldb_core::EdgeId::new(self.id),
manifoldb_core::EntityId::new(self.source),
manifoldb_core::EntityId::new(self.target),
self.edge_type.as_str(),
);
for (key, value) in &self.properties {
edge = edge.with_property(key.as_str(), json_to_value(value));
}
edge
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeyValueRecord {
#[serde(with = "base64_bytes")]
pub key: Vec<u8>,
#[serde(with = "base64_bytes")]
pub value: Vec<u8>,
}
fn value_to_json(value: &manifoldb_core::Value) -> serde_json::Value {
match value {
manifoldb_core::Value::Null => serde_json::Value::Null,
manifoldb_core::Value::Bool(b) => serde_json::Value::Bool(*b),
manifoldb_core::Value::Int(i) => serde_json::Value::Number((*i).into()),
manifoldb_core::Value::Float(f) => serde_json::Number::from_f64(*f)
.map_or(serde_json::Value::Null, serde_json::Value::Number),
manifoldb_core::Value::String(s) => serde_json::Value::String(s.clone()),
manifoldb_core::Value::Bytes(b) => {
use base64::Engine;
serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(b))
}
manifoldb_core::Value::Vector(v) => serde_json::Value::Array(
v.iter()
.map(|f| {
serde_json::Number::from_f64(f64::from(*f))
.map_or(serde_json::Value::Null, serde_json::Value::Number)
})
.collect(),
),
manifoldb_core::Value::SparseVector(v) => {
serde_json::Value::Array(
v.iter()
.map(|(idx, val)| {
serde_json::Value::Array(vec![
serde_json::Value::Number((*idx).into()),
serde_json::Number::from_f64(f64::from(*val))
.map_or(serde_json::Value::Null, serde_json::Value::Number),
])
})
.collect(),
)
}
manifoldb_core::Value::MultiVector(vecs) => {
serde_json::Value::Array(
vecs.iter()
.map(|v| {
serde_json::Value::Array(
v.iter()
.map(|f| {
serde_json::Number::from_f64(f64::from(*f))
.map_or(serde_json::Value::Null, serde_json::Value::Number)
})
.collect(),
)
})
.collect(),
)
}
manifoldb_core::Value::Array(arr) => {
serde_json::Value::Array(arr.iter().map(value_to_json).collect())
}
}
}
fn json_to_value(json: &serde_json::Value) -> manifoldb_core::Value {
match json {
serde_json::Value::Null => manifoldb_core::Value::Null,
serde_json::Value::Bool(b) => manifoldb_core::Value::Bool(*b),
serde_json::Value::Number(n) => n
.as_i64()
.map(manifoldb_core::Value::Int)
.or_else(|| n.as_f64().map(manifoldb_core::Value::Float))
.unwrap_or(manifoldb_core::Value::Null),
serde_json::Value::String(s) => manifoldb_core::Value::String(s.clone()),
serde_json::Value::Array(arr) => {
let as_floats: Option<Vec<f32>> =
arr.iter().map(|v| v.as_f64().map(|f| f as f32)).collect();
if let Some(floats) = as_floats {
manifoldb_core::Value::Vector(floats)
} else {
manifoldb_core::Value::Array(arr.iter().map(json_to_value).collect())
}
}
serde_json::Value::Object(_) => {
manifoldb_core::Value::Null
}
}
}
fn current_timestamp() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
mod base64_bytes {
use base64::Engine;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
serializer.serialize_str(&encoded)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
base64::engine::general_purpose::STANDARD.decode(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backup_metadata_full() {
let meta = BackupMetadata::new_full(42);
assert_eq!(meta.version, BACKUP_FORMAT_VERSION);
assert!(!meta.is_incremental);
assert_eq!(meta.sequence_number, 42);
assert!(meta.previous_sequence.is_none());
}
#[test]
fn test_backup_metadata_incremental() {
let meta = BackupMetadata::new_incremental(100, 50);
assert!(meta.is_incremental);
assert_eq!(meta.sequence_number, 100);
assert_eq!(meta.previous_sequence, Some(50));
}
#[test]
fn test_entity_record_roundtrip() {
let entity = manifoldb_core::Entity::new(manifoldb_core::EntityId::new(123))
.with_label("Person")
.with_property("name", "Alice")
.with_property("age", 30i64);
let record = EntityRecord::from_entity(&entity);
let restored = record.to_entity();
assert_eq!(restored.id.as_u64(), 123);
assert!(restored.has_label("Person"));
assert_eq!(
restored.properties.get("name"),
Some(&manifoldb_core::Value::String("Alice".to_string()))
);
}
#[test]
fn test_edge_record_roundtrip() {
let edge = manifoldb_core::Edge::new(
manifoldb_core::EdgeId::new(456),
manifoldb_core::EntityId::new(1),
manifoldb_core::EntityId::new(2),
"FOLLOWS",
)
.with_property("since", 2024i64);
let record = EdgeRecord::from_edge(&edge);
let restored = record.to_edge();
assert_eq!(restored.id.as_u64(), 456);
assert_eq!(restored.source.as_u64(), 1);
assert_eq!(restored.target.as_u64(), 2);
assert_eq!(restored.edge_type.as_str(), "FOLLOWS");
}
#[test]
fn test_value_json_roundtrip() {
let values = vec![
manifoldb_core::Value::Null,
manifoldb_core::Value::Bool(true),
manifoldb_core::Value::Int(42),
manifoldb_core::Value::Float(3.14),
manifoldb_core::Value::String("hello".to_string()),
];
for value in values {
let json = value_to_json(&value);
let restored = json_to_value(&json);
match (&value, &restored) {
(manifoldb_core::Value::Float(a), manifoldb_core::Value::Float(b)) => {
assert!((a - b).abs() < f64::EPSILON);
}
_ => assert_eq!(value, restored),
}
}
}
}