Skip to main content

icydb_core/db/sql/lowering/
mod.rs

1//! Module: db::sql::lowering
2//! Responsibility: reduced SQL statement lowering into canonical query intent.
3//! Does not own: SQL tokenization/parsing, planner validation policy, or executor semantics.
4//! Boundary: frontend-only translation from parsed SQL statement contracts to `Query<E>`.
5
6mod aggregate;
7mod normalize;
8mod prepare;
9mod select;
10
11///
12/// TESTS
13///
14
15#[cfg(test)]
16mod tests;
17
18use crate::db::{
19    query::intent::QueryError,
20    sql::parser::{SqlExplainMode, SqlStatement},
21};
22#[cfg(test)]
23use crate::{
24    db::{predicate::MissingRowPolicy, query::intent::Query},
25    traits::EntityKind,
26};
27use thiserror::Error as ThisError;
28
29pub(in crate::db::sql::lowering) use aggregate::LoweredSqlGlobalAggregateCommand;
30pub(in crate::db) use aggregate::compile_sql_global_aggregate_command_core_from_prepared;
31pub(in crate::db) use aggregate::is_sql_global_aggregate_statement;
32#[cfg(test)]
33pub(crate) use aggregate::{
34    PreparedSqlScalarAggregateDescriptorShape, PreparedSqlScalarAggregateDomain,
35    PreparedSqlScalarAggregateEmptySetBehavior, PreparedSqlScalarAggregateOrderingRequirement,
36    PreparedSqlScalarAggregateRowSource, SqlGlobalAggregateCommand,
37    TypedSqlGlobalAggregateTerminal, compile_sql_global_aggregate_command,
38};
39pub(crate) use aggregate::{
40    PreparedSqlScalarAggregateRuntimeDescriptor, PreparedSqlScalarAggregateStrategy,
41    SqlGlobalAggregateCommandCore, bind_lowered_sql_explain_global_aggregate_structural,
42};
43pub(crate) use prepare::{lower_sql_command_from_prepared_statement, prepare_sql_statement};
44pub(in crate::db::sql::lowering) use select::apply_lowered_base_query_shape;
45#[cfg(test)]
46pub(in crate::db) use select::apply_lowered_select_shape;
47pub(crate) use select::{LoweredBaseQueryShape, LoweredSelectShape};
48pub(in crate::db) use select::{
49    bind_lowered_sql_query, bind_lowered_sql_query_structural,
50    bind_lowered_sql_select_query_structural, canonicalize_sql_predicate_for_model,
51};
52
53///
54/// LoweredSqlCommand
55///
56/// Generic-free SQL command shape after reduced SQL parsing and entity-route
57/// normalization.
58/// This keeps statement-shape lowering shared across entities before typed
59/// `Query<E>` binding happens at the execution boundary.
60///
61#[derive(Clone, Debug)]
62pub struct LoweredSqlCommand(pub(in crate::db::sql::lowering) LoweredSqlCommandInner);
63
64#[derive(Clone, Debug)]
65pub(in crate::db::sql::lowering) enum LoweredSqlCommandInner {
66    Query(LoweredSqlQuery),
67    Explain {
68        mode: SqlExplainMode,
69        query: LoweredSqlQuery,
70    },
71    ExplainGlobalAggregate {
72        mode: SqlExplainMode,
73        command: LoweredSqlGlobalAggregateCommand,
74    },
75    DescribeEntity,
76    ShowIndexesEntity,
77    ShowColumnsEntity,
78    ShowEntities,
79}
80
81///
82/// SqlCommand
83///
84/// Test-only typed SQL command shell over the shared lowered SQL surface.
85/// Runtime dispatch now consumes `LoweredSqlCommand` directly, but lowering
86/// tests still validate typed binding behavior on this local envelope.
87///
88#[cfg(test)]
89#[derive(Debug)]
90pub(crate) enum SqlCommand<E: EntityKind> {
91    Query(Query<E>),
92    Explain {
93        mode: SqlExplainMode,
94        query: Query<E>,
95    },
96    ExplainGlobalAggregate {
97        mode: SqlExplainMode,
98        command: SqlGlobalAggregateCommand<E>,
99    },
100    DescribeEntity,
101    ShowIndexesEntity,
102    ShowColumnsEntity,
103    ShowEntities,
104}
105
106impl LoweredSqlCommand {
107    #[must_use]
108    pub(in crate::db) const fn query(&self) -> Option<&LoweredSqlQuery> {
109        match &self.0 {
110            LoweredSqlCommandInner::Query(query) => Some(query),
111            LoweredSqlCommandInner::Explain { .. }
112            | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
113            | LoweredSqlCommandInner::DescribeEntity
114            | LoweredSqlCommandInner::ShowIndexesEntity
115            | LoweredSqlCommandInner::ShowColumnsEntity
116            | LoweredSqlCommandInner::ShowEntities => None,
117        }
118    }
119
120    #[must_use]
121    pub(in crate::db) fn into_query(self) -> Option<LoweredSqlQuery> {
122        match self.0 {
123            LoweredSqlCommandInner::Query(query) => Some(query),
124            LoweredSqlCommandInner::Explain { .. }
125            | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
126            | LoweredSqlCommandInner::DescribeEntity
127            | LoweredSqlCommandInner::ShowIndexesEntity
128            | LoweredSqlCommandInner::ShowColumnsEntity
129            | LoweredSqlCommandInner::ShowEntities => None,
130        }
131    }
132
133    #[must_use]
134    pub(in crate::db) const fn explain_query(&self) -> Option<(SqlExplainMode, &LoweredSqlQuery)> {
135        match &self.0 {
136            LoweredSqlCommandInner::Explain { mode, query } => Some((*mode, query)),
137            LoweredSqlCommandInner::Query(_)
138            | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
139            | LoweredSqlCommandInner::DescribeEntity
140            | LoweredSqlCommandInner::ShowIndexesEntity
141            | LoweredSqlCommandInner::ShowColumnsEntity
142            | LoweredSqlCommandInner::ShowEntities => None,
143        }
144    }
145}
146
147///
148/// LoweredSqlQuery
149///
150/// Generic-free executable SQL query shape prepared before typed query binding.
151/// Select and delete lowering stay shared until the final `Query<E>` build.
152///
153#[derive(Clone, Debug)]
154pub(crate) enum LoweredSqlQuery {
155    Select(LoweredSelectShape),
156    Delete(LoweredBaseQueryShape),
157}
158
159impl LoweredSqlQuery {
160    // Report whether this lowered query carries grouped execution semantics.
161    pub(crate) const fn has_grouping(&self) -> bool {
162        match self {
163            Self::Select(select) => select.has_grouping(),
164            Self::Delete(_) => false,
165        }
166    }
167}
168
169///
170/// SqlLoweringError
171///
172/// SQL frontend lowering failures before planner validation/execution.
173///
174#[derive(Debug, ThisError)]
175pub(crate) enum SqlLoweringError {
176    #[error("{0}")]
177    Parse(#[from] crate::db::sql::parser::SqlParseError),
178
179    #[error("{0}")]
180    Query(Box<QueryError>),
181
182    #[error("SQL entity '{sql_entity}' does not match requested entity type '{expected_entity}'")]
183    EntityMismatch {
184        sql_entity: String,
185        expected_entity: &'static str,
186    },
187
188    #[error(
189        "unsupported SQL SELECT projection; supported forms are SELECT *, field lists, or grouped aggregate shapes"
190    )]
191    UnsupportedSelectProjection,
192
193    #[error("unsupported SQL SELECT DISTINCT")]
194    UnsupportedSelectDistinct,
195
196    #[error("unsupported SQL GROUP BY projection shape")]
197    UnsupportedSelectGroupBy,
198
199    #[error("unsupported SQL HAVING shape")]
200    UnsupportedSelectHaving,
201
202    #[error("ORDER BY alias '{alias}' does not resolve to a supported order target")]
203    UnsupportedOrderByAlias { alias: String },
204}
205
206impl SqlLoweringError {
207    /// Construct one entity-mismatch SQL lowering error.
208    fn entity_mismatch(sql_entity: impl Into<String>, expected_entity: &'static str) -> Self {
209        Self::EntityMismatch {
210            sql_entity: sql_entity.into(),
211            expected_entity,
212        }
213    }
214
215    /// Construct one unsupported SELECT projection SQL lowering error.
216    const fn unsupported_select_projection() -> Self {
217        Self::UnsupportedSelectProjection
218    }
219
220    /// Construct one unsupported SELECT DISTINCT SQL lowering error.
221    const fn unsupported_select_distinct() -> Self {
222        Self::UnsupportedSelectDistinct
223    }
224
225    /// Construct one unsupported SELECT GROUP BY shape SQL lowering error.
226    const fn unsupported_select_group_by() -> Self {
227        Self::UnsupportedSelectGroupBy
228    }
229
230    /// Construct one unsupported SELECT HAVING shape SQL lowering error.
231    const fn unsupported_select_having() -> Self {
232        Self::UnsupportedSelectHaving
233    }
234
235    /// Construct one unsupported ORDER BY alias SQL lowering error.
236    fn unsupported_order_by_alias(alias: impl Into<String>) -> Self {
237        Self::UnsupportedOrderByAlias {
238            alias: alias.into(),
239        }
240    }
241}
242
243impl From<QueryError> for SqlLoweringError {
244    fn from(value: QueryError) -> Self {
245        Self::Query(Box::new(value))
246    }
247}
248
249///
250/// PreparedSqlStatement
251///
252/// SQL statement envelope after entity-scope normalization and
253/// entity-match validation for one target entity descriptor.
254///
255/// This pre-lowering contract is entity-agnostic and reusable across
256/// dynamic SQL route branches before typed `Query<E>` binding.
257///
258#[derive(Clone, Debug)]
259pub(crate) struct PreparedSqlStatement {
260    pub(in crate::db::sql::lowering) statement: SqlStatement,
261}
262
263#[derive(Clone, Copy, Debug, Eq, PartialEq)]
264pub(crate) enum LoweredSqlLaneKind {
265    Query,
266    Explain,
267    Describe,
268    ShowIndexes,
269    ShowColumns,
270    ShowEntities,
271}
272
273/// Parse and lower one SQL statement into canonical query intent for `E`.
274#[cfg(test)]
275pub(crate) fn compile_sql_command<E: EntityKind>(
276    sql: &str,
277    consistency: MissingRowPolicy,
278) -> Result<SqlCommand<E>, SqlLoweringError> {
279    let statement = crate::db::sql::parser::parse_sql(sql)?;
280
281    compile_sql_command_from_statement::<E>(statement, consistency)
282}
283
284/// Lower one parsed SQL statement into canonical query intent for `E`.
285#[cfg(test)]
286pub(crate) fn compile_sql_command_from_statement<E: EntityKind>(
287    statement: SqlStatement,
288    consistency: MissingRowPolicy,
289) -> Result<SqlCommand<E>, SqlLoweringError> {
290    let prepared = prepare_sql_statement(statement, E::MODEL.name())?;
291
292    compile_sql_command_from_prepared_statement::<E>(prepared, consistency)
293}
294
295/// Lower one prepared SQL statement into canonical query intent for `E`.
296#[cfg(test)]
297pub(crate) fn compile_sql_command_from_prepared_statement<E: EntityKind>(
298    prepared: PreparedSqlStatement,
299    consistency: MissingRowPolicy,
300) -> Result<SqlCommand<E>, SqlLoweringError> {
301    let lowered = lower_sql_command_from_prepared_statement(prepared, E::MODEL.primary_key.name)?;
302
303    bind_lowered_sql_command::<E>(lowered, consistency)
304}
305
306pub(crate) const fn lowered_sql_command_lane(command: &LoweredSqlCommand) -> LoweredSqlLaneKind {
307    match command.0 {
308        LoweredSqlCommandInner::Query(_) => LoweredSqlLaneKind::Query,
309        LoweredSqlCommandInner::Explain { .. }
310        | LoweredSqlCommandInner::ExplainGlobalAggregate { .. } => LoweredSqlLaneKind::Explain,
311        LoweredSqlCommandInner::DescribeEntity => LoweredSqlLaneKind::Describe,
312        LoweredSqlCommandInner::ShowIndexesEntity => LoweredSqlLaneKind::ShowIndexes,
313        LoweredSqlCommandInner::ShowColumnsEntity => LoweredSqlLaneKind::ShowColumns,
314        LoweredSqlCommandInner::ShowEntities => LoweredSqlLaneKind::ShowEntities,
315    }
316}
317
318/// Bind one shared generic-free SQL command shape to the typed query surface.
319#[cfg(test)]
320pub(crate) fn bind_lowered_sql_command<E: EntityKind>(
321    lowered: LoweredSqlCommand,
322    consistency: MissingRowPolicy,
323) -> Result<SqlCommand<E>, SqlLoweringError> {
324    match lowered.0 {
325        LoweredSqlCommandInner::Query(query) => Ok(SqlCommand::Query(bind_lowered_sql_query::<E>(
326            query,
327            consistency,
328        )?)),
329        LoweredSqlCommandInner::Explain { mode, query } => Ok(SqlCommand::Explain {
330            mode,
331            query: bind_lowered_sql_query::<E>(query, consistency)?,
332        }),
333        LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command } => {
334            Ok(SqlCommand::ExplainGlobalAggregate {
335                mode,
336                command: aggregate::bind_lowered_sql_global_aggregate_command::<E>(
337                    command,
338                    consistency,
339                )?,
340            })
341        }
342        LoweredSqlCommandInner::DescribeEntity => Ok(SqlCommand::DescribeEntity),
343        LoweredSqlCommandInner::ShowIndexesEntity => Ok(SqlCommand::ShowIndexesEntity),
344        LoweredSqlCommandInner::ShowColumnsEntity => Ok(SqlCommand::ShowColumnsEntity),
345        LoweredSqlCommandInner::ShowEntities => Ok(SqlCommand::ShowEntities),
346    }
347}