icydb_core/db/query/explain/
json.rs1use crate::db::{
7 TraceReuseArtifactClass,
8 query::{
9 admission::QueryAdmissionSummary,
10 explain::{
11 ExplainExecutionNodeDescriptor, FinalizedQueryDiagnostics,
12 access_projection::write_access_json,
13 execution::{execution_mode_label, ordering_source_label},
14 nodes::{
15 execution_mode_detail_label, fast_path_reason, fast_path_selected,
16 predicate_pushdown_mode,
17 },
18 writer::JsonWriter,
19 },
20 },
21};
22
23impl ExplainExecutionNodeDescriptor {
24 #[must_use]
26 pub fn render_json_canonical(&self) -> String {
27 let mut out = String::new();
28 let mut node_id_counter = 0_u64;
29 write_execution_node_json(self, &mut node_id_counter, &mut out);
30 out
31 }
32}
33
34impl FinalizedQueryDiagnostics {
35 #[must_use]
37 pub(in crate::db) fn render_json_canonical(&self) -> String {
38 let mut out = String::new();
39 let mut object = JsonWriter::begin_object(&mut out);
40 object.field_with("admission", |out| match self.admission() {
41 Some(admission) => write_admission_json(admission, out),
42 None => out.push_str("null"),
43 });
44 object.field_with("execution", |out| {
45 let mut node_id_counter = 0_u64;
46 write_execution_node_json(self.execution(), &mut node_id_counter, out);
47 });
48 object.field_str_slice("route_diagnostics", &self.route_diagnostics);
49 object.field_str_slice("logical_diagnostics", &self.logical_diagnostics);
50 object.field_with("reuse", |out| {
51 let Some(reuse) = self.reuse else {
52 out.push_str("null");
53 return;
54 };
55 let mut reuse_object = JsonWriter::begin_object(out);
56 reuse_object.field_str(
57 "artifact",
58 match reuse.artifact_class() {
59 TraceReuseArtifactClass::SharedPreparedQueryPlan => {
60 "shared_prepared_query_plan"
61 }
62 },
63 );
64 reuse_object.field_str("outcome", if reuse.is_hit() { "hit" } else { "miss" });
65 reuse_object.finish();
66 });
67 object.finish();
68
69 out
70 }
71}
72
73fn write_admission_json(admission: &QueryAdmissionSummary, out: &mut String) {
74 let mut object = JsonWriter::begin_object(out);
75 object.field_str("lane", admission.lane().as_str());
76 object.field_str("decision", admission.decision().as_str());
77 match admission.rejection() {
78 Some(rejection) => object.field_str("reason", rejection.as_str()),
79 None => object.field_null("reason"),
80 }
81 object.field_str("plan_shape", admission.plan_shape().as_str());
82 object.field_str("selected_access", admission.selected_access().as_str());
83 match admission.selected_index() {
84 Some(selected_index) => object.field_str("selected_index", selected_index),
85 None => object.field_null("selected_index"),
86 }
87 write_optional_u32(&mut object, "limit", admission.limit());
88 write_optional_u32(&mut object, "offset", admission.offset());
89 write_optional_u64(&mut object, "scan_bound", admission.scan_bound());
90 object.field_str("scan_bound_kind", admission.scan_bound_kind().as_str());
91 write_optional_u32(
92 &mut object,
93 "returned_row_bound",
94 admission.returned_row_bound(),
95 );
96 object.field_str(
97 "returned_row_bound_kind",
98 admission.returned_row_bound_kind().as_str(),
99 );
100 write_optional_u32(
101 &mut object,
102 "primary_key_input_terms",
103 admission.primary_key_input_terms(),
104 );
105 write_optional_u32(
106 &mut object,
107 "primary_key_input_payload_bytes",
108 admission.primary_key_input_payload_bytes(),
109 );
110 object.field_str("residual_filter", admission.residual_filter().as_str());
111 object.field_str("ordering", admission.ordering().as_str());
112 object.field_with("materialization", |out| {
113 let materialization = admission.materialization();
114 let mut materialization_object = JsonWriter::begin_object(out);
115 materialization_object.field_bool("materialized_sort", materialization.materialized_sort());
116 write_optional_u32(
117 &mut materialization_object,
118 "materialized_rows",
119 materialization.materialized_rows(),
120 );
121 materialization_object
122 .field_str("row_bound_kind", materialization.row_bound_kind().as_str());
123 materialization_object.finish();
124 });
125 object.field_with("grouped", |out| {
126 let Some(grouped) = admission.grouped() else {
127 out.push_str("null");
128 return;
129 };
130 let mut grouped_object = JsonWriter::begin_object(out);
131 grouped_object.field_u64("group_field_count", u64::from(grouped.group_field_count()));
132 grouped_object.field_u64("aggregate_count", u64::from(grouped.aggregate_count()));
133 grouped_object.field_u64(
134 "distinct_aggregate_count",
135 u64::from(grouped.distinct_aggregate_count()),
136 );
137 grouped_object.field_u64("max_groups", grouped.max_groups());
138 grouped_object.field_u64("max_group_bytes", grouped.max_group_bytes());
139 grouped_object.field_bool("having_filter", grouped.has_having_filter());
140 grouped_object.finish();
141 });
142 object.finish();
143}
144
145fn write_optional_u32(object: &mut JsonWriter<'_>, key: &str, value: Option<u32>) {
146 match value {
147 Some(value) => object.field_u64(key, u64::from(value)),
148 None => object.field_null(key),
149 }
150}
151
152fn write_optional_u64(object: &mut JsonWriter<'_>, key: &str, value: Option<u64>) {
153 match value {
154 Some(value) => object.field_u64(key, value),
155 None => object.field_null(key),
156 }
157}
158
159fn write_execution_node_json(
160 node: &ExplainExecutionNodeDescriptor,
161 node_id_counter: &mut u64,
162 out: &mut String,
163) {
164 let node_id = *node_id_counter;
165 *node_id_counter = node_id_counter.saturating_add(1);
166 let mut object = JsonWriter::begin_object(out);
167
168 object.field_u64("node_id", node_id);
169 object.field_str("node_type", node.node_type().as_str());
170 object.field_str("layer", node.node_type().layer_label());
171 object.field_str(
172 "execution_mode",
173 execution_mode_label(node.execution_mode()),
174 );
175 object.field_str(
176 "execution_mode_detail",
177 execution_mode_detail_label(node.execution_mode()),
178 );
179 object.field_with("access_strategy", |out| {
180 match node.access_strategy().as_ref() {
181 Some(access) => write_access_json(access, out),
182 None => out.push_str("null"),
183 }
184 });
185 object.field_str("predicate_pushdown_mode", predicate_pushdown_mode(node));
186 match node.predicate_pushdown() {
187 Some(predicate_pushdown) => object.field_str("predicate_pushdown", predicate_pushdown),
188 None => object.field_null("predicate_pushdown"),
189 }
190 match node.filter_expr() {
191 Some(filter_expr) => object.field_str("filter_expr", filter_expr),
192 None => object.field_null("filter_expr"),
193 }
194 match node.residual_filter_expr() {
195 Some(residual_filter_expr) => {
196 object.field_str("residual_filter_expr", residual_filter_expr);
197 }
198 None => object.field_null("residual_filter_expr"),
199 }
200 match fast_path_selected(node) {
201 Some(selected) => object.field_bool("fast_path_selected", selected),
202 None => object.field_null("fast_path_selected"),
203 }
204 match fast_path_reason(node) {
205 Some(reason) => object.field_str("fast_path_reason", reason),
206 None => object.field_null("fast_path_reason"),
207 }
208 match node.residual_filter_predicate() {
209 Some(residual_filter_predicate) => {
210 object.field_value_debug("residual_filter_predicate", residual_filter_predicate);
211 }
212 None => object.field_null("residual_filter_predicate"),
213 }
214 match node.projection() {
215 Some(projection) => object.field_str("projection", projection),
216 None => object.field_null("projection"),
217 }
218 match node.ordering_source() {
219 Some(ordering_source) => {
220 object.field_str("ordering_source", ordering_source_label(ordering_source));
221 }
222 None => object.field_null("ordering_source"),
223 }
224 match node.limit() {
225 Some(limit) => object.field_u64("limit", u64::from(limit)),
226 None => object.field_null("limit"),
227 }
228 match node.cursor() {
229 Some(cursor) => object.field_bool("cursor", cursor),
230 None => object.field_null("cursor"),
231 }
232 match node.covering_scan() {
233 Some(covering_scan) => object.field_bool("covering_scan", covering_scan),
234 None => object.field_null("covering_scan"),
235 }
236 match node.rows_expected() {
237 Some(rows_expected) => object.field_u64("rows_expected", rows_expected),
238 None => object.field_null("rows_expected"),
239 }
240 object.field_with("children", |out| {
241 out.push('[');
242 for (index, child) in node.children().iter().enumerate() {
243 if index > 0 {
244 out.push(',');
245 }
246 write_execution_node_json(child, node_id_counter, out);
247 }
248 out.push(']');
249 });
250 object.field_debug_map("node_properties", node.node_properties());
251
252 object.finish();
253}