use crate::datatypes::values::Value;
use crate::graph::constraints::ConstraintKind;
use crate::graph::property_types::DeclaredType;
use crate::graph::schema::{DirGraph, InternedKey};
use crate::graph::storage::column_store::TypedColumn;
use crate::graph::storage::GraphRead;
use petgraph::Direction;
use rustc_hash::FxHashMap;
use std::collections::{HashMap, HashSet};
use super::capabilities::discover_endpoint_types_batch;
use super::connectivity::derive_edge_counts_from_triples;
use super::{
ConnectionTypeStats, NeighborConnection, NeighborsSchema, NodeTypeOverview, PropertyStatInfo,
SchemaOverview,
};
pub fn compute_connection_type_stats(graph: &DirGraph) -> Vec<ConnectionTypeStats> {
let _arena_guard = graph.graph.begin_query();
if !graph.connection_type_metadata.is_empty() {
let counts = graph.get_edge_type_counts();
let mut result: Vec<ConnectionTypeStats> = graph
.connection_type_metadata
.iter()
.map(|(conn_type, info)| {
let mut source_types: Vec<String> = info.source_types.iter().cloned().collect();
source_types.sort();
let mut target_types: Vec<String> = info.target_types.iter().cloned().collect();
target_types.sort();
let mut property_names: Vec<String> = info
.property_types
.keys()
.filter(|k| !crate::graph::schema::is_reserved_provenance_key(k))
.cloned()
.collect();
property_names.sort();
ConnectionTypeStats {
connection_type: conn_type.clone(),
count: counts.get(conn_type).copied().unwrap_or(0),
source_types,
target_types,
property_names,
}
})
.collect();
result.sort_by(|a, b| a.connection_type.cmp(&b.connection_type));
let has_empty = result
.iter()
.any(|ct| ct.source_types.is_empty() && ct.target_types.is_empty() && ct.count > 0);
if has_empty {
let triples_guard = graph.type_connectivity_cache.read().unwrap();
if let Some(triples) = triples_guard.as_ref() {
let derived = derive_edge_counts_from_triples(triples);
for ct in &mut result {
if ct.source_types.is_empty() && ct.target_types.is_empty() {
if let Some((src, tgt)) = derived.endpoints.get(&ct.connection_type) {
let mut src_vec: Vec<String> = src.iter().cloned().collect();
src_vec.sort();
let mut tgt_vec: Vec<String> = tgt.iter().cloned().collect();
tgt_vec.sort();
ct.source_types = src_vec;
ct.target_types = tgt_vec;
}
}
}
} else {
let discovered = discover_endpoint_types_batch(graph, 1_000_000);
for ct in &mut result {
if ct.source_types.is_empty() && ct.target_types.is_empty() {
if let Some((src, tgt)) = discovered.get(&ct.connection_type) {
let mut src_vec: Vec<String> = src.iter().cloned().collect();
src_vec.sort();
let mut tgt_vec: Vec<String> = tgt.iter().cloned().collect();
tgt_vec.sort();
ct.source_types = src_vec;
ct.target_types = tgt_vec;
}
}
}
}
}
return result;
}
struct Accum {
count: usize,
sources: HashSet<String>,
targets: HashSet<String>,
props: HashSet<String>,
}
let mut stats: HashMap<String, Accum> = HashMap::new();
let g = &graph.graph;
for edge_ref in g.edge_references() {
let edge_data = edge_ref.weight();
let entry = stats
.entry(edge_data.connection_type_str(&graph.interner).to_string())
.or_insert_with(|| Accum {
count: 0,
sources: HashSet::new(),
targets: HashSet::new(),
props: HashSet::new(),
});
entry.count += 1;
if let Some(source_node) = graph.node_view(edge_ref.source()) {
entry
.sources
.insert(source_node.node_type_str(&graph.interner).to_string());
}
if let Some(target_node) = graph.node_view(edge_ref.target()) {
entry
.targets
.insert(target_node.node_type_str(&graph.interner).to_string());
}
for key in edge_data.property_keys(&graph.interner) {
entry.props.insert(key.to_string());
}
}
let mut result: Vec<ConnectionTypeStats> = stats
.into_iter()
.map(|(conn_type, acc)| {
let mut source_types: Vec<String> = acc.sources.into_iter().collect();
source_types.sort();
let mut target_types: Vec<String> = acc.targets.into_iter().collect();
target_types.sort();
let mut property_names: Vec<String> = acc
.props
.into_iter()
.filter(|k| !crate::graph::schema::is_reserved_provenance_key(k))
.collect();
property_names.sort();
ConnectionTypeStats {
connection_type: conn_type,
count: acc.count,
source_types,
target_types,
property_names,
}
})
.collect();
result.sort_by(|a, b| a.connection_type.cmp(&b.connection_type));
result
}
pub(super) fn compute_connected_types(conn_stats: &[ConnectionTypeStats]) -> HashSet<String> {
let mut connected = HashSet::new();
for ct in conn_stats {
for s in &ct.source_types {
connected.insert(s.clone());
}
for t in &ct.target_types {
connected.insert(t.clone());
}
}
connected
}
pub(super) fn compute_connected_type_pairs(
conn_stats: &[ConnectionTypeStats],
) -> HashSet<(String, String)> {
let mut pairs = HashSet::new();
for ct in conn_stats {
for s in &ct.source_types {
for t in &ct.target_types {
pairs.insert((s.clone(), t.clone()));
pairs.insert((t.clone(), s.clone()));
}
}
}
pairs
}
pub(super) struct JoinCandidate {
pub(super) left_type: String,
pub(super) left_prop: String,
pub(super) left_unique: usize,
pub(super) right_type: String,
pub(super) right_prop: String,
pub(super) right_unique: usize,
pub(super) overlap: usize,
}
pub(super) fn types_compatible(left: &str, right: &str) -> bool {
let is_str = |t: &str| {
t.eq_ignore_ascii_case("string")
|| t.eq_ignore_ascii_case("uniqueid")
|| t.eq_ignore_ascii_case("str")
};
let is_num = |t: &str| {
t.eq_ignore_ascii_case("int64")
|| t.eq_ignore_ascii_case("float64")
|| t.eq_ignore_ascii_case("int")
|| t.eq_ignore_ascii_case("float")
};
(is_str(left) && is_str(right)) || (is_num(left) && is_num(right))
}
pub(super) fn sample_unique_values(
graph: &DirGraph,
node_type: &str,
property: &str,
max: usize,
) -> HashSet<String> {
let mut unique = HashSet::new();
let Some(indices) = graph.type_indices.get(node_type) else {
return unique;
};
let key = InternedKey::from_str(property);
let backend = &graph.graph;
for idx in indices.iter() {
if unique.len() >= max {
break;
}
if let Some(val) = backend.get_node_property(idx, key) {
if !is_null_value(&val) {
let s = match &val {
Value::String(s) => s.clone(),
Value::Int64(n) => n.to_string(),
Value::Float64(f) => f.to_string(),
Value::UniqueId(id) => id.to_string(),
_ => format!("{:?}", val),
};
unique.insert(s);
}
}
}
unique
}
pub(super) fn populate_sample(
cache: &mut HashMap<(String, String), Option<HashSet<String>>>,
graph: &DirGraph,
node_type: &str,
property: &str,
max: usize,
) {
let key = (node_type.to_string(), property.to_string());
if cache.contains_key(&key) {
return;
}
let vals = sample_unique_values(graph, node_type, property, max);
cache.insert(key, if vals.is_empty() { None } else { Some(vals) });
}
pub(super) fn compute_join_candidates(
graph: &DirGraph,
connected_pairs: &HashSet<(String, String)>,
max_candidates: usize,
max_sample: usize,
) -> Vec<JoinCandidate> {
let mut core_types: Vec<&str> = graph
.type_indices
.keys()
.filter(|nt| !graph.parent_types.contains_key(*nt))
.collect();
core_types.sort();
let mut candidates: Vec<JoinCandidate> = Vec::new();
let mut sample_cache: HashMap<(String, String), Option<HashSet<String>>> = HashMap::new();
'outer: for i in 0..core_types.len() {
if candidates.len() >= max_candidates * 3 {
break; }
for j in (i + 1)..core_types.len() {
if candidates.len() >= max_candidates * 3 {
break 'outer;
}
let left = core_types[i];
let right = core_types[j];
if connected_pairs.contains(&(left.to_string(), right.to_string())) {
continue;
}
let left_meta = match graph.node_type_metadata.get(left) {
Some(m) => m,
None => continue,
};
let right_meta = match graph.node_type_metadata.get(right) {
Some(m) => m,
None => continue,
};
let mut props: Vec<(&String, &String)> = left_meta.iter().collect();
props.sort_by(|a, b| a.0.cmp(b.0));
for (prop, left_type) in props {
let Some(right_type) = right_meta.get(prop) else {
continue;
};
if !types_compatible(left_type, right_type) {
continue;
}
populate_sample(&mut sample_cache, graph, left, prop, max_sample);
if sample_cache
.get(&(left.to_string(), prop.clone()))
.is_none_or(|v| v.is_none())
{
continue;
}
populate_sample(&mut sample_cache, graph, right, prop, max_sample);
let left_vals = match sample_cache.get(&(left.to_string(), prop.clone())) {
Some(Some(v)) => v,
_ => continue,
};
let right_vals = match sample_cache.get(&(right.to_string(), prop.clone())) {
Some(Some(v)) => v,
_ => continue,
};
let overlap = left_vals.intersection(right_vals).count();
if overlap > 0 {
candidates.push(JoinCandidate {
left_type: left.to_string(),
left_prop: prop.clone(),
left_unique: left_vals.len(),
right_type: right.to_string(),
right_prop: prop.clone(),
right_unique: right_vals.len(),
overlap,
});
}
}
}
}
candidates.sort_by(|a, b| {
b.overlap
.cmp(&a.overlap)
.then_with(|| a.left_type.cmp(&b.left_type))
.then_with(|| a.right_type.cmp(&b.right_type))
.then_with(|| a.left_prop.cmp(&b.left_prop))
});
candidates.truncate(max_candidates);
candidates
}
pub(crate) fn collect_labels(graph: &DirGraph) -> Vec<String> {
let mut labels = graph.get_node_types();
labels.sort();
labels
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum IndexKind {
Equality,
Composite,
Range,
}
impl IndexKind {
pub(crate) fn neo4j_type(self) -> &'static str {
match self {
IndexKind::Equality | IndexKind::Composite => "PROPERTY",
IndexKind::Range => "RANGE",
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct IndexInfo {
pub name: String,
pub kind: IndexKind,
pub entity_type: &'static str,
pub labels_or_types: Vec<String>,
pub properties: Vec<String>,
pub state: &'static str,
}
pub(crate) fn collect_indexes_structured(graph: &DirGraph) -> Vec<IndexInfo> {
let mut out: Vec<IndexInfo> = Vec::new();
for (node_type, property) in graph.property_indices.keys() {
out.push(IndexInfo {
name: format!("{node_type}.{property}"),
kind: IndexKind::Equality,
entity_type: "NODE",
labels_or_types: vec![node_type.clone()],
properties: vec![property.clone()],
state: "ONLINE",
});
}
for (node_type, properties) in graph.composite_indices.keys() {
out.push(IndexInfo {
name: format!("{node_type}.({})", properties.join(",")),
kind: IndexKind::Composite,
entity_type: "NODE",
labels_or_types: vec![node_type.clone()],
properties: properties.clone(),
state: "ONLINE",
});
}
for (node_type, property) in graph.range_indices.keys() {
out.push(IndexInfo {
name: format!("{node_type}.{property}"),
kind: IndexKind::Range,
entity_type: "NODE",
labels_or_types: vec![node_type.clone()],
properties: vec![property.clone()],
state: "ONLINE",
});
}
out.sort_by(|a, b| {
a.name
.cmp(&b.name)
.then_with(|| (a.kind as u8).cmp(&(b.kind as u8)))
});
out
}
#[derive(Debug, Clone)]
pub(crate) struct ConstraintInfo {
pub name: String,
pub kind: ConstraintKind,
pub entity_type: &'static str,
pub labels_or_types: Vec<String>,
pub properties: Vec<String>,
pub property_type: Option<DeclaredType>,
}
impl ConstraintInfo {
pub(crate) fn neo4j_type(&self) -> &'static str {
match self.kind {
ConstraintKind::Unique => "UNIQUENESS",
ConstraintKind::NodeKey => "NODE_KEY",
ConstraintKind::NotNull => "NODE_PROPERTY_EXISTENCE",
ConstraintKind::PropertyType => "NODE_PROPERTY_TYPE",
}
}
}
pub(crate) fn collect_constraints_structured(graph: &DirGraph) -> Vec<ConstraintInfo> {
let mut out: Vec<ConstraintInfo> = Vec::new();
let mut covered: HashSet<(String, String)> = HashSet::new();
for (node_type, properties) in graph.list_unique_constraints() {
let kind = graph.unique_kind_for(&node_type, &properties);
if kind == ConstraintKind::NodeKey {
for property in &properties {
covered.insert((node_type.clone(), property.clone()));
}
}
out.push(ConstraintInfo {
name: constraint_name(graph, &node_type, &properties),
kind,
entity_type: "NODE",
labels_or_types: vec![node_type.clone()],
properties,
property_type: None,
});
}
for (node_type, property) in graph.list_not_null_constraints() {
if covered.contains(&(node_type.clone(), property.clone())) {
continue;
}
let properties = vec![property];
out.push(ConstraintInfo {
name: constraint_name(graph, &node_type, &properties),
kind: ConstraintKind::NotNull,
entity_type: "NODE",
labels_or_types: vec![node_type.clone()],
properties,
property_type: None,
});
}
for (node_type, property, declared) in graph.list_property_type_constraints() {
let properties = vec![property];
out.push(ConstraintInfo {
name: constraint_name(graph, &node_type, &properties),
kind: ConstraintKind::PropertyType,
entity_type: "NODE",
labels_or_types: vec![node_type.clone()],
properties,
property_type: Some(declared),
});
}
out.sort_by(|a, b| {
a.name
.cmp(&b.name)
.then_with(|| a.neo4j_type().cmp(b.neo4j_type()))
});
out
}
fn constraint_name(graph: &DirGraph, node_type: &str, properties: &[String]) -> String {
graph
.name_for_constraint(node_type, properties)
.map(str::to_string)
.unwrap_or_else(|| crate::graph::constraints::descriptor(node_type, properties))
}
pub(crate) fn collect_relationship_types(graph: &DirGraph) -> Vec<String> {
let mut types: HashSet<String> = graph.connection_type_metadata.keys().cloned().collect();
types.extend(graph.get_edge_type_counts().keys().cloned());
let mut out: Vec<String> = types.into_iter().collect();
out.sort();
out
}
pub(crate) fn collect_property_keys(graph: &DirGraph) -> Vec<String> {
let mut keys: HashSet<String> = HashSet::new();
for props in graph.node_type_metadata.values() {
keys.extend(props.keys().cloned());
}
for info in graph.connection_type_metadata.values() {
keys.extend(info.property_types.keys().cloned());
}
let mut out: Vec<String> = keys.into_iter().collect();
out.sort();
out
}
pub fn compute_schema(graph: &DirGraph) -> SchemaOverview {
let _arena_guard = graph.graph.begin_query();
let mut node_types: Vec<(String, NodeTypeOverview)> = graph
.type_indices
.iter()
.map(|(nt, indices)| {
let properties = graph
.node_type_metadata
.get(nt)
.cloned()
.unwrap_or_default();
(
nt.to_string(),
NodeTypeOverview {
count: indices.len(),
properties,
},
)
})
.collect();
node_types.sort_by(|a, b| a.0.cmp(&b.0));
let connection_types = compute_connection_type_stats(graph);
let mut indexes: Vec<String> = collect_indexes_structured(graph)
.into_iter()
.map(|idx| match idx.kind {
IndexKind::Equality => format!("{}.{}", idx.labels_or_types[0], idx.properties[0]),
IndexKind::Composite => {
format!("{}.({})", idx.labels_or_types[0], idx.properties.join(", "))
}
IndexKind::Range => format!("{}.{} [range]", idx.labels_or_types[0], idx.properties[0]),
})
.collect();
indexes.sort();
SchemaOverview {
node_types,
connection_types,
indexes,
node_count: graph.graph.node_count(),
edge_count: graph.graph.edge_count(),
}
}
pub(super) fn is_null_value(v: &Value) -> bool {
match v {
Value::Null => true,
Value::Float64(f) => f.is_nan(),
_ => false,
}
}
pub(super) fn value_type_name(v: &Value) -> &'static str {
match v {
Value::String(_) => "str",
Value::Int64(_) => "int",
Value::Float64(_) => "float",
Value::Boolean(_) => "bool",
Value::DateTime(_) => "datetime",
Value::Timestamp(_) => "timestamp",
Value::UniqueId(_) => "uniqueid",
Value::Point { .. } => "point",
Value::Duration { .. } => "duration",
Value::Null => "unknown",
Value::NodeRef(_) => "noderef",
Value::List(_) => "list",
Value::Map(_) => "map",
Value::Node(_) => "node",
Value::Relationship(_) => "relationship",
Value::Path(_) => "path",
}
}
pub(super) fn value_display_compact(v: &Value, truncate_at: Option<usize>) -> String {
match v {
Value::String(s) => match truncate_at {
Some(n) if n >= 4 && s.chars().count() > n => {
let truncated: String = s.chars().take(n - 3).collect();
format!("{}...", truncated)
}
_ => s.clone(),
},
Value::Int64(i) => i.to_string(),
Value::Float64(f) => format!("{}", f),
Value::Boolean(b) => {
if *b {
"true"
} else {
"false"
}
}
.to_string(),
Value::DateTime(d) => d.to_string(),
Value::Timestamp(d) => d.to_string(),
Value::UniqueId(u) => u.to_string(),
Value::Point { lat, lon } => format!("({},{})", lat, lon),
Value::Duration {
months,
days,
seconds,
} => format!("dur(M={},D={},S={})", months, days, seconds),
Value::NodeRef(idx) => format!("node#{}", idx),
Value::Null => String::new(),
Value::List(_)
| Value::Map(_)
| Value::Node(_)
| Value::Relationship(_)
| Value::Path(_) => crate::datatypes::values::format_value(v),
}
}
struct PropAccum {
non_null: usize,
value_set: HashSet<Value>,
value_cap: usize,
first_type: Option<&'static str>,
}
impl PropAccum {
fn new(cap: usize) -> Self {
Self {
non_null: 0,
value_set: HashSet::new(),
value_cap: cap,
first_type: None,
}
}
fn add(&mut self, v: &Value) {
if !is_null_value(v) {
self.non_null += 1;
if self.value_set.len() < self.value_cap {
self.value_set.insert(v.clone());
}
if self.first_type.is_none() {
self.first_type = Some(value_type_name(v));
}
}
}
#[inline]
fn needs_value(&self) -> bool {
self.value_set.len() < self.value_cap || self.first_type.is_none()
}
fn add_column(&mut self, col: &TypedColumn, rows: &[u32]) -> bool {
let mut yielded = false;
match col {
TypedColumn::Mixed { .. } => {
for &row in rows {
if let Some(value) = col.get_ref(row) {
yielded = true;
self.add(value);
}
}
}
TypedColumn::Float64 { .. } => {
for &row in rows {
if let Some(value) = col.get(row) {
yielded = true;
self.add(&value);
}
}
}
_ => {
for &row in rows {
if self.needs_value() {
if let Some(value) = col.get(row) {
yielded = true;
self.add(&value);
}
} else if col.is_present(row) {
yielded = true;
self.non_null += 1;
}
}
}
}
yielded
}
}
fn builtin_accum_keys() -> (InternedKey, InternedKey) {
(InternedKey::from_str("id"), InternedKey::from_str("title"))
}
#[cfg(test)]
thread_local! {
static FORCE_ROW_MAJOR_STATS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[cfg(test)]
pub(crate) fn with_row_major_stats<R>(f: impl FnOnce() -> R) -> R {
FORCE_ROW_MAJOR_STATS.set(true);
let out = f();
FORCE_ROW_MAJOR_STATS.set(false);
out
}
fn columnar_scan_rows(
graph: &DirGraph,
store: &crate::graph::storage::ColumnStore,
scan_indices: &[petgraph::graph::NodeIndex],
) -> Option<Vec<u32>> {
#[cfg(test)]
if FORCE_ROW_MAJOR_STATS.get() {
return None;
}
if store.has_mmap_base() || store.has_overflow() {
return None;
}
let mut rows = Vec::with_capacity(scan_indices.len());
for &idx in scan_indices {
let node = graph.node_view(idx)?;
let row = node.data().properties.columnar_row_id()?;
if !store.is_tombstoned(row) {
rows.push(row);
}
}
Some(rows)
}
fn accumulate_property_values(
graph: &DirGraph,
node_type: &str,
scan_indices: &[petgraph::graph::NodeIndex],
value_cap: usize,
accum: &mut FxHashMap<InternedKey, PropAccum>,
) {
let (id_key, title_key) = builtin_accum_keys();
let store = graph.graph.column_store(InternedKey::from_str(node_type));
let column_major = store
.and_then(|store| columnar_scan_rows(graph, store, scan_indices).map(|rows| (store, rows)));
for &idx in scan_indices {
let Some(node) = graph.node_view(idx) else {
continue;
};
accum
.entry(id_key)
.or_insert_with(|| PropAccum::new(value_cap))
.add(&node.id());
accum
.entry(title_key)
.or_insert_with(|| PropAccum::new(value_cap))
.add(&node.title());
if column_major.is_none() {
for (key, value) in node.property_pairs() {
accum
.entry(key)
.or_insert_with(|| PropAccum::new(value_cap))
.add(&value);
}
}
}
let Some((store, rows)) = column_major else {
return;
};
for (slot, key) in store.schema().iter() {
let Some(col) = store.column(slot as usize) else {
continue;
};
match accum.entry(key) {
std::collections::hash_map::Entry::Occupied(mut existing) => {
existing.get_mut().add_column(col, &rows);
}
std::collections::hash_map::Entry::Vacant(vacant) => {
let mut fresh = PropAccum::new(value_cap);
if fresh.add_column(col, &rows) {
vacant.insert(fresh);
}
}
}
}
}
fn resolve_accum_names(
graph: &DirGraph,
interned: FxHashMap<InternedKey, PropAccum>,
) -> HashMap<String, PropAccum> {
let (id_key, title_key) = builtin_accum_keys();
let mut out = HashMap::with_capacity(interned.len());
for (key, accum) in interned {
let name = if key == id_key {
"id"
} else if key == title_key {
"title"
} else if let Some(name) = graph.interner.try_resolve(key) {
name
} else {
continue;
};
out.insert(name.to_string(), accum);
}
out
}
pub fn compute_property_stats(
graph: &DirGraph,
node_type: &str,
max_values: usize,
sample_size: Option<usize>,
) -> Result<Vec<PropertyStatInfo>, String> {
let _arena_guard = graph.graph.begin_query();
let node_indices = graph
.type_indices
.get(node_type)
.ok_or_else(|| format!("Node type '{}' not found", node_type))?;
let total_nodes = node_indices.len();
let value_cap = if max_values > 0 {
max_values + 1
} else {
usize::MAX };
let (scan_indices, sample_count): (Vec<petgraph::graph::NodeIndex>, usize) = match sample_size {
Some(n) if n > 0 && n < total_nodes => {
let step = total_nodes / n;
let sampled: Vec<_> = (0..n).filter_map(|i| node_indices.get(i * step)).collect();
let count = sampled.len();
(sampled, count)
}
_ => {
(node_indices.to_vec(), total_nodes)
}
};
let (id_key, title_key) = builtin_accum_keys();
let mut interned_accum: FxHashMap<InternedKey, PropAccum> = FxHashMap::default();
interned_accum.insert(title_key, PropAccum::new(value_cap));
interned_accum.insert(id_key, PropAccum::new(value_cap));
if sample_size.is_some() {
if let Some(schema) = graph.type_schemas.get(node_type) {
for slot_key in schema.iter() {
interned_accum
.entry(slot_key.1)
.or_insert_with(|| PropAccum::new(value_cap));
}
}
}
accumulate_property_values(
graph,
node_type,
&scan_indices,
value_cap,
&mut interned_accum,
);
let mut accum = resolve_accum_names(graph, interned_accum);
let scale_factor = if sample_count < total_nodes && sample_count > 0 {
total_nodes as f64 / sample_count as f64
} else {
1.0
};
let mut results = Vec::new();
results.push(PropertyStatInfo {
property_name: "type".to_string(),
type_string: "str".to_string(),
non_null: total_nodes,
unique: 1,
values: Some(vec![Value::String(node_type.to_string())]),
sample: None,
approx: false, });
let sampled = sample_count < total_nodes;
let builtins = ["title", "id"];
let mut discovered: Vec<String> = accum
.keys()
.filter(|k| !builtins.contains(&k.as_str()))
.cloned()
.collect();
discovered.sort();
let ordered: Vec<String> = builtins
.iter()
.map(|s| s.to_string())
.chain(discovered)
.collect();
let metadata = graph.node_type_metadata.get(node_type);
for prop_name in &ordered {
if let Some(pa) = accum.remove(prop_name) {
let type_string = metadata
.and_then(|meta| meta.get(prop_name))
.cloned()
.unwrap_or_else(|| pa.first_type.unwrap_or("unknown").to_string());
let unique = pa.value_set.len();
let capped = pa.value_cap != usize::MAX && unique >= pa.value_cap;
let approx = sampled || capped;
let non_null = (pa.non_null as f64 * scale_factor).round() as usize;
let (values, sample) = if max_values > 0 && unique <= max_values && unique > 0 {
let mut vals: Vec<Value> = pa.value_set.into_iter().collect();
vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
(Some(vals), None)
} else if unique > 0 {
let sample = pa.value_set.into_iter().next();
(None, sample)
} else {
(None, None)
};
results.push(PropertyStatInfo {
property_name: prop_name.clone(),
type_string,
non_null,
unique,
values,
sample,
approx,
});
}
}
Ok(results)
}
pub fn compute_neighbors_schema(
graph: &DirGraph,
node_type: &str,
) -> Result<NeighborsSchema, String> {
let _arena_guard = graph.graph.begin_query();
let node_indices = graph
.type_indices
.get(node_type)
.ok_or_else(|| format!("Node type '{}' not found", node_type))?;
let mut outgoing: HashMap<(String, String), usize> = HashMap::new();
let mut incoming: HashMap<(String, String), usize> = HashMap::new();
let g = &graph.graph;
for node_idx in node_indices.iter() {
for edge_ref in g.edges_directed(node_idx, Direction::Outgoing) {
if let Some(target_node) = graph.node_view(edge_ref.target()) {
let key = (
edge_ref
.weight()
.connection_type_str(&graph.interner)
.to_string(),
target_node.node_type_str(&graph.interner).to_string(),
);
*outgoing.entry(key).or_insert(0) += 1;
}
}
for edge_ref in g.edges_directed(node_idx, Direction::Incoming) {
if let Some(source_node) = graph.node_view(edge_ref.source()) {
let key = (
edge_ref
.weight()
.connection_type_str(&graph.interner)
.to_string(),
source_node.node_type_str(&graph.interner).to_string(),
);
*incoming.entry(key).or_insert(0) += 1;
}
}
}
let mut outgoing_list: Vec<NeighborConnection> = outgoing
.into_iter()
.map(|((ct, ot), count)| NeighborConnection {
connection_type: ct,
other_type: ot,
count,
})
.collect();
outgoing_list.sort_by(|a, b| {
(&a.connection_type, &a.other_type).cmp(&(&b.connection_type, &b.other_type))
});
let mut incoming_list: Vec<NeighborConnection> = incoming
.into_iter()
.map(|((ct, ot), count)| NeighborConnection {
connection_type: ct,
other_type: ot,
count,
})
.collect();
incoming_list.sort_by(|a, b| {
(&a.connection_type, &a.other_type).cmp(&(&b.connection_type, &b.other_type))
});
Ok(NeighborsSchema {
outgoing: outgoing_list,
incoming: incoming_list,
})
}
pub fn compute_all_neighbors_schemas(graph: &DirGraph) -> HashMap<String, NeighborsSchema> {
let _arena_guard = graph.graph.begin_query();
let mut edge_counts: HashMap<(String, String, String), usize> = HashMap::new();
let g = &graph.graph;
for edge_ref in g.edge_references() {
if let (Some(source), Some(target)) = (
graph.node_view(edge_ref.source()),
graph.node_view(edge_ref.target()),
) {
let conn_type = edge_ref
.weight()
.connection_type_str(&graph.interner)
.to_string();
let key = (
source.node_type_str(&graph.interner).to_string(),
conn_type,
target.node_type_str(&graph.interner).to_string(),
);
*edge_counts.entry(key).or_insert(0) += 1;
}
}
let mut result: HashMap<String, NeighborsSchema> = HashMap::new();
for ((src_type, conn_type, tgt_type), count) in &edge_counts {
let schema = result
.entry(src_type.clone())
.or_insert_with(|| NeighborsSchema {
outgoing: Vec::new(),
incoming: Vec::new(),
});
schema.outgoing.push(NeighborConnection {
connection_type: conn_type.clone(),
other_type: tgt_type.clone(),
count: *count,
});
let schema = result
.entry(tgt_type.clone())
.or_insert_with(|| NeighborsSchema {
outgoing: Vec::new(),
incoming: Vec::new(),
});
schema.incoming.push(NeighborConnection {
connection_type: conn_type.clone(),
other_type: src_type.clone(),
count: *count,
});
}
for schema in result.values_mut() {
schema.outgoing.sort_by(|a, b| {
(&a.connection_type, &a.other_type).cmp(&(&b.connection_type, &b.other_type))
});
schema.incoming.sort_by(|a, b| {
(&a.connection_type, &a.other_type).cmp(&(&b.connection_type, &b.other_type))
});
}
result
}
pub fn compute_sample<'a>(
graph: &'a DirGraph,
node_type: &str,
n: usize,
) -> Result<Vec<crate::graph::storage::NodeView<'a>>, String> {
let _arena_guard = graph.graph.begin_query();
let node_indices = graph
.type_indices
.get(node_type)
.ok_or_else(|| format!("Node type '{}' not found", node_type))?;
let mut result = Vec::with_capacity(n.min(node_indices.len()));
for idx in node_indices.iter().take(n) {
if let Some(node) = graph.node_view(idx) {
result.push(node);
}
}
Ok(result)
}
#[cfg(test)]
mod column_major_stats_tests {
use super::*;
use crate::datatypes::DataFrame;
use crate::graph::dir_graph::DirGraph;
fn wide_fixture(n: i64) -> DirGraph {
let mut graph = DirGraph::new();
let columns: Vec<String> = [
"key",
"label",
"bucket",
"unique_text",
"count",
"ratio",
"flag",
"sparse",
"vec",
"id",
]
.iter()
.map(|s| s.to_string())
.collect();
let rows: Vec<Vec<Value>> = (0..n)
.map(|i| {
vec![
Value::Int64(i),
Value::String(format!("Item_{i}")),
Value::String(format!("bucket_{}", i % 3)),
Value::String(format!("text-{i}-{}", i * 7)),
Value::Int64(i * 10),
if i % 11 == 0 {
Value::Float64(f64::NAN)
} else {
Value::Float64(i as f64 / 3.0)
},
Value::Boolean(i % 2 == 0),
if i % 4 == 0 {
Value::Null
} else {
Value::String(format!("s{i}"))
},
Value::List(vec![Value::Int64(i), Value::Int64(i + 1)]),
Value::String(format!("shadow-{i}")),
]
})
.collect();
let df = DataFrame::from_cypher_rows(columns, rows).unwrap();
crate::graph::mutation::maintain::add_nodes(
&mut graph,
df,
"Item".to_string(),
"key".to_string(),
Some("label".to_string()),
None,
)
.unwrap();
graph.enable_columnar();
graph
}
pub(super) fn wide_probe_fixture(n: i64) -> DirGraph {
let mut graph = DirGraph::new();
let names = [
"key",
"label",
"citation",
"summary",
"section",
"court",
"docket",
"para",
"keywords",
"source_url",
"decided",
"pages",
];
let columns: Vec<String> = names.iter().map(|s| s.to_string()).collect();
let rows: Vec<Vec<Value>> = (0..n)
.map(|i| {
vec![
Value::Int64(i),
Value::String(format!("Decision {i}")),
Value::String(format!("HR-{}-{i}-A", 1900 + (i % 120))),
Value::String(format!(
"A summary of decision {i} running to a realistic width for a \
law-shaped corpus, with clause {} and reference {}.",
i % 37,
i * 13
)),
Value::String(format!("section_{}", i % 24)),
Value::String(format!("court_{}", i % 9)),
Value::String(format!("{}-{:06}", i % 4, i)),
Value::String(format!("paragraph {} of {}", i % 60, 60)),
Value::String(format!("kw_{},kw_{},kw_{}", i % 11, i % 17, i % 23)),
Value::String(format!("https://example.invalid/decisions/{i}")),
Value::Int64(1900 + (i % 120)),
Value::Int64(i % 400),
]
})
.collect();
let df = DataFrame::from_cypher_rows(columns, rows).unwrap();
crate::graph::mutation::maintain::add_nodes(
&mut graph,
df,
"Item".to_string(),
"key".to_string(),
Some("label".to_string()),
None,
)
.unwrap();
graph.enable_columnar();
graph
}
fn comparable(stats: &[PropertyStatInfo]) -> Vec<(String, String, usize, usize, bool, bool)> {
stats
.iter()
.map(|s| {
(
s.property_name.clone(),
s.type_string.clone(),
s.non_null,
s.unique,
s.approx,
s.values.is_some(),
)
})
.collect()
}
fn both_routes(graph: &DirGraph, max_values: usize, sample: Option<usize>) {
let column_major = compute_property_stats(graph, "Item", max_values, sample).unwrap();
let row_major =
with_row_major_stats(|| compute_property_stats(graph, "Item", max_values, sample))
.unwrap();
assert_eq!(
comparable(&column_major),
comparable(&row_major),
"column-major stats diverged from the row loop (max_values={max_values}, \
sample={sample:?})"
);
for (col, row) in column_major.iter().zip(row_major.iter()) {
assert_eq!(
col.values, row.values,
"enumerated values diverged for {}",
col.property_name
);
}
}
#[test]
fn column_major_stats_match_the_row_loop() {
let graph = wide_fixture(64);
both_routes(&graph, 16, None);
both_routes(&graph, 0, None);
both_routes(&graph, 1024, None);
both_routes(&graph, 16, Some(8));
}
#[test]
fn column_major_stats_match_the_row_loop_after_writes() {
let mut graph = wide_fixture(48);
let params = std::collections::HashMap::new();
let opts = crate::graph::session::ExecuteOptions::eager(¶ms);
crate::graph::session::execute_mut(
&mut graph,
"MATCH (n:Item) WHERE n.count < 100 SET n.bucket = 'relocated-to-a-much-longer-value'",
&opts,
)
.unwrap();
crate::graph::session::execute_mut(
&mut graph,
"MATCH (n:Item) WHERE n.count < 30 SET n.late = 'added'",
&opts,
)
.unwrap();
both_routes(&graph, 16, None);
crate::graph::session::execute_mut(
&mut graph,
"MATCH (n:Item) WHERE n.count > 400 DELETE n",
&opts,
)
.unwrap();
both_routes(&graph, 16, None);
both_routes(&graph, 0, None);
}
#[test]
fn the_fixture_takes_the_column_major_path() {
let graph = wide_fixture(8);
let store = graph
.graph
.column_store(InternedKey::from_str("Item"))
.expect("fixture must be columnar");
let indices: Vec<_> = graph.type_indices.get("Item").unwrap().to_vec();
assert_eq!(
columnar_scan_rows(&graph, store, &indices).map(|rows| rows.len()),
Some(8),
"the fixture must qualify for the column-major path, or the \
equivalence test compares the row loop with itself"
);
assert!(
with_row_major_stats(|| columnar_scan_rows(&graph, store, &indices)).is_none(),
"the decline hook must actually decline"
);
}
}
#[cfg(test)]
mod column_major_stats_probe {
use super::column_major_stats_tests::wide_probe_fixture;
use super::{compute_property_stats, with_row_major_stats};
use std::time::Instant;
fn min_of(rounds: usize, mut f: impl FnMut()) -> f64 {
let mut best = f64::MAX;
for _ in 0..rounds {
let start = Instant::now();
f();
best = best.min(start.elapsed().as_secs_f64() * 1e3);
}
best
}
#[test]
#[ignore = "perf probe — release profile only"]
fn stats_column_major_ab() {
if cfg!(debug_assertions) {
panic!("run this probe with --release; a debug-profile number is invalid");
}
for nodes in [5_000i64, 50_000] {
let graph = wide_probe_fixture(nodes);
let _ = compute_property_stats(&graph, "Item", 16, None).unwrap();
let _ = with_row_major_stats(|| compute_property_stats(&graph, "Item", 16, None));
let rounds = if nodes <= 5_000 { 40 } else { 10 };
let row = min_of(rounds, || {
with_row_major_stats(|| compute_property_stats(&graph, "Item", 16, None)).unwrap();
});
let col = min_of(rounds, || {
compute_property_stats(&graph, "Item", 16, None).unwrap();
});
println!(
"compute_property_stats n={nodes:>6} row-major {row:8.3} ms \
column-major {col:8.3} ms speedup {:.2}x",
row / col
);
}
}
}