Skip to main content

icydb_core/db/query/explain/
json.rs

1//! Module: query::explain::json
2//! Responsibility: canonical JSON rendering helpers for execution explain descriptors.
3//! Does not own: execution decision derivation or text-tree rendering.
4//! Boundary: deterministic JSON field ordering for execution explain output.
5
6use 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    /// Render this execution subtree as canonical JSON.
25    #[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    /// Render this finalized execution diagnostics artifact as canonical JSON.
36    #[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}