Skip to main content

icydb_core/db/session/sql/
explain.rs

1//! Module: db::session::sql::explain
2//! Responsibility: module-local ownership and contracts for db::session::sql::explain.
3//! Does not own: cross-module orchestration outside this module.
4//! Boundary: exposes this module API while keeping implementation details internal.
5
6use crate::{
7    db::{
8        MissingRowPolicy, QueryError,
9        query::builder::aggregate::AggregateExpr,
10        query::plan::{FieldSlot, resolve_aggregate_target_field_slot},
11        session::sql::surface::{SqlSurface, session_sql_lane, unsupported_sql_lane_message},
12        sql::lowering::{
13            LoweredSqlCommand, SqlGlobalAggregateCommandCore, SqlGlobalAggregateTerminal,
14            bind_lowered_sql_explain_global_aggregate_structural,
15            render_lowered_sql_explain_plan_or_json,
16        },
17        sql::parser::SqlExplainMode,
18    },
19    model::EntityModel,
20};
21
22// Resolve one aggregate target field through planner slot contracts before
23// aggregate terminal execution.
24fn resolve_sql_aggregate_target_slot_with_model(
25    model: &'static EntityModel,
26    field: &str,
27) -> Result<FieldSlot, QueryError> {
28    resolve_aggregate_target_field_slot(model, field)
29}
30
31pub(in crate::db::session::sql) fn resolve_sql_aggregate_target_slot<E>(
32    field: &str,
33) -> Result<FieldSlot, QueryError>
34where
35    E: crate::traits::EntityKind,
36{
37    resolve_sql_aggregate_target_slot_with_model(E::MODEL, field)
38}
39
40// Convert one lowered global SQL aggregate terminal into aggregate expression
41// contracts used by aggregate explain execution descriptors.
42fn sql_global_aggregate_terminal_to_expr_with_model(
43    model: &'static EntityModel,
44    terminal: &SqlGlobalAggregateTerminal,
45) -> Result<AggregateExpr, QueryError> {
46    match terminal {
47        SqlGlobalAggregateTerminal::CountRows => Ok(crate::db::query::builder::aggregate::count()),
48        SqlGlobalAggregateTerminal::CountField(field) => {
49            let _ = resolve_sql_aggregate_target_slot_with_model(model, field)?;
50
51            Ok(crate::db::query::builder::aggregate::count_by(
52                field.as_str(),
53            ))
54        }
55        SqlGlobalAggregateTerminal::SumField(field) => {
56            let _ = resolve_sql_aggregate_target_slot_with_model(model, field)?;
57
58            Ok(crate::db::query::builder::aggregate::sum(field.as_str()))
59        }
60        SqlGlobalAggregateTerminal::AvgField(field) => {
61            let _ = resolve_sql_aggregate_target_slot_with_model(model, field)?;
62
63            Ok(crate::db::query::builder::aggregate::avg(field.as_str()))
64        }
65        SqlGlobalAggregateTerminal::MinField(field) => {
66            let _ = resolve_sql_aggregate_target_slot_with_model(model, field)?;
67
68            Ok(crate::db::query::builder::aggregate::min_by(field.as_str()))
69        }
70        SqlGlobalAggregateTerminal::MaxField(field) => {
71            let _ = resolve_sql_aggregate_target_slot_with_model(model, field)?;
72
73            Ok(crate::db::query::builder::aggregate::max_by(field.as_str()))
74        }
75    }
76}
77
78impl LoweredSqlCommand {
79    /// Render this lowered SQL command through the shared EXPLAIN surface for
80    /// one concrete model authority.
81    #[inline(never)]
82    pub fn explain_for_model(&self, model: &'static EntityModel) -> Result<String, QueryError> {
83        // First validate lane selection once on the shared lowered-command path
84        // so explain callers do not rebuild lane guards around the same shape.
85        let lane = session_sql_lane(self);
86        if lane != crate::db::session::sql::surface::SqlLaneKind::Explain {
87            return Err(QueryError::unsupported_query(unsupported_sql_lane_message(
88                SqlSurface::Explain,
89                lane,
90            )));
91        }
92
93        // Then prefer the structural renderer because plan/json explain output
94        // can stay generic-free all the way to the final render step.
95        if let Some(rendered) =
96            render_lowered_sql_explain_plan_or_json(self, model, MissingRowPolicy::Ignore)
97                .map_err(QueryError::from_sql_lowering_error)?
98        {
99            return Ok(rendered);
100        }
101
102        // Structural global aggregate explain is the remaining explain-only
103        // shape that still needs dedicated aggregate descriptor rendering.
104        if let Some((mode, command)) = bind_lowered_sql_explain_global_aggregate_structural(
105            self,
106            model,
107            MissingRowPolicy::Ignore,
108        ) {
109            return explain_sql_global_aggregate_structural(mode, command);
110        }
111
112        Err(QueryError::unsupported_query(
113            "shared EXPLAIN dispatch could not classify lowered SQL shape",
114        ))
115    }
116}
117
118// Render one EXPLAIN payload for constrained global aggregate SQL command
119// entirely through structural query and descriptor authority.
120#[inline(never)]
121fn explain_sql_global_aggregate_structural(
122    mode: SqlExplainMode,
123    command: SqlGlobalAggregateCommandCore,
124) -> Result<String, QueryError> {
125    let model = command.query().model();
126
127    match mode {
128        SqlExplainMode::Plan => {
129            let _ = sql_global_aggregate_terminal_to_expr_with_model(model, command.terminal())?;
130
131            Ok(command
132                .query()
133                .build_plan()?
134                .explain_with_model(model)
135                .render_text_canonical())
136        }
137        SqlExplainMode::Execution => {
138            let aggregate =
139                sql_global_aggregate_terminal_to_expr_with_model(model, command.terminal())?;
140            let plan = command.query().explain_aggregate_terminal(aggregate)?;
141
142            Ok(plan.execution_node_descriptor().render_text_tree())
143        }
144        SqlExplainMode::Json => {
145            let _ = sql_global_aggregate_terminal_to_expr_with_model(model, command.terminal())?;
146
147            Ok(command
148                .query()
149                .build_plan()?
150                .explain_with_model(model)
151                .render_json_canonical())
152        }
153    }
154}