1use crate::error::Result;
32use crate::field::document_format::DocumentFormat;
33use crate::field::manifest::FieldRoot;
34use crate::field::observe::{ObserveRequest, ObserveStats, OpenedField, observe_opened};
35use crate::field::plan::{ObservePlan, plan};
36use crate::field::provenance::{Basis, IntegrityScope, json_escape};
37use crate::field::{FieldId, FieldStore};
38use crate::limits::Limits;
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct ExplainPlan {
43 pub plan: ObservePlan,
45 pub json: String,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct ExplainActual {
52 pub stats: ObserveStats,
54 pub answer_basis: Basis,
56 pub exact: bool,
58 pub format: String,
60 pub adapter: String,
62 pub whole_source_materialized: bool,
64}
65
66impl ExplainActual {
67 pub fn to_json(&self) -> String {
69 let s = &self.stats;
70 format!(
71 concat!(
72 "{{",
73 "\"format\":\"{}\",",
74 "\"adapter\":\"{}\",",
75 "\"index_nodes_read\":{},",
76 "\"seed_nodes_fetched\":{},",
77 "\"seed_nodes_materialized\":{},",
78 "\"member_decodes\":{},",
79 "\"xml_parses\":{},",
80 "\"nodes_id_shared\":{},",
81 "\"shared_resource_ids\":{},",
82 "\"inverse_work_units\":{},",
83 "\"descriptor_bytes_read\":{},",
84 "\"descriptor_read_mode\":\"{}\",",
85 "\"manifest_bytes_read\":{},",
86 "\"index_bytes_read\":{},",
87 "\"seed_bytes_read\":{},",
88 "\"bytes_read\":{},",
89 "\"bytes_returned\":{},",
90 "\"deepened\":{},",
91 "\"whole_source_materialized\":{},",
92 "\"wall_micros\":{},",
93 "\"basis\":\"{}\",",
94 "\"exact\":{}",
95 "}}"
96 ),
97 json_escape(&self.format),
98 json_escape(&self.adapter),
99 s.index_nodes_read,
100 s.seed_nodes_fetched,
101 s.seed_nodes_materialized,
102 s.member_decodes,
103 s.xml_parses,
104 s.nodes_id_shared,
105 s.shared_resource_ids,
106 s.seed_nodes_executed.saturating_add(s.bytes_read),
107 s.descriptor_bytes_read,
108 s.descriptor_read_mode.name(),
109 s.manifest_bytes_read,
110 s.index_bytes_read,
111 s.seed_bytes_read,
112 s.bytes_read,
113 s.bytes_returned,
114 s.deepened,
115 self.whole_source_materialized,
116 s.wall_micros,
117 self.answer_basis.name(),
118 self.exact,
119 )
120 }
121}
122
123pub fn explain(
125 manifest: &FieldRoot,
126 store: &FieldStore,
127 req: &ObserveRequest,
128) -> Result<ExplainPlan> {
129 let plan = plan(manifest, store, req)?;
130 let json = plan_json(req, &plan, manifest);
131 Ok(ExplainPlan { plan, json })
132}
133
134pub fn explain_analyze(
141 store: &mut FieldStore,
142 id: &FieldId,
143 req: &ObserveRequest,
144 limits: Limits,
145) -> Result<(ExplainPlan, ExplainActual)> {
146 let started = std::time::Instant::now();
147 let opened = OpenedField::open(store, id, req, limits)?;
148 let planned = plan(opened.manifest(), store, req)?;
149 let json = plan_json(req, &planned, opened.manifest());
150 let (answer, mut stats, _) = observe_opened(store, &opened, req, limits)?;
151 stats.wall_micros = started.elapsed().as_micros().min(u128::from(u64::MAX)) as u64;
152 let fmt = DocumentFormat::from_provenance(&opened.manifest().provenance);
153 let actual = ExplainActual {
154 stats,
155 answer_basis: answer.basis,
156 exact: answer.exact,
157 format: fmt.map_or("unknown", |f| f.name()).to_string(),
158 adapter: fmt.map_or("unknown", |f| f.adapter()).to_string(),
159 whole_source_materialized: answer.integrity_scope == IntegrityScope::WholeSource,
160 };
161 Ok((
162 ExplainPlan {
163 plan: planned,
164 json,
165 },
166 actual,
167 ))
168}
169
170fn plan_json(req: &ObserveRequest, plan: &ObservePlan, manifest: &FieldRoot) -> String {
171 let fmt = DocumentFormat::from_provenance(&manifest.provenance);
172 let format_name = fmt.map_or("unknown", |f| f.name());
173 let adapter = fmt.map_or("unknown", |f| f.adapter());
174 let capability = match crate::field::capabilities::common_selector_name(&req.selector) {
175 Some(name) => format!("common:{name} -> {adapter}"),
176 None => "native".to_string(),
177 };
178 let index_route = if manifest.has_index() {
179 "hier-index"
180 } else {
181 "none"
182 };
183 format!(
184 concat!(
185 "{{",
186 "\"selector\":\"{}\",",
187 "\"representation\":\"{}\",",
188 "\"format\":\"{}\",",
189 "\"adapter\":\"{}\",",
190 "\"capability\":\"{}\",",
191 "\"index_route\":\"{}\",",
192 "\"shape\":\"{}\",",
193 "\"index_reads\":{},",
194 "\"required_nodes\":{},",
195 "\"will_materialize\":{},",
196 "\"will_not_materialize\":{}",
197 "}}"
198 ),
199 json_escape(&req.selector.canonical()),
200 json_escape(req.representation.name()),
201 json_escape(format_name),
202 json_escape(adapter),
203 json_escape(&capability),
204 index_route,
205 plan.shape.name(),
206 plan.index_reads,
207 plan.required_nodes,
208 string_array(&plan.will_materialize),
209 string_array(&plan.will_not_materialize),
210 )
211}
212
213fn string_array(items: &[String]) -> String {
214 let body = items
215 .iter()
216 .map(|s| format!("\"{}\"", json_escape(s)))
217 .collect::<Vec<_>>()
218 .join(",");
219 format!("[{body}]")
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn actual_json_has_exactly_the_documented_keys() {
228 let actual = ExplainActual {
229 stats: ObserveStats::default(),
230 answer_basis: Basis::DirectlyObserved,
231 exact: true,
232 format: "unknown".to_string(),
233 adapter: "unknown".to_string(),
234 whole_source_materialized: false,
235 };
236 assert_eq!(
237 actual.to_json(),
238 "{\"format\":\"unknown\",\"adapter\":\"unknown\",\"index_nodes_read\":0,\"seed_nodes_fetched\":0,\"seed_nodes_materialized\":0,\"member_decodes\":0,\"xml_parses\":0,\"nodes_id_shared\":0,\"shared_resource_ids\":0,\"inverse_work_units\":0,\"descriptor_bytes_read\":0,\"descriptor_read_mode\":\"full\",\"manifest_bytes_read\":0,\"index_bytes_read\":0,\"seed_bytes_read\":0,\"bytes_read\":0,\"bytes_returned\":0,\"deepened\":false,\"whole_source_materialized\":false,\"wall_micros\":0,\"basis\":\"directly-observed\",\"exact\":true}"
239 );
240 }
241
242 #[test]
243 fn string_array_escapes() {
244 assert_eq!(
245 string_array(&["a".into(), "b\"c".into()]),
246 "[\"a\",\"b\\\"c\"]"
247 );
248 assert_eq!(string_array(&[]), "[]");
249 }
250}