use crate::digest::{sanitize, SEP};
use crate::explain_digest::predicate_summary;
use crate::memory::brief::{brief, BriefOptions, SchemaBrief};
use crate::memory_schema::{PROVISIONAL_LABEL, PROVISIONAL_PROP};
use crate::{CmpOp, Filter, GraphDb, PredicateSummary, Value};
use core_storage::fs::Fs;
use serde::Serialize;
pub const SCHEMA_LIST_CAP: usize = 20;
pub const PROVISIONAL_SAMPLE: usize = 10;
pub const SCHEMA_MAX_BYTES: usize = 12_000;
fn cap_schema(out: String) -> String {
if out.len() <= SCHEMA_MAX_BYTES {
return out;
}
let note = format!(
"(schema truncated at {SCHEMA_MAX_BYTES} bytes; json: true returns the whole report)\n"
);
let mut kept = String::new();
for line in out.lines() {
if kept.len() + line.len() + 1 + note.len() > SCHEMA_MAX_BYTES {
break;
}
kept.push_str(line);
kept.push('\n');
}
let last_start = kept.trim_end_matches('\n').rfind('\n').map_or(0, |i| i + 1);
let last = kept[last_start..].trim_end_matches('\n');
if last.ends_with(':') && !last.starts_with(' ') {
kept.truncate(last_start);
}
kept.push_str(¬e);
kept
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RuleBrief {
pub name: String,
pub src_label: String,
pub dst_label: String,
pub edge_type: String,
pub predicate: String,
pub namespace: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct SchemaReport {
pub brief: SchemaBrief,
pub rules: Vec<RuleBrief>,
pub fulltext: Vec<(String, String)>,
pub indexes: Vec<(String, String)>,
pub provisional: usize,
pub provisional_sample: Vec<String>,
}
pub fn provisional_keys<F: Fs>(db: &GraphDb<F>) -> Vec<String> {
let filter = Filter::Cmp {
field: PROVISIONAL_PROP.to_string(),
op: CmpOp::Eq,
value: Value::Bool(true),
};
let mut keys: Vec<String> = db
.find_nodes(PROVISIONAL_LABEL, &filter)
.into_iter()
.map(|n| n.key().to_string())
.collect();
keys.sort();
keys
}
pub fn schema_report<F: Fs>(db: &GraphDb<F>, opts: &BriefOptions) -> SchemaReport {
let mut rules: Vec<RuleBrief> = db
.rules()
.into_iter()
.map(|r| RuleBrief {
predicate: predicate_summary(&PredicateSummary::from(&r.predicate)),
name: r.name,
src_label: r.src_label,
dst_label: r.dst_label,
edge_type: r.edge_type,
namespace: r.namespace,
})
.collect();
rules.sort_by(|a, b| a.name.cmp(&b.name));
let provisional = provisional_keys(db);
SchemaReport {
brief: brief(db, opts),
rules,
fulltext: db.fulltext_pairs(),
indexes: db.index_pairs(),
provisional: provisional.len(),
provisional_sample: provisional.into_iter().take(PROVISIONAL_SAMPLE).collect(),
}
}
fn section(out: &mut String, heading: &str, items: &[String]) {
if items.is_empty() {
return;
}
out.push_str(heading);
out.push_str(":\n");
for item in items.iter().take(SCHEMA_LIST_CAP) {
out.push_str(" ");
out.push_str(item);
out.push('\n');
}
if items.len() > SCHEMA_LIST_CAP {
out.push_str(&format!(" … and {} more\n", items.len() - SCHEMA_LIST_CAP));
}
}
fn pairs(ps: &[(String, String)]) -> Vec<String> {
ps.iter()
.map(|(l, f)| format!("{}.{}", sanitize(l), sanitize(f)))
.collect()
}
#[must_use]
pub fn render_schema(r: &SchemaReport) -> String {
let b = &r.brief;
let at_least = if b.partial { "≥ " } else { "" };
let mut out = format!(
"mushroomdb schema — {at_least}{} node(s){SEP}{} edge(s){SEP}{} label(s)\n",
b.nodes,
b.edges,
b.labels.len(),
);
let labels: Vec<String> = b
.labels
.iter()
.map(|l| {
let props: Vec<String> = l.props.iter().map(|p| sanitize(p)).collect();
let hidden = if l.hidden_props > 0 {
format!(" (+{} more)", l.hidden_props)
} else {
String::new()
};
format!(
"{} ({}): {}{hidden}",
sanitize(&l.label),
l.nodes,
props.join(", ")
)
})
.collect();
section(&mut out, "labels", &labels);
let edge_types: Vec<String> = b
.edge_types
.iter()
.map(|e| {
let ends = |v: &[String]| v.iter().map(|s| sanitize(s)).collect::<Vec<_>>().join(", ");
let rule = match &e.rule {
Some(name) if e.hidden_rules > 0 => {
format!(" — rule {} (+{} more)", sanitize(name), e.hidden_rules)
}
Some(name) => format!(" — rule {}", sanitize(name)),
None => String::new(),
};
format!(
"{} ({}) {} → {}{rule}",
sanitize(&e.edge_type),
e.edges,
ends(&e.src),
ends(&e.dst)
)
})
.collect();
section(&mut out, "edge types", &edge_types);
let rules: Vec<String> = r
.rules
.iter()
.map(|rule| {
let scope = match &rule.namespace {
Some(ns) => format!("namespace {}", sanitize(ns)),
None => "global".to_string(),
};
format!(
"{}: {} → {} derives {} — {} ({scope})",
sanitize(&rule.name),
sanitize(&rule.src_label),
sanitize(&rule.dst_label),
sanitize(&rule.edge_type),
rule.predicate
)
})
.collect();
section(&mut out, "rules", &rules);
section(
&mut out,
"full-text (recall searches these)",
&pairs(&r.fulltext),
);
section(&mut out, "equality indexes", &pairs(&r.indexes));
if r.provisional > 0 {
let named: Vec<String> = r.provisional_sample.iter().map(|k| sanitize(k)).collect();
let more = r.provisional.saturating_sub(named.len());
out.push_str(&format!(
"provisional: {} — named but not yet described: {}{}\n",
r.provisional,
named.join(", "),
if more > 0 {
format!(" (+{more} more)")
} else {
String::new()
}
));
}
if b.partial {
out.push_str("(partial: the time budget ran out; counts are lower bounds)\n");
}
cap_schema(out)
}