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        DbSession, MissingRowPolicy, QueryError,
9        executor::{
10            EntityAuthority, assemble_load_execution_node_descriptor_with_model_store_witness,
11        },
12        query::builder::aggregate::AggregateExpr,
13        query::intent::StructuralQuery,
14        query::plan::{FieldSlot, resolve_aggregate_target_field_slot},
15        session::sql::surface::{SqlSurface, session_sql_lane, unsupported_sql_lane_message},
16        sql::lowering::{
17            LoweredSqlCommand, LoweredSqlQuery, SqlGlobalAggregateCommandCore,
18            SqlGlobalAggregateTerminal, apply_lowered_select_shape,
19            bind_lowered_sql_explain_global_aggregate_structural,
20            render_lowered_sql_explain_plan_or_json,
21        },
22        sql::parser::SqlExplainMode,
23    },
24    model::EntityModel,
25    traits::CanisterKind,
26};
27
28// Resolve one aggregate target field through planner slot contracts before
29// aggregate terminal execution.
30fn resolve_sql_aggregate_target_slot_with_model(
31    model: &'static EntityModel,
32    field: &str,
33) -> Result<FieldSlot, QueryError> {
34    resolve_aggregate_target_field_slot(model, field)
35}
36
37pub(in crate::db::session::sql) fn resolve_sql_aggregate_target_slot<E>(
38    field: &str,
39) -> Result<FieldSlot, QueryError>
40where
41    E: crate::traits::EntityKind,
42{
43    resolve_sql_aggregate_target_slot_with_model(E::MODEL, field)
44}
45
46// Convert one lowered global SQL aggregate terminal into aggregate expression
47// contracts used by aggregate explain execution descriptors.
48fn sql_global_aggregate_terminal_to_expr_with_model(
49    model: &'static EntityModel,
50    terminal: &SqlGlobalAggregateTerminal,
51) -> Result<AggregateExpr, QueryError> {
52    match terminal {
53        SqlGlobalAggregateTerminal::CountRows => Ok(crate::db::query::builder::aggregate::count()),
54        SqlGlobalAggregateTerminal::CountField(field) => {
55            let _ = resolve_sql_aggregate_target_slot_with_model(model, field)?;
56
57            Ok(crate::db::query::builder::aggregate::count_by(
58                field.as_str(),
59            ))
60        }
61        SqlGlobalAggregateTerminal::SumField(field) => {
62            let _ = resolve_sql_aggregate_target_slot_with_model(model, field)?;
63
64            Ok(crate::db::query::builder::aggregate::sum(field.as_str()))
65        }
66        SqlGlobalAggregateTerminal::AvgField(field) => {
67            let _ = resolve_sql_aggregate_target_slot_with_model(model, field)?;
68
69            Ok(crate::db::query::builder::aggregate::avg(field.as_str()))
70        }
71        SqlGlobalAggregateTerminal::MinField(field) => {
72            let _ = resolve_sql_aggregate_target_slot_with_model(model, field)?;
73
74            Ok(crate::db::query::builder::aggregate::min_by(field.as_str()))
75        }
76        SqlGlobalAggregateTerminal::MaxField(field) => {
77            let _ = resolve_sql_aggregate_target_slot_with_model(model, field)?;
78
79            Ok(crate::db::query::builder::aggregate::max_by(field.as_str()))
80        }
81    }
82}
83
84impl LoweredSqlCommand {
85    /// Render this lowered SQL command through the shared EXPLAIN surface for
86    /// one concrete model authority.
87    #[inline(never)]
88    pub fn explain_for_model(&self, model: &'static EntityModel) -> Result<String, QueryError> {
89        // First validate lane selection once on the shared lowered-command path
90        // so explain callers do not rebuild lane guards around the same shape.
91        let lane = session_sql_lane(self);
92        if lane != crate::db::session::sql::surface::SqlLaneKind::Explain {
93            return Err(QueryError::unsupported_query(unsupported_sql_lane_message(
94                SqlSurface::Explain,
95                lane,
96            )));
97        }
98
99        // Then prefer the structural renderer because plan/json explain output
100        // can stay generic-free all the way to the final render step.
101        if let Some(rendered) =
102            render_lowered_sql_explain_plan_or_json(self, model, MissingRowPolicy::Ignore)
103                .map_err(QueryError::from_sql_lowering_error)?
104        {
105            return Ok(rendered);
106        }
107
108        // Structural global aggregate explain is the remaining explain-only
109        // shape that still needs dedicated aggregate descriptor rendering.
110        if let Some((mode, command)) = bind_lowered_sql_explain_global_aggregate_structural(
111            self,
112            model,
113            MissingRowPolicy::Ignore,
114        ) {
115            return explain_sql_global_aggregate_structural(mode, command);
116        }
117
118        Err(QueryError::unsupported_query(
119            "shared EXPLAIN dispatch could not classify lowered SQL shape",
120        ))
121    }
122}
123
124impl<C: CanisterKind> DbSession<C> {
125    // Render one SQL EXPLAIN EXECUTION payload through the store-backed route
126    // planner so witness-validated covering routes remain visible on the SQL
127    // surface even though generic query-builder explain stays conservative.
128    pub(in crate::db::session::sql) fn explain_lowered_sql_execution_for_authority(
129        &self,
130        lowered: &LoweredSqlCommand,
131        authority: EntityAuthority,
132    ) -> Result<Option<String>, QueryError> {
133        let Some((SqlExplainMode::Execution, query)) = lowered.explain_query() else {
134            return Ok(None);
135        };
136        let LoweredSqlQuery::Select(select) = query else {
137            return Ok(None);
138        };
139
140        let structural = apply_lowered_select_shape(
141            StructuralQuery::new(authority.model(), MissingRowPolicy::Ignore),
142            select.clone(),
143        )
144        .map_err(QueryError::from_sql_lowering_error)?;
145        let plan = structural.build_plan()?;
146        let store = self
147            .db
148            .recovered_store(authority.store_path())
149            .map_err(QueryError::execute)?;
150        let descriptor = assemble_load_execution_node_descriptor_with_model_store_witness(
151            authority.model(),
152            &plan,
153            store,
154        )
155        .map_err(QueryError::execute)?;
156
157        Ok(Some(descriptor.render_text_tree()))
158    }
159}
160
161// Render one EXPLAIN payload for constrained global aggregate SQL command
162// entirely through structural query and descriptor authority.
163#[inline(never)]
164fn explain_sql_global_aggregate_structural(
165    mode: SqlExplainMode,
166    command: SqlGlobalAggregateCommandCore,
167) -> Result<String, QueryError> {
168    let model = command.query().model();
169
170    match mode {
171        SqlExplainMode::Plan => {
172            let _ = sql_global_aggregate_terminal_to_expr_with_model(model, command.terminal())?;
173
174            Ok(command
175                .query()
176                .build_plan()?
177                .explain_with_model(model)
178                .render_text_canonical())
179        }
180        SqlExplainMode::Execution => {
181            let aggregate =
182                sql_global_aggregate_terminal_to_expr_with_model(model, command.terminal())?;
183            let plan = command.query().explain_aggregate_terminal(aggregate)?;
184
185            Ok(plan.execution_node_descriptor().render_text_tree())
186        }
187        SqlExplainMode::Json => {
188            let _ = sql_global_aggregate_terminal_to_expr_with_model(model, command.terminal())?;
189
190            Ok(command
191                .query()
192                .build_plan()?
193                .explain_with_model(model)
194                .render_json_canonical())
195        }
196    }
197}