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