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