use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use crate::cypher::procedure::ProcedureRegistry;
use crate::cypher::record::NamedRecord;
use crate::{Database, GraphError, Value};
use cucumber::World as CucumberWorld;
use rusqlite::Connection;
fn hash_value(v: &Value) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
v.hash(&mut hasher);
hasher.finish()
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GraphCounts {
pub nodes: i64,
pub relationships: i64,
pub labels: i64,
pub node_keys: HashSet<Vec<u8>>,
pub edge_keys: HashSet<Vec<u8>>,
pub property_fingerprints: HashSet<(String, String, u64)>,
}
impl GraphCounts {
pub fn snapshot(conn: &Connection) -> Self {
let nodes: i64 = conn
.query_row("SELECT COUNT(*) FROM nodes", [], |r| r.get(0))
.unwrap_or(0);
let relationships: i64 = conn
.query_row("SELECT COUNT(*) FROM edge_props", [], |r| r.get(0))
.unwrap_or(0);
let labels: i64 = conn
.query_row(
"SELECT COUNT(*) FROM metadata WHERE key LIKE 'stats:label_count:%'",
[],
|r| r.get(0),
)
.unwrap_or(0);
#[derive(serde::Deserialize)]
struct NodeBlob {
#[allow(dead_code)]
labels: Vec<String>,
properties: HashMap<String, crate::Value>,
}
let mut fingerprints = HashSet::new();
let mut node_keys = HashSet::new();
let mut edge_keys = HashSet::new();
{
let mut stmt = conn.prepare("SELECT key, value FROM nodes").unwrap();
let mut rows = stmt.query([]).unwrap();
while let Some(row) = rows.next().unwrap() {
let key: Vec<u8> = row.get(0).unwrap();
node_keys.insert(key.clone());
let owner = format!("n:{key:?}");
let data: Vec<u8> = row.get(1).unwrap();
if let Ok(rec) = rmp_serde::from_slice::<NodeBlob>(&data) {
for (k, v) in &rec.properties {
fingerprints.insert((owner.clone(), k.clone(), hash_value(v)));
}
}
}
}
{
let mut stmt = conn.prepare("SELECT key, value FROM edge_props").unwrap();
let mut rows = stmt.query([]).unwrap();
while let Some(row) = rows.next().unwrap() {
let key: Vec<u8> = row.get(0).unwrap();
edge_keys.insert(key.clone());
let owner = format!("e:{key:?}");
let data: Vec<u8> = row.get(1).unwrap();
if let Ok(props) = rmp_serde::from_slice::<HashMap<String, crate::Value>>(&data) {
for (k, v) in &props {
fingerprints.insert((owner.clone(), k.clone(), hash_value(v)));
}
}
}
}
Self {
nodes,
relationships,
labels,
node_keys,
edge_keys,
property_fingerprints: fingerprints,
}
}
pub fn delta(&self, after: &Self) -> HashMap<String, i64> {
let mut m = HashMap::new();
let nodes_added = after.node_keys.difference(&self.node_keys).count() as i64;
let nodes_removed = self.node_keys.difference(&after.node_keys).count() as i64;
m.insert("+nodes".into(), nodes_added);
m.insert("-nodes".into(), nodes_removed);
let rels_added = after.edge_keys.difference(&self.edge_keys).count() as i64;
let rels_removed = self.edge_keys.difference(&after.edge_keys).count() as i64;
m.insert("+relationships".into(), rels_added);
m.insert("-relationships".into(), rels_removed);
m.insert("+labels".into(), (after.labels - self.labels).max(0));
m.insert("-labels".into(), (self.labels - after.labels).max(0));
let added = after
.property_fingerprints
.difference(&self.property_fingerprints)
.count() as i64;
let removed = self
.property_fingerprints
.difference(&after.property_fingerprints)
.count() as i64;
m.insert("+properties".into(), added);
m.insert("-properties".into(), removed);
m
}
}
#[derive(CucumberWorld, Default)]
#[world(init = Self::default)]
pub struct World {
pub db: Option<Database>,
pub last_result: Option<Vec<NamedRecord>>,
pub last_error: Option<GraphError>,
pub params: HashMap<String, Value>,
pub pre_counts: GraphCounts,
pub procedures: ProcedureRegistry,
pub current_feature: String,
pub current_scenario: String,
}
impl std::fmt::Debug for World {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("World")
.field("db", &self.db.as_ref().map(|_| "<Database>"))
.field("last_result", &self.last_result)
.field("last_error", &self.last_error)
.field("params", &self.params)
.field("pre_counts", &self.pre_counts)
.finish()
}
}