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 expr;
8mod normalize;
9mod predicate;
10mod prepare;
11mod select;
12
13///
14/// TESTS
15///
16
17#[cfg(test)]
18mod tests;
19
20use crate::db::{
21    query::intent::QueryError,
22    sql::parser::{SqlExplainMode, SqlStatement},
23};
24#[cfg(test)]
25use crate::{
26    db::{predicate::MissingRowPolicy, query::intent::Query},
27    traits::EntityKind,
28};
29use thiserror::Error as ThisError;
30
31pub(in crate::db::sql::lowering) use aggregate::LoweredSqlGlobalAggregateCommand;
32pub(in crate::db) use aggregate::compile_sql_global_aggregate_command_core_from_prepared;
33pub(in crate::db) use aggregate::is_sql_global_aggregate_statement;
34#[cfg(test)]
35pub(crate) use aggregate::{
36    PreparedSqlScalarAggregateDescriptorShape, PreparedSqlScalarAggregateDomain,
37    PreparedSqlScalarAggregateEmptySetBehavior, PreparedSqlScalarAggregateOrderingRequirement,
38    PreparedSqlScalarAggregateRowSource, SqlGlobalAggregateCommand,
39    compile_sql_global_aggregate_command,
40};
41pub(crate) use aggregate::{
42    PreparedSqlScalarAggregateRuntimeDescriptor, PreparedSqlScalarAggregateStrategy,
43    SqlGlobalAggregateCommandCore, bind_lowered_sql_explain_global_aggregate_structural,
44};
45pub(in crate::db) use predicate::lower_sql_where_expr;
46pub(crate) use prepare::{lower_sql_command_from_prepared_statement, prepare_sql_statement};
47pub(in crate::db::sql::lowering) use select::apply_lowered_base_query_shape;
48#[cfg(test)]
49pub(in crate::db) use select::apply_lowered_select_shape;
50pub(crate) use select::{LoweredBaseQueryShape, LoweredSelectShape};
51pub(in crate::db) use select::{
52    bind_lowered_sql_query, bind_lowered_sql_query_structural,
53    bind_lowered_sql_select_query_structural, canonicalize_sql_predicate_for_model,
54    canonicalize_strict_sql_literal_for_kind,
55};
56
57///
58/// LoweredSqlCommand
59///
60/// Generic-free SQL command shape after reduced SQL parsing and entity-route
61/// normalization.
62/// This keeps statement-shape lowering shared across entities before typed
63/// `Query<E>` binding happens at the execution boundary.
64///
65#[derive(Clone, Debug)]
66pub struct LoweredSqlCommand(pub(in crate::db::sql::lowering) LoweredSqlCommandInner);
67
68#[derive(Clone, Debug)]
69pub(in crate::db::sql::lowering) enum LoweredSqlCommandInner {
70    Query(LoweredSqlQuery),
71    Explain {
72        mode: SqlExplainMode,
73        query: LoweredSqlQuery,
74    },
75    ExplainGlobalAggregate {
76        mode: SqlExplainMode,
77        command: LoweredSqlGlobalAggregateCommand,
78    },
79    DescribeEntity,
80    ShowIndexesEntity,
81    ShowColumnsEntity,
82    ShowEntities,
83}
84
85///
86/// SqlCommand
87///
88/// Test-only typed SQL command shell over the shared lowered SQL surface.
89/// Runtime dispatch now consumes `LoweredSqlCommand` directly, but lowering
90/// tests still validate typed binding behavior on this local envelope.
91///
92#[cfg(test)]
93#[derive(Debug)]
94pub(crate) enum SqlCommand<E: EntityKind> {
95    Query(Query<E>),
96    Explain {
97        mode: SqlExplainMode,
98        query: Query<E>,
99    },
100    ExplainGlobalAggregate {
101        mode: SqlExplainMode,
102        command: SqlGlobalAggregateCommand<E>,
103    },
104    DescribeEntity,
105    ShowIndexesEntity,
106    ShowColumnsEntity,
107    ShowEntities,
108}
109
110impl LoweredSqlCommand {
111    #[must_use]
112    #[allow(dead_code)]
113    pub(in crate::db) const fn query(&self) -> Option<&LoweredSqlQuery> {
114        match &self.0 {
115            LoweredSqlCommandInner::Query(query) => Some(query),
116            LoweredSqlCommandInner::Explain { .. }
117            | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
118            | LoweredSqlCommandInner::DescribeEntity
119            | LoweredSqlCommandInner::ShowIndexesEntity
120            | LoweredSqlCommandInner::ShowColumnsEntity
121            | LoweredSqlCommandInner::ShowEntities => None,
122        }
123    }
124
125    #[must_use]
126    pub(in crate::db) fn into_query(self) -> Option<LoweredSqlQuery> {
127        match self.0 {
128            LoweredSqlCommandInner::Query(query) => Some(query),
129            LoweredSqlCommandInner::Explain { .. }
130            | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
131            | LoweredSqlCommandInner::DescribeEntity
132            | LoweredSqlCommandInner::ShowIndexesEntity
133            | LoweredSqlCommandInner::ShowColumnsEntity
134            | LoweredSqlCommandInner::ShowEntities => None,
135        }
136    }
137
138    #[must_use]
139    pub(in crate::db) const fn explain_query(&self) -> Option<(SqlExplainMode, &LoweredSqlQuery)> {
140        match &self.0 {
141            LoweredSqlCommandInner::Explain { mode, query } => Some((*mode, query)),
142            LoweredSqlCommandInner::Query(_)
143            | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
144            | LoweredSqlCommandInner::DescribeEntity
145            | LoweredSqlCommandInner::ShowIndexesEntity
146            | LoweredSqlCommandInner::ShowColumnsEntity
147            | LoweredSqlCommandInner::ShowEntities => None,
148        }
149    }
150}
151
152///
153/// LoweredSqlQuery
154///
155/// Generic-free executable SQL query shape prepared before typed query binding.
156/// Select and delete lowering stay shared until the final `Query<E>` build.
157///
158#[derive(Clone, Debug)]
159pub(crate) enum LoweredSqlQuery {
160    Select(LoweredSelectShape),
161    Delete(LoweredBaseQueryShape),
162}
163
164///
165/// SqlLoweringError
166///
167/// SQL frontend lowering failures before planner validation/execution.
168///
169#[derive(Debug, ThisError)]
170pub(crate) enum SqlLoweringError {
171    #[error("{0}")]
172    Parse(#[from] crate::db::sql::parser::SqlParseError),
173
174    #[error("{0}")]
175    Query(Box<QueryError>),
176
177    #[error("SQL entity '{sql_entity}' does not match requested entity type '{expected_entity}'")]
178    EntityMismatch {
179        sql_entity: String,
180        expected_entity: &'static str,
181    },
182
183    #[error(
184        "unsupported SQL SELECT projection; supported forms are SELECT *, field lists, global aggregate terminal lists, or grouped aggregate shapes"
185    )]
186    UnsupportedSelectProjection,
187
188    #[error("unsupported SQL SELECT DISTINCT")]
189    UnsupportedSelectDistinct,
190
191    #[error(
192        "unsupported global aggregate SQL projection; supported forms are aggregate projections such as COUNT(*), SUM(field), AVG(expr), or scalar wrappers over aggregate results"
193    )]
194    UnsupportedGlobalAggregateProjection,
195
196    #[error("global aggregate SQL does not support GROUP BY")]
197    GlobalAggregateDoesNotSupportGroupBy,
198
199    #[error("unsupported SQL GROUP BY projection shape")]
200    UnsupportedSelectGroupBy,
201
202    #[error("grouped SELECT requires an explicit projection list")]
203    GroupedProjectionRequiresExplicitList,
204
205    #[error("grouped SELECT projection must include at least one aggregate expression")]
206    GroupedProjectionRequiresAggregate,
207
208    #[error(
209        "grouped projection expression at index={index} references fields outside GROUP BY keys"
210    )]
211    GroupedProjectionReferencesNonGroupField { index: usize },
212
213    #[error(
214        "grouped projection expression at index={index} appears after aggregate expressions started"
215    )]
216    GroupedProjectionScalarAfterAggregate { index: usize },
217
218    #[error("HAVING requires GROUP BY")]
219    HavingRequiresGroupBy,
220
221    #[error("unsupported SQL HAVING shape")]
222    UnsupportedSelectHaving,
223
224    #[error("aggregate input expressions are not executable in this release")]
225    UnsupportedAggregateInputExpressions,
226
227    #[error("unsupported SQL WHERE expression shape")]
228    UnsupportedWhereExpression,
229
230    #[error("unknown field '{field}'")]
231    UnknownField { field: String },
232
233    #[error("ORDER BY alias '{alias}' does not resolve to a supported order target")]
234    UnsupportedOrderByAlias { alias: String },
235
236    #[error("query-lane lowering reached a non query-compatible statement")]
237    UnexpectedQueryLaneStatement,
238}
239
240impl SqlLoweringError {
241    /// Construct one entity-mismatch SQL lowering error.
242    fn entity_mismatch(sql_entity: impl Into<String>, expected_entity: &'static str) -> Self {
243        Self::EntityMismatch {
244            sql_entity: sql_entity.into(),
245            expected_entity,
246        }
247    }
248
249    /// Construct one unsupported SELECT projection SQL lowering error.
250    const fn unsupported_select_projection() -> Self {
251        Self::UnsupportedSelectProjection
252    }
253
254    /// Construct one query-lane lowering misuse error.
255    pub(crate) const fn unexpected_query_lane_statement() -> Self {
256        Self::UnexpectedQueryLaneStatement
257    }
258
259    /// Construct one unsupported SELECT DISTINCT SQL lowering error.
260    const fn unsupported_select_distinct() -> Self {
261        Self::UnsupportedSelectDistinct
262    }
263
264    /// Construct one unsupported global aggregate projection SQL lowering error.
265    const fn unsupported_global_aggregate_projection() -> Self {
266        Self::UnsupportedGlobalAggregateProjection
267    }
268
269    /// Construct one unsupported SQL WHERE expression lowering error.
270    pub(crate) const fn unsupported_where_expression() -> Self {
271        Self::UnsupportedWhereExpression
272    }
273
274    /// Construct one global-aggregate-GROUP-BY SQL lowering error.
275    const fn global_aggregate_does_not_support_group_by() -> Self {
276        Self::GlobalAggregateDoesNotSupportGroupBy
277    }
278
279    /// Construct one unsupported SELECT GROUP BY shape SQL lowering error.
280    const fn unsupported_select_group_by() -> Self {
281        Self::UnsupportedSelectGroupBy
282    }
283
284    /// Construct one grouped-projection-explicit-list SQL lowering error.
285    const fn grouped_projection_requires_explicit_list() -> Self {
286        Self::GroupedProjectionRequiresExplicitList
287    }
288
289    /// Construct one grouped-projection-missing-aggregate SQL lowering error.
290    const fn grouped_projection_requires_aggregate() -> Self {
291        Self::GroupedProjectionRequiresAggregate
292    }
293
294    /// Construct one grouped projection non-group-field SQL lowering error.
295    const fn grouped_projection_references_non_group_field(index: usize) -> Self {
296        Self::GroupedProjectionReferencesNonGroupField { index }
297    }
298
299    /// Construct one grouped projection scalar-after-aggregate SQL lowering error.
300    const fn grouped_projection_scalar_after_aggregate(index: usize) -> Self {
301        Self::GroupedProjectionScalarAfterAggregate { index }
302    }
303
304    /// Construct one HAVING-requires-GROUP-BY SQL lowering error.
305    const fn having_requires_group_by() -> Self {
306        Self::HavingRequiresGroupBy
307    }
308
309    /// Construct one unsupported SELECT HAVING shape SQL lowering error.
310    const fn unsupported_select_having() -> Self {
311        Self::UnsupportedSelectHaving
312    }
313
314    /// Construct one aggregate-input execution seam SQL lowering error.
315    const fn unsupported_aggregate_input_expressions() -> Self {
316        Self::UnsupportedAggregateInputExpressions
317    }
318
319    /// Construct one unknown-field SQL lowering error.
320    fn unknown_field(field: impl Into<String>) -> Self {
321        Self::UnknownField {
322            field: field.into(),
323        }
324    }
325
326    /// Construct one unsupported ORDER BY alias SQL lowering error.
327    fn unsupported_order_by_alias(alias: impl Into<String>) -> Self {
328        Self::UnsupportedOrderByAlias {
329            alias: alias.into(),
330        }
331    }
332}
333
334impl From<QueryError> for SqlLoweringError {
335    fn from(value: QueryError) -> Self {
336        Self::Query(Box::new(value))
337    }
338}
339
340///
341/// PreparedSqlStatement
342///
343/// SQL statement envelope after entity-scope normalization and
344/// entity-match validation for one target entity descriptor.
345///
346/// This pre-lowering contract is entity-agnostic and reusable across
347/// dynamic SQL route branches before typed `Query<E>` binding.
348///
349#[derive(Clone, Debug)]
350pub(crate) struct PreparedSqlStatement {
351    pub(in crate::db::sql::lowering) statement: SqlStatement,
352}
353
354impl PreparedSqlStatement {
355    /// Consume one prepared SQL statement back into its normalized parsed form.
356    #[must_use]
357    pub(in crate::db) fn into_statement(self) -> SqlStatement {
358        self.statement
359    }
360}
361
362#[derive(Clone, Copy, Debug, Eq, PartialEq)]
363pub(crate) enum LoweredSqlLaneKind {
364    Query,
365    Explain,
366    Describe,
367    ShowIndexes,
368    ShowColumns,
369    ShowEntities,
370}
371
372/// Parse and lower one SQL statement into canonical query intent for `E`.
373#[cfg(test)]
374pub(crate) fn compile_sql_command<E: EntityKind>(
375    sql: &str,
376    consistency: MissingRowPolicy,
377) -> Result<SqlCommand<E>, SqlLoweringError> {
378    let statement = crate::db::sql::parser::parse_sql(sql)?;
379    let prepared = prepare_sql_statement(statement, E::MODEL.name())?;
380    let lowered = lower_sql_command_from_prepared_statement(prepared, E::MODEL)?;
381
382    // Keep the test-only typed envelope local to the single public test entry
383    // point instead of preserving a private forwarding chain.
384    match lowered.0 {
385        LoweredSqlCommandInner::Query(query) => Ok(SqlCommand::Query(bind_lowered_sql_query::<E>(
386            query,
387            consistency,
388        )?)),
389        LoweredSqlCommandInner::Explain { mode, query } => Ok(SqlCommand::Explain {
390            mode,
391            query: bind_lowered_sql_query::<E>(query, consistency)?,
392        }),
393        LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command } => {
394            Ok(SqlCommand::ExplainGlobalAggregate {
395                mode,
396                command: aggregate::bind_lowered_sql_global_aggregate_command::<E>(
397                    command,
398                    consistency,
399                )?,
400            })
401        }
402        LoweredSqlCommandInner::DescribeEntity => Ok(SqlCommand::DescribeEntity),
403        LoweredSqlCommandInner::ShowIndexesEntity => Ok(SqlCommand::ShowIndexesEntity),
404        LoweredSqlCommandInner::ShowColumnsEntity => Ok(SqlCommand::ShowColumnsEntity),
405        LoweredSqlCommandInner::ShowEntities => Ok(SqlCommand::ShowEntities),
406    }
407}
408
409pub(crate) const fn lowered_sql_command_lane(command: &LoweredSqlCommand) -> LoweredSqlLaneKind {
410    match command.0 {
411        LoweredSqlCommandInner::Query(_) => LoweredSqlLaneKind::Query,
412        LoweredSqlCommandInner::Explain { .. }
413        | LoweredSqlCommandInner::ExplainGlobalAggregate { .. } => LoweredSqlLaneKind::Explain,
414        LoweredSqlCommandInner::DescribeEntity => LoweredSqlLaneKind::Describe,
415        LoweredSqlCommandInner::ShowIndexesEntity => LoweredSqlLaneKind::ShowIndexes,
416        LoweredSqlCommandInner::ShowColumnsEntity => LoweredSqlLaneKind::ShowColumns,
417        LoweredSqlCommandInner::ShowEntities => LoweredSqlLaneKind::ShowEntities,
418    }
419}