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        "response_byte_bound",
103        admission.response_byte_bound(),
104    );
105    object.field_str(
106        "response_byte_bound_kind",
107        admission.response_byte_bound_kind().as_str(),
108    );
109    object.field_str("residual_filter", admission.residual_filter().as_str());
110    object.field_str("ordering", admission.ordering().as_str());
111    object.field_with("materialization", |out| {
112        let materialization = admission.materialization();
113        let mut materialization_object = JsonWriter::begin_object(out);
114        materialization_object.field_bool("materialized_sort", materialization.materialized_sort());
115        write_optional_u32(
116            &mut materialization_object,
117            "materialized_rows",
118            materialization.materialized_rows(),
119        );
120        materialization_object
121            .field_str("row_bound_kind", materialization.row_bound_kind().as_str());
122        materialization_object.finish();
123    });
124    object.field_with("grouped", |out| {
125        let Some(grouped) = admission.grouped() else {
126            out.push_str("null");
127            return;
128        };
129        let mut grouped_object = JsonWriter::begin_object(out);
130        grouped_object.field_u64("group_field_count", u64::from(grouped.group_field_count()));
131        grouped_object.field_u64("aggregate_count", u64::from(grouped.aggregate_count()));
132        grouped_object.field_u64(
133            "distinct_aggregate_count",
134            u64::from(grouped.distinct_aggregate_count()),
135        );
136        grouped_object.field_u64("max_groups", grouped.max_groups());
137        grouped_object.field_u64("max_group_bytes", grouped.max_group_bytes());
138        grouped_object.field_bool("having_filter", grouped.has_having_filter());
139        grouped_object.finish();
140    });
141    object.finish();
142}
143
144fn write_optional_u32(object: &mut JsonWriter<'_>, key: &str, value: Option<u32>) {
145    match value {
146        Some(value) => object.field_u64(key, u64::from(value)),
147        None => object.field_null(key),
148    }
149}
150
151fn write_optional_u64(object: &mut JsonWriter<'_>, key: &str, value: Option<u64>) {
152    match value {
153        Some(value) => object.field_u64(key, value),
154        None => object.field_null(key),
155    }
156}
157
158fn write_execution_node_json(
159    node: &ExplainExecutionNodeDescriptor,
160    node_id_counter: &mut u64,
161    out: &mut String,
162) {
163    let node_id = *node_id_counter;
164    *node_id_counter = node_id_counter.saturating_add(1);
165    let mut object = JsonWriter::begin_object(out);
166
167    object.field_u64("node_id", node_id);
168    object.field_str("node_type", node.node_type().as_str());
169    object.field_str("layer", node.node_type().layer_label());
170    object.field_str(
171        "execution_mode",
172        execution_mode_label(node.execution_mode()),
173    );
174    object.field_str(
175        "execution_mode_detail",
176        execution_mode_detail_label(node.execution_mode()),
177    );
178    object.field_with("access_strategy", |out| {
179        match node.access_strategy().as_ref() {
180            Some(access) => write_access_json(access, out),
181            None => out.push_str("null"),
182        }
183    });
184    object.field_str("predicate_pushdown_mode", predicate_pushdown_mode(node));
185    match node.predicate_pushdown() {
186        Some(predicate_pushdown) => object.field_str("predicate_pushdown", predicate_pushdown),
187        None => object.field_null("predicate_pushdown"),
188    }
189    match node.filter_expr() {
190        Some(filter_expr) => object.field_str("filter_expr", filter_expr),
191        None => object.field_null("filter_expr"),
192    }
193    match node.residual_filter_expr() {
194        Some(residual_filter_expr) => {
195            object.field_str("residual_filter_expr", residual_filter_expr);
196        }
197        None => object.field_null("residual_filter_expr"),
198    }
199    match fast_path_selected(node) {
200        Some(selected) => object.field_bool("fast_path_selected", selected),
201        None => object.field_null("fast_path_selected"),
202    }
203    match fast_path_reason(node) {
204        Some(reason) => object.field_str("fast_path_reason", reason),
205        None => object.field_null("fast_path_reason"),
206    }
207    match node.residual_filter_predicate() {
208        Some(residual_filter_predicate) => {
209            object.field_value_debug("residual_filter_predicate", residual_filter_predicate);
210        }
211        None => object.field_null("residual_filter_predicate"),
212    }
213    match node.projection() {
214        Some(projection) => object.field_str("projection", projection),
215        None => object.field_null("projection"),
216    }
217    match node.ordering_source() {
218        Some(ordering_source) => {
219            object.field_str("ordering_source", ordering_source_label(ordering_source));
220        }
221        None => object.field_null("ordering_source"),
222    }
223    match node.limit() {
224        Some(limit) => object.field_u64("limit", u64::from(limit)),
225        None => object.field_null("limit"),
226    }
227    match node.cursor() {
228        Some(cursor) => object.field_bool("cursor", cursor),
229        None => object.field_null("cursor"),
230    }
231    match node.covering_scan() {
232        Some(covering_scan) => object.field_bool("covering_scan", covering_scan),
233        None => object.field_null("covering_scan"),
234    }
235    match node.rows_expected() {
236        Some(rows_expected) => object.field_u64("rows_expected", rows_expected),
237        None => object.field_null("rows_expected"),
238    }
239    object.field_with("children", |out| {
240        out.push('[');
241        for (index, child) in node.children().iter().enumerate() {
242            if index > 0 {
243                out.push(',');
244            }
245            write_execution_node_json(child, node_id_counter, out);
246        }
247        out.push(']');
248    });
249    object.field_debug_map("node_properties", node.node_properties());
250
251    object.finish();
252}