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