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::explain::{
7    ExplainExecutionNodeDescriptor,
8    access_projection::write_access_json,
9    execution::{execution_mode_label, ordering_source_label},
10    nodes::{
11        execution_mode_detail_label, fast_path_reason, fast_path_selected, predicate_pushdown_mode,
12    },
13    writer::JsonWriter,
14};
15
16impl ExplainExecutionNodeDescriptor {
17    /// Render this execution subtree as canonical JSON.
18    #[must_use]
19    pub fn render_json_canonical(&self) -> String {
20        let mut out = String::new();
21        let mut node_id_counter = 0_u64;
22        write_execution_node_json(self, &mut node_id_counter, &mut out);
23        out
24    }
25}
26
27fn write_execution_node_json(
28    node: &ExplainExecutionNodeDescriptor,
29    node_id_counter: &mut u64,
30    out: &mut String,
31) {
32    let node_id = *node_id_counter;
33    *node_id_counter = node_id_counter.saturating_add(1);
34    let mut object = JsonWriter::begin_object(out);
35
36    object.field_u64("node_id", node_id);
37    object.field_str("node_type", node.node_type().as_str());
38    object.field_str("layer", node.node_type().layer_label());
39    object.field_str(
40        "execution_mode",
41        execution_mode_label(node.execution_mode()),
42    );
43    object.field_str(
44        "execution_mode_detail",
45        execution_mode_detail_label(node.execution_mode()),
46    );
47    object.field_with("access_strategy", |out| {
48        match node.access_strategy().as_ref() {
49            Some(access) => write_access_json(access, out),
50            None => out.push_str("null"),
51        }
52    });
53    object.field_str("predicate_pushdown_mode", predicate_pushdown_mode(node));
54    match node.predicate_pushdown() {
55        Some(predicate_pushdown) => object.field_str("predicate_pushdown", predicate_pushdown),
56        None => object.field_null("predicate_pushdown"),
57    }
58    match node.filter_expr() {
59        Some(filter_expr) => object.field_str("filter_expr", filter_expr),
60        None => object.field_null("filter_expr"),
61    }
62    match node.residual_filter_expr() {
63        Some(residual_filter_expr) => {
64            object.field_str("residual_filter_expr", residual_filter_expr);
65        }
66        None => object.field_null("residual_filter_expr"),
67    }
68    match fast_path_selected(node) {
69        Some(selected) => object.field_bool("fast_path_selected", selected),
70        None => object.field_null("fast_path_selected"),
71    }
72    match fast_path_reason(node) {
73        Some(reason) => object.field_str("fast_path_reason", reason),
74        None => object.field_null("fast_path_reason"),
75    }
76    match node.residual_filter_predicate() {
77        Some(residual_filter_predicate) => {
78            object.field_value_debug("residual_filter_predicate", residual_filter_predicate);
79        }
80        None => object.field_null("residual_filter_predicate"),
81    }
82    match node.projection() {
83        Some(projection) => object.field_str("projection", projection),
84        None => object.field_null("projection"),
85    }
86    match node.ordering_source() {
87        Some(ordering_source) => {
88            object.field_str("ordering_source", ordering_source_label(ordering_source));
89        }
90        None => object.field_null("ordering_source"),
91    }
92    match node.limit() {
93        Some(limit) => object.field_u64("limit", u64::from(limit)),
94        None => object.field_null("limit"),
95    }
96    match node.cursor() {
97        Some(cursor) => object.field_bool("cursor", cursor),
98        None => object.field_null("cursor"),
99    }
100    match node.covering_scan() {
101        Some(covering_scan) => object.field_bool("covering_scan", covering_scan),
102        None => object.field_null("covering_scan"),
103    }
104    match node.rows_expected() {
105        Some(rows_expected) => object.field_u64("rows_expected", rows_expected),
106        None => object.field_null("rows_expected"),
107    }
108    object.field_with("children", |out| {
109        out.push('[');
110        for (index, child) in node.children().iter().enumerate() {
111            if index > 0 {
112                out.push(',');
113            }
114            write_execution_node_json(child, node_id_counter, out);
115        }
116        out.push(']');
117    });
118    object.field_debug_map("node_properties", node.node_properties());
119
120    object.finish();
121}