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