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) use select::LoweredSelectQueryShape;
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_query, bind_lowered_sql_query_structural,
51    bind_lowered_sql_select_query_structural, canonicalize_sql_predicate_for_model,
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    #[cfg_attr(not(any(test, feature = "perf-attribution")), allow(dead_code))]
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    // Surface the lowered query execution family without re-deriving it from
163    // grouped fields or statement syntax in downstream layers.
164    #[cfg(test)]
165    pub(crate) const fn select_shape(&self) -> Option<LoweredSelectQueryShape> {
166        match self {
167            Self::Select(select) => Some(select.shape()),
168            Self::Delete(_) => None,
169        }
170    }
171}
172
173///
174/// SqlLoweringError
175///
176/// SQL frontend lowering failures before planner validation/execution.
177///
178#[derive(Debug, ThisError)]
179pub(crate) enum SqlLoweringError {
180    #[error("{0}")]
181    Parse(#[from] crate::db::sql::parser::SqlParseError),
182
183    #[error("{0}")]
184    Query(Box<QueryError>),
185
186    #[error("SQL entity '{sql_entity}' does not match requested entity type '{expected_entity}'")]
187    EntityMismatch {
188        sql_entity: String,
189        expected_entity: &'static str,
190    },
191
192    #[error(
193        "unsupported SQL SELECT projection; supported forms are SELECT *, field lists, or grouped aggregate shapes"
194    )]
195    UnsupportedSelectProjection,
196
197    #[error("unsupported SQL SELECT DISTINCT")]
198    UnsupportedSelectDistinct,
199
200    #[error("unsupported SQL GROUP BY projection shape")]
201    UnsupportedSelectGroupBy,
202
203    #[error("unsupported SQL HAVING shape")]
204    UnsupportedSelectHaving,
205
206    #[error("ORDER BY alias '{alias}' does not resolve to a supported order target")]
207    UnsupportedOrderByAlias { alias: String },
208
209    #[error("query-lane lowering reached a non query-compatible statement")]
210    UnexpectedQueryLaneStatement,
211}
212
213impl SqlLoweringError {
214    /// Construct one entity-mismatch SQL lowering error.
215    fn entity_mismatch(sql_entity: impl Into<String>, expected_entity: &'static str) -> Self {
216        Self::EntityMismatch {
217            sql_entity: sql_entity.into(),
218            expected_entity,
219        }
220    }
221
222    /// Construct one unsupported SELECT projection SQL lowering error.
223    const fn unsupported_select_projection() -> Self {
224        Self::UnsupportedSelectProjection
225    }
226
227    /// Construct one query-lane lowering misuse error.
228    pub(crate) const fn unexpected_query_lane_statement() -> Self {
229        Self::UnexpectedQueryLaneStatement
230    }
231
232    /// Construct one unsupported SELECT DISTINCT SQL lowering error.
233    const fn unsupported_select_distinct() -> Self {
234        Self::UnsupportedSelectDistinct
235    }
236
237    /// Construct one unsupported SELECT GROUP BY shape SQL lowering error.
238    const fn unsupported_select_group_by() -> Self {
239        Self::UnsupportedSelectGroupBy
240    }
241
242    /// Construct one unsupported SELECT HAVING shape SQL lowering error.
243    const fn unsupported_select_having() -> Self {
244        Self::UnsupportedSelectHaving
245    }
246
247    /// Construct one unsupported ORDER BY alias SQL lowering error.
248    fn unsupported_order_by_alias(alias: impl Into<String>) -> Self {
249        Self::UnsupportedOrderByAlias {
250            alias: alias.into(),
251        }
252    }
253}
254
255impl From<QueryError> for SqlLoweringError {
256    fn from(value: QueryError) -> Self {
257        Self::Query(Box::new(value))
258    }
259}
260
261///
262/// PreparedSqlStatement
263///
264/// SQL statement envelope after entity-scope normalization and
265/// entity-match validation for one target entity descriptor.
266///
267/// This pre-lowering contract is entity-agnostic and reusable across
268/// dynamic SQL route branches before typed `Query<E>` binding.
269///
270#[derive(Clone, Debug)]
271pub(crate) struct PreparedSqlStatement {
272    pub(in crate::db::sql::lowering) statement: SqlStatement,
273}
274
275impl PreparedSqlStatement {
276    /// Consume one prepared SQL statement back into its normalized parsed form.
277    #[must_use]
278    pub(in crate::db) fn into_statement(self) -> SqlStatement {
279        self.statement
280    }
281}
282
283#[derive(Clone, Copy, Debug, Eq, PartialEq)]
284pub(crate) enum LoweredSqlLaneKind {
285    Query,
286    Explain,
287    Describe,
288    ShowIndexes,
289    ShowColumns,
290    ShowEntities,
291}
292
293/// Parse and lower one SQL statement into canonical query intent for `E`.
294#[cfg(test)]
295pub(crate) fn compile_sql_command<E: EntityKind>(
296    sql: &str,
297    consistency: MissingRowPolicy,
298) -> Result<SqlCommand<E>, SqlLoweringError> {
299    let statement = crate::db::sql::parser::parse_sql(sql)?;
300
301    compile_sql_command_from_statement::<E>(statement, consistency)
302}
303
304/// Lower one parsed SQL statement into canonical query intent for `E`.
305#[cfg(test)]
306pub(crate) fn compile_sql_command_from_statement<E: EntityKind>(
307    statement: SqlStatement,
308    consistency: MissingRowPolicy,
309) -> Result<SqlCommand<E>, SqlLoweringError> {
310    let prepared = prepare_sql_statement(statement, E::MODEL.name())?;
311
312    compile_sql_command_from_prepared_statement::<E>(prepared, consistency)
313}
314
315/// Lower one prepared SQL statement into canonical query intent for `E`.
316#[cfg(test)]
317pub(crate) fn compile_sql_command_from_prepared_statement<E: EntityKind>(
318    prepared: PreparedSqlStatement,
319    consistency: MissingRowPolicy,
320) -> Result<SqlCommand<E>, SqlLoweringError> {
321    let lowered = lower_sql_command_from_prepared_statement(prepared, E::MODEL.primary_key.name)?;
322
323    bind_lowered_sql_command::<E>(lowered, consistency)
324}
325
326pub(crate) const fn lowered_sql_command_lane(command: &LoweredSqlCommand) -> LoweredSqlLaneKind {
327    match command.0 {
328        LoweredSqlCommandInner::Query(_) => LoweredSqlLaneKind::Query,
329        LoweredSqlCommandInner::Explain { .. }
330        | LoweredSqlCommandInner::ExplainGlobalAggregate { .. } => LoweredSqlLaneKind::Explain,
331        LoweredSqlCommandInner::DescribeEntity => LoweredSqlLaneKind::Describe,
332        LoweredSqlCommandInner::ShowIndexesEntity => LoweredSqlLaneKind::ShowIndexes,
333        LoweredSqlCommandInner::ShowColumnsEntity => LoweredSqlLaneKind::ShowColumns,
334        LoweredSqlCommandInner::ShowEntities => LoweredSqlLaneKind::ShowEntities,
335    }
336}
337
338/// Bind one shared generic-free SQL command shape to the typed query surface.
339#[cfg(test)]
340pub(crate) fn bind_lowered_sql_command<E: EntityKind>(
341    lowered: LoweredSqlCommand,
342    consistency: MissingRowPolicy,
343) -> Result<SqlCommand<E>, SqlLoweringError> {
344    match lowered.0 {
345        LoweredSqlCommandInner::Query(query) => Ok(SqlCommand::Query(bind_lowered_sql_query::<E>(
346            query,
347            consistency,
348        )?)),
349        LoweredSqlCommandInner::Explain { mode, query } => Ok(SqlCommand::Explain {
350            mode,
351            query: bind_lowered_sql_query::<E>(query, consistency)?,
352        }),
353        LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command } => {
354            Ok(SqlCommand::ExplainGlobalAggregate {
355                mode,
356                command: aggregate::bind_lowered_sql_global_aggregate_command::<E>(
357                    command,
358                    consistency,
359                )?,
360            })
361        }
362        LoweredSqlCommandInner::DescribeEntity => Ok(SqlCommand::DescribeEntity),
363        LoweredSqlCommandInner::ShowIndexesEntity => Ok(SqlCommand::ShowIndexesEntity),
364        LoweredSqlCommandInner::ShowColumnsEntity => Ok(SqlCommand::ShowColumnsEntity),
365        LoweredSqlCommandInner::ShowEntities => Ok(SqlCommand::ShowEntities),
366    }
367}