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