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