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