1use crate::digest::{sanitize, SEP};
15use crate::explain_digest::predicate_summary;
16use crate::memory::brief::{brief, BriefOptions, SchemaBrief};
17use crate::memory_schema::{PROVISIONAL_LABEL, PROVISIONAL_PROP};
18use crate::{CmpOp, Filter, GraphDb, PredicateSummary, Value};
19use core_storage::fs::Fs;
20use serde::Serialize;
21
22pub const SCHEMA_LIST_CAP: usize = 20;
24
25pub const PROVISIONAL_SAMPLE: usize = 10;
27
28pub const SCHEMA_MAX_BYTES: usize = 12_000;
43
44fn cap_schema(out: String) -> String {
51 if out.len() <= SCHEMA_MAX_BYTES {
52 return out;
53 }
54 let note = format!(
55 "(schema truncated at {SCHEMA_MAX_BYTES} bytes; json: true returns the whole report)\n"
56 );
57 let mut kept = String::new();
58 for line in out.lines() {
59 if kept.len() + line.len() + 1 + note.len() > SCHEMA_MAX_BYTES {
60 break;
61 }
62 kept.push_str(line);
63 kept.push('\n');
64 }
65 let last_start = kept.trim_end_matches('\n').rfind('\n').map_or(0, |i| i + 1);
66 let last = kept[last_start..].trim_end_matches('\n');
67 if last.ends_with(':') && !last.starts_with(' ') {
68 kept.truncate(last_start);
69 }
70 kept.push_str(¬e);
71 kept
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize)]
76pub struct RuleBrief {
77 pub name: String,
78 pub src_label: String,
79 pub dst_label: String,
80 pub edge_type: String,
81 pub predicate: String,
84 pub namespace: Option<String>,
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize)]
90pub struct SchemaReport {
91 pub brief: SchemaBrief,
92 pub rules: Vec<RuleBrief>,
93 pub fulltext: Vec<(String, String)>,
95 pub indexes: Vec<(String, String)>,
97 pub provisional: usize,
100 pub provisional_sample: Vec<String>,
102}
103
104pub fn provisional_keys<F: Fs>(db: &GraphDb<F>) -> Vec<String> {
109 let filter = Filter::Cmp {
110 field: PROVISIONAL_PROP.to_string(),
111 op: CmpOp::Eq,
112 value: Value::Bool(true),
113 };
114 let mut keys: Vec<String> = db
115 .find_nodes(PROVISIONAL_LABEL, &filter)
116 .into_iter()
117 .map(|n| n.key().to_string())
118 .collect();
119 keys.sort();
120 keys
121}
122
123pub fn schema_report<F: Fs>(db: &GraphDb<F>, opts: &BriefOptions) -> SchemaReport {
125 let mut rules: Vec<RuleBrief> = db
126 .rules()
127 .into_iter()
128 .map(|r| RuleBrief {
129 predicate: predicate_summary(&PredicateSummary::from(&r.predicate)),
130 name: r.name,
131 src_label: r.src_label,
132 dst_label: r.dst_label,
133 edge_type: r.edge_type,
134 namespace: r.namespace,
135 })
136 .collect();
137 rules.sort_by(|a, b| a.name.cmp(&b.name));
138 let provisional = provisional_keys(db);
139 SchemaReport {
140 brief: brief(db, opts),
141 rules,
142 fulltext: db.fulltext_pairs(),
143 indexes: db.index_pairs(),
144 provisional: provisional.len(),
145 provisional_sample: provisional.into_iter().take(PROVISIONAL_SAMPLE).collect(),
146 }
147}
148
149fn section(out: &mut String, heading: &str, items: &[String]) {
152 if items.is_empty() {
153 return;
154 }
155 out.push_str(heading);
156 out.push_str(":\n");
157 for item in items.iter().take(SCHEMA_LIST_CAP) {
158 out.push_str(" ");
159 out.push_str(item);
160 out.push('\n');
161 }
162 if items.len() > SCHEMA_LIST_CAP {
163 out.push_str(&format!(" … and {} more\n", items.len() - SCHEMA_LIST_CAP));
164 }
165}
166
167fn pairs(ps: &[(String, String)]) -> Vec<String> {
168 ps.iter()
169 .map(|(l, f)| format!("{}.{}", sanitize(l), sanitize(f)))
170 .collect()
171}
172
173#[must_use]
175pub fn render_schema(r: &SchemaReport) -> String {
176 let b = &r.brief;
177 let at_least = if b.partial { "≥ " } else { "" };
178 let mut out = format!(
179 "mushroomdb schema — {at_least}{} node(s){SEP}{} edge(s){SEP}{} label(s)\n",
180 b.nodes,
181 b.edges,
182 b.labels.len(),
183 );
184 let labels: Vec<String> = b
185 .labels
186 .iter()
187 .map(|l| {
188 let props: Vec<String> = l.props.iter().map(|p| sanitize(p)).collect();
189 let hidden = if l.hidden_props > 0 {
190 format!(" (+{} more)", l.hidden_props)
191 } else {
192 String::new()
193 };
194 format!(
195 "{} ({}): {}{hidden}",
196 sanitize(&l.label),
197 l.nodes,
198 props.join(", ")
199 )
200 })
201 .collect();
202 section(&mut out, "labels", &labels);
203 let edge_types: Vec<String> = b
204 .edge_types
205 .iter()
206 .map(|e| {
207 let ends = |v: &[String]| v.iter().map(|s| sanitize(s)).collect::<Vec<_>>().join(", ");
208 let rule = match &e.rule {
209 Some(name) if e.hidden_rules > 0 => {
210 format!(" — rule {} (+{} more)", sanitize(name), e.hidden_rules)
211 }
212 Some(name) => format!(" — rule {}", sanitize(name)),
213 None => String::new(),
214 };
215 format!(
216 "{} ({}) {} → {}{rule}",
217 sanitize(&e.edge_type),
218 e.edges,
219 ends(&e.src),
220 ends(&e.dst)
221 )
222 })
223 .collect();
224 section(&mut out, "edge types", &edge_types);
225 let rules: Vec<String> = r
226 .rules
227 .iter()
228 .map(|rule| {
229 let scope = match &rule.namespace {
230 Some(ns) => format!("namespace {}", sanitize(ns)),
231 None => "global".to_string(),
232 };
233 format!(
234 "{}: {} → {} derives {} — {} ({scope})",
235 sanitize(&rule.name),
236 sanitize(&rule.src_label),
237 sanitize(&rule.dst_label),
238 sanitize(&rule.edge_type),
239 rule.predicate
240 )
241 })
242 .collect();
243 section(&mut out, "rules", &rules);
244 section(
245 &mut out,
246 "full-text (recall searches these)",
247 &pairs(&r.fulltext),
248 );
249 section(&mut out, "equality indexes", &pairs(&r.indexes));
250 if r.provisional > 0 {
251 let named: Vec<String> = r.provisional_sample.iter().map(|k| sanitize(k)).collect();
252 let more = r.provisional.saturating_sub(named.len());
253 out.push_str(&format!(
254 "provisional: {} — named but not yet described: {}{}\n",
255 r.provisional,
256 named.join(", "),
257 if more > 0 {
258 format!(" (+{more} more)")
259 } else {
260 String::new()
261 }
262 ));
263 }
264 if b.partial {
265 out.push_str("(partial: the time budget ran out; counts are lower bounds)\n");
266 }
267 cap_schema(out)
268}