use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use crate::datatypes::Value;
use crate::graph::schema::{DirGraph, PropertyStorage};
use crate::graph::session::execute::{execute_mut, ExecuteOptions};
use crate::graph::storage::GraphRead;
type PropPairs = Vec<(String, String)>;
type NodeFingerprint = (usize, String, String, String, PropPairs, Vec<String>);
type EdgeFingerprint = (usize, usize, usize, String, PropPairs);
type MasterRows = Vec<(u32, String, String, PropPairs)>;
#[derive(Debug, PartialEq, Eq)]
struct Fingerprint {
version: u64,
node_count: usize,
edge_count: usize,
nodes: Vec<NodeFingerprint>,
edges: Vec<EdgeFingerprint>,
type_indices: Vec<(String, Vec<usize>)>,
secondary_labels: Vec<(String, Vec<usize>)>,
has_secondary_labels: bool,
node_type_metadata: Vec<(String, PropPairs)>,
connection_type_metadata: Vec<(String, Vec<String>, Vec<String>, PropPairs)>,
type_schemas: Vec<(String, Vec<String>)>,
id_lookup: Vec<(String, String, usize)>,
column_masters: Vec<(String, MasterRows)>,
columnar_rows: Vec<(usize, u32)>,
user_indexes: Vec<(String, String, Vec<usize>)>,
}
fn sorted_props(props: &HashMap<String, Value>) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = props
.iter()
.map(|(k, v)| (k.clone(), format!("{v:?}")))
.collect();
out.sort();
out
}
fn fingerprint_nodes(graph: &DirGraph) -> Vec<NodeFingerprint> {
let mut nodes = Vec::new();
for idx in graph.graph.node_indices().collect::<Vec<_>>() {
let Some(node) = graph.graph.node_view(idx) else {
continue;
};
let mut labels: Vec<String> = graph
.node_labels(idx)
.into_iter()
.map(|key| graph.interner.resolve(key).to_string())
.collect();
labels.sort();
nodes.push((
idx.index(),
node.node_type_str(&graph.interner).to_string(),
format!("{:?}", node.id()),
format!("{:?}", node.title()),
sorted_props(&node.properties_cloned(&graph.interner)),
labels,
));
}
nodes.sort();
nodes
}
fn fingerprint_edges(graph: &DirGraph) -> Vec<EdgeFingerprint> {
let mut edges = Vec::new();
for eidx in graph.graph.edge_indices().collect::<Vec<_>>() {
let (Some((src, tgt)), Some(edge)) = (
graph.graph.edge_endpoints(eidx),
graph.graph.edge_weight(eidx),
) else {
continue;
};
edges.push((
eidx.index(),
src.index(),
tgt.index(),
edge.connection_type_str(&graph.interner).to_string(),
sorted_props(&edge.properties_cloned(&graph.interner)),
));
}
edges.sort();
edges
}
fn fingerprint_column_masters(graph: &DirGraph) -> Vec<(String, MasterRows)> {
let mut column_masters: Vec<(String, MasterRows)> = graph
.column_stores_by_name()
.into_iter()
.map(|(node_type, store)| {
let rows: MasterRows = (0..store.row_count())
.map(|row| {
let mut props: PropPairs = store
.row_properties(row)
.into_iter()
.map(|(key, value)| {
(
graph.interner.resolve(key).to_string(),
format!("{value:?}"),
)
})
.collect();
props.sort();
(
row,
format!("{:?}", store.get_id(row)),
format!("{:?}", store.get_title(row)),
props,
)
})
.collect();
(node_type.to_string(), rows)
})
.collect();
column_masters.sort();
column_masters
}
fn fingerprint_columnar_rows(graph: &DirGraph) -> Vec<(usize, u32)> {
let mut columnar_rows: Vec<(usize, u32)> = Vec::new();
for idx in graph.graph.node_indices().collect::<Vec<_>>() {
let Some(node) = graph.graph.node_weight(idx) else {
continue;
};
let PropertyStorage::Columnar(row) = &node.properties else {
continue;
};
columnar_rows.push((idx.index(), row.row_id()));
}
columnar_rows.sort();
columnar_rows
}
fn fingerprint_user_indexes(graph: &DirGraph) -> Vec<(String, String, Vec<usize>)> {
let mut user_indexes: Vec<(String, String, Vec<usize>)> = Vec::new();
for ((node_type, property), value_map) in &graph.property_indices {
for (value, members) in value_map.iter() {
user_indexes.push((
format!("property {node_type}.{property}"),
format!("{value:?}"),
members.iter().map(|idx| idx.index()).collect(),
));
}
}
for ((node_type, property), btree) in &graph.range_indices {
for (value, members) in btree.iter() {
user_indexes.push((
format!("range {node_type}.{property}"),
format!("{value:?}"),
members.iter().map(|idx| idx.index()).collect(),
));
}
}
for ((node_type, properties), comp_map) in &graph.composite_indices {
for (value, members) in comp_map.iter() {
user_indexes.push((
format!("composite {node_type}.{}", properties.join("+")),
format!("{value:?}"),
members.iter().map(|idx| idx.index()).collect(),
));
}
}
user_indexes.sort();
user_indexes
}
fn fingerprint(graph: &mut DirGraph) -> Fingerprint {
let nodes = fingerprint_nodes(graph);
let edges = fingerprint_edges(graph);
let mut type_indices: Vec<(String, Vec<usize>)> = graph
.type_indices
.iter()
.map(|(name, members)| {
(
name.to_string(),
members.to_vec().iter().map(|i| i.index()).collect(),
)
})
.collect();
type_indices.sort();
let mut secondary_labels: Vec<(String, Vec<usize>)> = graph
.secondary_label_index
.iter()
.map(|(label, members)| {
(
graph.interner.resolve(*label).to_string(),
members.iter().map(|i| i.index()).collect(),
)
})
.collect();
secondary_labels.sort();
let mut node_type_metadata: Vec<(String, Vec<(String, String)>)> = graph
.node_type_metadata
.iter()
.map(|(t, props)| {
let mut props: Vec<(String, String)> =
props.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
props.sort();
(t.clone(), props)
})
.collect();
node_type_metadata.sort();
let mut connection_type_metadata: Vec<(String, Vec<String>, Vec<String>, PropPairs)> = graph
.connection_type_metadata
.iter()
.map(|(conn, info)| {
let mut sources: Vec<String> = info.source_types.iter().cloned().collect();
sources.sort();
let mut targets: Vec<String> = info.target_types.iter().cloned().collect();
targets.sort();
let mut props: PropPairs = info
.property_types
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
props.sort();
(conn.clone(), sources, targets, props)
})
.collect();
connection_type_metadata.sort();
let mut type_schemas: Vec<(String, Vec<String>)> = graph
.type_schemas
.iter()
.map(|(node_type, schema)| {
let keys: Vec<String> = schema
.iter()
.map(|(_, key)| graph.interner.resolve(key).to_string())
.collect();
(node_type.clone(), keys)
})
.collect();
type_schemas.sort();
let mut id_lookup = Vec::new();
for entry in &nodes {
let (slot, node_type, id_debug, ..) = entry;
if let Some(node) = graph
.graph
.node_weight(petgraph::graph::NodeIndex::new(*slot))
{
let owned_id = node.id().into_owned();
if let Some(found) = graph.lookup_by_id(node_type, &owned_id) {
id_lookup.push((node_type.clone(), id_debug.clone(), found.index()));
}
}
}
id_lookup.sort();
let column_masters = fingerprint_column_masters(graph);
let columnar_rows = fingerprint_columnar_rows(graph);
let user_indexes = fingerprint_user_indexes(graph);
Fingerprint {
version: graph.version,
node_count: graph.graph.node_count(),
edge_count: graph.graph.edge_count(),
nodes,
edges,
type_indices,
secondary_labels,
has_secondary_labels: graph.has_secondary_labels,
node_type_metadata,
connection_type_metadata,
type_schemas,
id_lookup,
column_masters,
columnar_rows,
user_indexes,
}
}
fn run(graph: &mut DirGraph, query: &str) {
let params = HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
execute_mut(graph, query, &opts).unwrap_or_else(|e| panic!("setup query failed: {query}: {e}"));
}
fn expect_failure(graph: &mut DirGraph, query: &str, scope: Option<&[&str]>) -> String {
let params = HashMap::new();
let mut opts = ExecuteOptions::eager(¶ms);
let owned: Option<HashSet<String>> =
scope.map(|names| names.iter().map(|s| s.to_string()).collect());
opts.write_scope = owned.as_ref();
match execute_mut(graph, query, &opts) {
Ok(_) => panic!("expected {query} to fail mid-statement"),
Err(error) => error.to_string(),
}
}
fn assert_rolls_back(graph: &mut DirGraph, query: &str, scope: Option<&[&str]>) {
let before = fingerprint(graph);
let error = expect_failure(graph, query, scope);
let after = fingerprint(graph);
assert_eq!(
before, after,
"statement must roll back completely.\nquery: {query}\nerror: {error}"
);
}
fn seeded() -> DirGraph {
let mut graph = DirGraph::new();
seed_into(&mut graph);
graph
}
fn seed_into(graph: &mut DirGraph) {
run(
graph,
"CREATE (a:Item {id: 1, name: 'a', qty: 10}), \
(b:Item {id: 2, name: 'b', qty: 20}), \
(c:Item {id: 3, name: 'c', qty: 30})",
);
run(graph, "CREATE (t:Tag:Hot {id: 1, name: 'urgent'})");
run(graph, "CREATE (t:Tag:Cold {id: 2, name: 'later'})");
run(
graph,
"MATCH (a:Item {id: 1}), (b:Item {id: 2}) CREATE (a)-[:LINKS {weight: 5}]->(b)",
);
run(
graph,
"MATCH (b:Item {id: 2}), (c:Item {id: 3}) CREATE (b)-[:LINKS {weight: 7}]->(c)",
);
run(
graph,
"MATCH (a:Item {id: 1}), (t:Tag {id: 1}) CREATE (a)-[:TAGGED]->(t)",
);
}
fn seeded_columnar() -> DirGraph {
let mut graph = seeded();
graph.enable_columnar();
assert!(
graph.column_store_count() > 0,
"the fixture must own master column stores, or the columnar arms are vacuous"
);
let columnar_nodes = graph
.graph
.node_indices()
.filter(|idx| {
matches!(
graph.graph.node_weight(*idx).map(|node| &node.properties),
Some(PropertyStorage::Columnar { .. })
)
})
.count();
assert_eq!(
columnar_nodes,
graph.graph.node_count(),
"every seeded node must read its properties through a column store"
);
graph
}
fn seeded_mapped() -> DirGraph {
use crate::graph::storage::mode::{new_dir_graph_in_mode, StorageMode};
let mut graph = new_dir_graph_in_mode(StorageMode::Mapped, None).expect("mapped graph");
assert!(
graph.graph.is_mapped(),
"the mapped fixture must actually be on the Mapped backend, or every \
arm below is a second run of the plain fixture"
);
seed_into(&mut graph);
graph
}
fn seeded_indexed() -> DirGraph {
let mut graph = seeded();
graph.create_index("Item", "name");
graph.create_index("Item", "qty");
graph.create_range_index("Item", "qty");
graph.create_composite_index("Item", &["name", "qty"]);
assert!(
!graph.property_indices.is_empty()
&& !graph.range_indices.is_empty()
&& !graph.composite_indices.is_empty(),
"all three index families must be live, or the indexed arms are vacuous"
);
assert!(
graph
.property_indices
.values()
.any(|value_map| value_map.iter().any(|(_, members)| !members.is_empty())),
"the indexes must have been populated from the seeded nodes"
);
graph
}
mod cell_fidelity;
mod columnar_cost;
const WIDE_ITEMS: usize = 200;
fn wide_columnar() -> DirGraph {
wide_columnar_into(DirGraph::new())
}
fn wide_columnar_mapped() -> DirGraph {
use crate::graph::storage::mode::{new_dir_graph_in_mode, StorageMode};
let graph = new_dir_graph_in_mode(StorageMode::Mapped, None).expect("mapped graph");
assert!(graph.graph.is_mapped(), "fixture must be on Mapped");
wide_columnar_into(graph)
}
fn wide_rows_into(mut graph: DirGraph) -> DirGraph {
let rows: Vec<String> = (0..WIDE_ITEMS)
.map(|i| format!("(:Item {{id: {i}, name: 'n{i}', qty: {i}}})"))
.collect();
run(&mut graph, &format!("CREATE {}", rows.join(", ")));
graph
}
const WIDE_SCHEMA_COLUMNS: usize = 24;
fn wide_schema_columnar() -> DirGraph {
let mut graph = DirGraph::new();
let rows: Vec<String> = (0..WIDE_ITEMS)
.map(|i| {
let props = (0..WIDE_SCHEMA_COLUMNS)
.map(|c| format!("p{c}: {}", i + c))
.collect::<Vec<_>>()
.join(", ");
format!("(:Item {{id: {i}, name: 'n{i}', {props}}})")
})
.collect();
run(&mut graph, &format!("CREATE {}", rows.join(", ")));
graph.enable_columnar();
assert!(
graph.column_store_count() > 0,
"the fixture must own a master column store, or this test is vacuous"
);
graph
}
fn wide_columnar_into(graph: DirGraph) -> DirGraph {
let mut graph = wide_rows_into(graph);
graph.enable_columnar();
assert!(
graph.column_store_count() > 0,
"the fixture must own a master column store, or this test is vacuous"
);
graph
}
const FAILS_AFTER_A_COLUMNAR_WRITE: &str = "WITH n MATCH (m:Item {id: 2}) \
SET m.qty = duration({months: 2147483648})";
fn item_prop(graph: &DirGraph, id: i64, property: &str) -> Option<Value> {
let idx = graph
.graph
.node_indices()
.find(|i| graph.graph.get_node_id(*i) == Some(Value::Int64(id)))
.unwrap_or_else(|| panic!("no Item with id {id}"));
graph
.graph
.node_view(idx)
.and_then(|n| n.get_property_value(property))
}
mod fidelity;
mod held_reader;
mod journal_invariants;
mod row_undo;
mod schema_shell;
mod store_clone;
mod unique_claims;