use super::DirGraph;
use crate::datatypes::values::Value;
use crate::graph::schema::InternedKey;
use crate::graph::storage::GraphRead; use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
#[derive(Debug, Default)]
pub struct ForkPrivateCache<T>(RwLock<Option<T>>);
impl<T> Clone for ForkPrivateCache<T> {
#[inline]
fn clone(&self) -> Self {
Self(RwLock::new(None))
}
}
impl<T> std::ops::Deref for ForkPrivateCache<T> {
type Target = RwLock<Option<T>>;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DirGraph {
pub fn get_edge_type_counts(&self) -> Arc<HashMap<String, usize>> {
{
let read = self.edge_type_counts_cache.read().unwrap();
if let Some(ref cached) = *read {
return Arc::clone(cached);
}
}
let mut counts: HashMap<InternedKey, usize> = HashMap::new();
for (_src, _tgt, conn_key) in self.graph.edge_endpoint_keys() {
*counts.entry(conn_key).or_insert(0) += 1;
}
let string_counts: Arc<HashMap<String, usize>> = Arc::new(
counts
.into_iter()
.map(|(k, v)| (self.interner.resolve(k).to_string(), v))
.collect(),
);
let mut write = self.edge_type_counts_cache.write().unwrap();
*write = Some(Arc::clone(&string_counts));
string_counts
}
pub(crate) fn invalidate_edge_type_counts_cache(&self) {
*self.edge_type_counts_cache.write().unwrap() = None;
*self.type_connectivity_cache.write().unwrap() = None;
}
pub fn property_ndv(&self, node_type: &str, property: &str) -> Option<usize> {
const MAX_SCAN: usize = 200_000;
let nodes = self.type_indices.get(node_type)?;
if nodes.is_empty() || nodes.len() > MAX_SCAN {
return None;
}
let field = self.resolve_alias(node_type, property);
let key = (node_type.to_string(), field.to_string());
{
let read = self.property_ndv_cache.read().unwrap();
if read.0 == self.version {
if let Some(&ndv) = read.1.get(&key) {
return (ndv > 0).then_some(ndv);
}
}
}
let _arena_guard = self.graph.begin_query();
let field_key = InternedKey::from_str(field);
let mut seen: std::collections::HashSet<Value> = std::collections::HashSet::new();
for idx in nodes.iter() {
if let Some(node) = self.node_view(idx) {
if let Some(val) = node.resolved_field(node_type, field, field_key) {
seen.insert(val.into_owned());
}
}
}
let ndv = seen.len();
let mut write = self.property_ndv_cache.write().unwrap();
if write.0 != self.version {
write.1.clear();
write.0 = self.version;
}
write.1.insert(key, ndv);
(ndv > 0).then_some(ndv)
}
pub fn has_edge_type_counts_cache(&self) -> bool {
self.edge_type_counts_cache.read().unwrap().is_some()
}
pub fn has_type_connectivity_cache(&self) -> bool {
self.type_connectivity_cache.read().unwrap().is_some()
}
}
#[cfg(test)]
mod fork_aliasing_tests {
use super::*;
use crate::graph::handle::make_dir_graph_mut;
use crate::graph::session::execute::{execute_mut, ExecuteOptions};
use std::sync::Arc;
fn run(graph: &mut DirGraph, query: &str) {
let params = HashMap::new();
execute_mut(graph, query, &ExecuteOptions::eager(¶ms)).expect("query");
}
fn two_edge_graph() -> DirGraph {
let mut graph = DirGraph::new();
run(
&mut graph,
"CREATE (a:Item {id: 1, name: 'a'}), (b:Item {id: 2, name: 'b'}), \
(c:Item {id: 3, name: 'c'})",
);
run(
&mut graph,
"MATCH (a:Item {id: 1}), (b:Item {id: 2}) CREATE (a)-[:LINKS]->(b)",
);
run(
&mut graph,
"MATCH (b:Item {id: 2}), (c:Item {id: 3}) CREATE (b)-[:LINKS]->(c)",
);
graph
}
#[test]
fn a_held_reader_reports_its_own_edge_type_counts_not_the_writers() {
let mut writer = Arc::new(two_edge_graph());
let reader = Arc::clone(&writer);
{
let graph = make_dir_graph_mut(&mut writer);
run(
graph,
"MATCH (a:Item {id: 1}), (c:Item {id: 3}) CREATE (a)-[:LINKS]->(c)",
);
}
assert_eq!(writer.get_edge_type_counts().get("LINKS"), Some(&3));
assert_eq!(
reader.get_edge_type_counts().get("LINKS"),
Some(&2),
"the reader's snapshot has two LINKS edges; reading three means it \
was handed the writer's cache entry through a shared Arc (D2 R6)"
);
assert_eq!(
reader.graph.edge_count(),
2,
"sanity: the snapshot really has 2"
);
}
#[test]
fn a_writer_reports_its_own_type_connectivity_not_the_readers() {
let mut writer = Arc::new(two_edge_graph());
let reader = Arc::clone(&writer);
assert_eq!(reader.get_edge_type_counts().get("LINKS"), Some(&2));
{
let graph = make_dir_graph_mut(&mut writer);
run(
graph,
"MATCH (a:Item {id: 1}), (c:Item {id: 3}) CREATE (a)-[:LINKS]->(c)",
);
}
assert_eq!(
writer.get_edge_type_counts().get("LINKS"),
Some(&3),
"the writer added an edge; reading two means the reader's stale entry \
survived in a shared cache the writer's mutation could not reach"
);
}
#[test]
fn the_pure_and_versioned_caches_are_deliberately_shared() {
let mut writer = Arc::new(two_edge_graph());
let reader = Arc::clone(&writer);
let version_before = reader.version();
{
let graph = make_dir_graph_mut(&mut writer);
run(graph, "CREATE (:Item {id: 9, name: 'z'})");
}
assert!(
!Arc::ptr_eq(&reader, &writer),
"the write must have forked the writer away from the reader, or the \
handle comparisons below are about a single graph"
);
assert!(
Arc::ptr_eq(&reader.wkt_cache, &writer.wkt_cache),
"wkt_cache is shared on purpose: pure function of its key"
);
assert!(
Arc::ptr_eq(&reader.property_ndv_cache, &writer.property_ndv_cache),
"property_ndv_cache is shared on purpose: version-tagged, and an estimate"
);
assert_ne!(
writer.version(),
version_before,
"a write must bump the version, or the NDV cache's tag proves nothing"
);
assert_eq!(
reader.version(),
version_before,
"the reader's snapshot keeps its own version"
);
}
}