use std::collections::BTreeMap;
use std::sync::atomic::{AtomicU64, Ordering};
use nodedb_types::DatabaseId;
use nodedb_types::diagnostic::DiagnosticLayer;
use serde_json::{Map, Value as JsonValue};
use tracing::info_span;
static GRAPH_STATS_CALLS: AtomicU64 = AtomicU64::new(0);
#[allow(dead_code)]
pub fn graph_stats_calls_total() -> u64 {
GRAPH_STATS_CALLS.load(Ordering::Relaxed)
}
use crate::bridge::envelope::PhysicalPlan;
use crate::control::security::identity::AuthenticatedIdentity;
use crate::control::server::broadcast::broadcast_to_all_cores;
use crate::control::server::response_shape::types::{DdlColType, ShapedRows};
use crate::control::state::SharedState;
use crate::engine::graph::edge_store::stats::CollectionStats;
use crate::types::TraceId;
use nodedb_physical::physical_plan::GraphOp;
use super::super::super::result::{DdlError, DdlResult};
use super::support::ddl_err;
pub async fn show_graph_stats(
state: &SharedState,
identity: &AuthenticatedIdentity,
database_id: DatabaseId,
collection: Option<String>,
verbose: bool,
as_of: Option<i64>,
) -> Result<Vec<DdlResult>, DdlError> {
GRAPH_STATS_CALLS.fetch_add(1, Ordering::Relaxed);
let scope = if collection.is_some() {
"collection"
} else {
"tenant"
};
let _span = info_span!(
"graph.stats",
layer = DiagnosticLayer::WritePath.as_str(),
tenant_id = identity.tenant_id.as_u64(),
scope = scope,
collection = ?collection,
verbose,
as_of = ?as_of,
);
if let Some(ref name) = collection {
super::support::ensure_collection_active(
state,
database_id,
identity.tenant_id.as_u64(),
name,
)?;
}
let plan = PhysicalPlan::Graph(GraphOp::Stats {
collection: collection.clone(),
as_of: as_of.or(Some(i64::MAX)),
});
let resp = broadcast_to_all_cores(state, identity.tenant_id, database_id, plan, TraceId::ZERO)
.await
.map_err(|e| ddl_err("58000", format!("graph stats dispatch failed: {e}")))?;
let merged: Vec<CollectionStats> = decode_merged_stats(resp.payload.as_bytes())
.map_err(|e| ddl_err("XX000", format!("graph stats decode failed: {e}")))?;
let aggregated = aggregate_by_collection(merged);
let aggregated = if collection.is_none() {
filter_active_collections(state, database_id, identity.tenant_id.as_u64(), aggregated)
} else {
aggregated
};
if verbose {
Ok(encode_verbose_response(aggregated))
} else {
Ok(encode_compact_response(aggregated))
}
}
fn decode_merged_stats(payload: &[u8]) -> crate::Result<Vec<CollectionStats>> {
if payload.is_empty() {
return Ok(Vec::new());
}
zerompk::from_msgpack(payload).map_err(|e| crate::Error::Serialization {
format: "msgpack".into(),
detail: e.to_string(),
})
}
fn filter_active_collections(
state: &SharedState,
database_id: DatabaseId,
tenant_id: u64,
rows: Vec<CollectionStats>,
) -> Vec<CollectionStats> {
let catalog = state.credentials.catalog();
let all_collections = catalog
.load_collections_for_tenant(database_id, tenant_id)
.unwrap_or_default();
let active: std::collections::BTreeSet<&str> = all_collections
.iter()
.filter(|c| c.is_active)
.map(|c| c.name.as_str())
.collect();
rows.into_iter()
.filter(|r| active.contains(r.collection.as_str()))
.collect()
}
fn aggregate_by_collection(entries: Vec<CollectionStats>) -> Vec<CollectionStats> {
let mut label_acc: BTreeMap<String, BTreeMap<String, u64>> = BTreeMap::new();
let mut historical_edges: BTreeMap<
String,
std::collections::BTreeSet<(String, String, String)>,
> = BTreeMap::new();
let mut by_name: BTreeMap<String, CollectionStats> = BTreeMap::new();
for e in entries {
let acc = by_name
.entry(e.collection.clone())
.or_insert_with(|| CollectionStats::zero(e.collection.clone()));
acc.edge_count = acc.edge_count.saturating_add(e.edge_count);
acc.distinct_node_count = acc
.distinct_node_count
.saturating_add(e.distinct_node_count);
let labels = label_acc.entry(e.collection.clone()).or_default();
for (label, count) in e.labels {
let slot = labels.entry(label).or_insert(0);
*slot = slot.saturating_add(count);
}
historical_edges
.entry(e.collection)
.or_default()
.extend(e.logical_edges);
}
let mut result: Vec<CollectionStats> = Vec::with_capacity(by_name.len());
for (collection, mut acc) in by_name {
let exact_edges = historical_edges.remove(&collection).unwrap_or_default();
if exact_edges.is_empty() {
let labels_map = label_acc.remove(&collection).unwrap_or_default();
acc.labels = labels_map.into_iter().collect();
} else {
let mut nodes = std::collections::BTreeSet::new();
let mut labels = BTreeMap::<String, u64>::new();
for (src, label, dst) in &exact_edges {
nodes.insert(src);
nodes.insert(dst);
*labels.entry(label.clone()).or_default() += 1;
}
acc.edge_count = exact_edges.len() as u64;
acc.distinct_node_count = nodes.len() as u64;
acc.labels = labels.into_iter().collect();
acc.logical_edges = exact_edges.into_iter().collect();
}
acc.distinct_label_count = acc.labels.len() as u64;
result.push(acc);
}
result
}
fn encode_compact_response(rows: Vec<CollectionStats>) -> Vec<DdlResult> {
let columns = vec![
"collection".to_string(),
"node_count".to_string(),
"edge_count".to_string(),
"distinct_label_count".to_string(),
"labels".to_string(),
];
let column_types = vec![
DdlColType::Text,
DdlColType::Int8,
DdlColType::Int8,
DdlColType::Int8,
DdlColType::Text,
];
let mut data_rows = Vec::with_capacity(rows.len());
for r in rows {
let labels_json = serde_json::Value::Array(
r.labels
.iter()
.map(|(name, count)| {
let mut m = serde_json::Map::new();
m.insert("label".into(), serde_json::Value::String(name.clone()));
m.insert("count".into(), serde_json::Value::Number((*count).into()));
serde_json::Value::Object(m)
})
.collect(),
)
.to_string();
let mut row = Map::new();
row.insert("collection".to_string(), JsonValue::String(r.collection));
row.insert(
"node_count".to_string(),
JsonValue::String((r.distinct_node_count as i64).to_string()),
);
row.insert(
"edge_count".to_string(),
JsonValue::String((r.edge_count as i64).to_string()),
);
row.insert(
"distinct_label_count".to_string(),
JsonValue::String((r.distinct_label_count as i64).to_string()),
);
row.insert("labels".to_string(), JsonValue::String(labels_json));
data_rows.push(row);
}
vec![DdlResult::Rows(ShapedRows {
columns,
column_types,
rows: data_rows,
notice: None,
})]
}
fn encode_verbose_response(rows: Vec<CollectionStats>) -> Vec<DdlResult> {
let columns = vec![
"collection".to_string(),
"label".to_string(),
"edge_count".to_string(),
];
let column_types = vec![DdlColType::Text, DdlColType::Text, DdlColType::Int8];
let mut data_rows = Vec::new();
for r in &rows {
for (label, count) in &r.labels {
let mut row = Map::new();
row.insert(
"collection".to_string(),
JsonValue::String(r.collection.clone()),
);
row.insert("label".to_string(), JsonValue::String(label.clone()));
row.insert(
"edge_count".to_string(),
JsonValue::String((*count as i64).to_string()),
);
data_rows.push(row);
}
}
vec![DdlResult::Rows(ShapedRows {
columns,
column_types,
rows: data_rows,
notice: None,
})]
}