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    TypedSqlGlobalAggregateTerminal, 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)]
69#[expect(
70    clippy::large_enum_variant,
71    reason = "global aggregate lowering keeps one owned command payload on the generic-free SQL seam"
72)]
73pub(in crate::db::sql::lowering) enum LoweredSqlCommandInner {
74    Query(LoweredSqlQuery),
75    Explain {
76        mode: SqlExplainMode,
77        query: LoweredSqlQuery,
78    },
79    ExplainGlobalAggregate {
80        mode: SqlExplainMode,
81        command: LoweredSqlGlobalAggregateCommand,
82    },
83    DescribeEntity,
84    ShowIndexesEntity,
85    ShowColumnsEntity,
86    ShowEntities,
87}
88
89///
90/// SqlCommand
91///
92/// Test-only typed SQL command shell over the shared lowered SQL surface.
93/// Runtime dispatch now consumes `LoweredSqlCommand` directly, but lowering
94/// tests still validate typed binding behavior on this local envelope.
95///
96#[cfg(test)]
97#[derive(Debug)]
98#[expect(
99    clippy::large_enum_variant,
100    reason = "typed SQL tests keep one owned global aggregate command to validate binding without adding box indirection"
101)]
102pub(crate) enum SqlCommand<E: EntityKind> {
103    Query(Query<E>),
104    Explain {
105        mode: SqlExplainMode,
106        query: Query<E>,
107    },
108    ExplainGlobalAggregate {
109        mode: SqlExplainMode,
110        command: SqlGlobalAggregateCommand<E>,
111    },
112    DescribeEntity,
113    ShowIndexesEntity,
114    ShowColumnsEntity,
115    ShowEntities,
116}
117
118impl LoweredSqlCommand {
119    #[must_use]
120    #[cfg_attr(not(any(test, feature = "diagnostics")), allow(dead_code))]
121    pub(in crate::db) const fn query(&self) -> Option<&LoweredSqlQuery> {
122        match &self.0 {
123            LoweredSqlCommandInner::Query(query) => Some(query),
124            LoweredSqlCommandInner::Explain { .. }
125            | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
126            | LoweredSqlCommandInner::DescribeEntity
127            | LoweredSqlCommandInner::ShowIndexesEntity
128            | LoweredSqlCommandInner::ShowColumnsEntity
129            | LoweredSqlCommandInner::ShowEntities => None,
130        }
131    }
132
133    #[must_use]
134    pub(in crate::db) fn into_query(self) -> Option<LoweredSqlQuery> {
135        match self.0 {
136            LoweredSqlCommandInner::Query(query) => Some(query),
137            LoweredSqlCommandInner::Explain { .. }
138            | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
139            | LoweredSqlCommandInner::DescribeEntity
140            | LoweredSqlCommandInner::ShowIndexesEntity
141            | LoweredSqlCommandInner::ShowColumnsEntity
142            | LoweredSqlCommandInner::ShowEntities => None,
143        }
144    }
145
146    #[must_use]
147    pub(in crate::db) const fn explain_query(&self) -> Option<(SqlExplainMode, &LoweredSqlQuery)> {
148        match &self.0 {
149            LoweredSqlCommandInner::Explain { mode, query } => Some((*mode, query)),
150            LoweredSqlCommandInner::Query(_)
151            | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
152            | LoweredSqlCommandInner::DescribeEntity
153            | LoweredSqlCommandInner::ShowIndexesEntity
154            | LoweredSqlCommandInner::ShowColumnsEntity
155            | LoweredSqlCommandInner::ShowEntities => None,
156        }
157    }
158}
159
160///
161/// LoweredSqlQuery
162///
163/// Generic-free executable SQL query shape prepared before typed query binding.
164/// Select and delete lowering stay shared until the final `Query<E>` build.
165///
166#[derive(Clone, Debug)]
167pub(crate) enum LoweredSqlQuery {
168    Select(LoweredSelectShape),
169    Delete(LoweredBaseQueryShape),
170}
171
172///
173/// SqlLoweringError
174///
175/// SQL frontend lowering failures before planner validation/execution.
176///
177#[derive(Debug, ThisError)]
178pub(crate) enum SqlLoweringError {
179    #[error("{0}")]
180    Parse(#[from] crate::db::sql::parser::SqlParseError),
181
182    #[error("{0}")]
183    Query(Box<QueryError>),
184
185    #[error("SQL entity '{sql_entity}' does not match requested entity type '{expected_entity}'")]
186    EntityMismatch {
187        sql_entity: String,
188        expected_entity: &'static str,
189    },
190
191    #[error(
192        "unsupported SQL SELECT projection; supported forms are SELECT *, field lists, global aggregate terminal lists, or grouped aggregate shapes"
193    )]
194    UnsupportedSelectProjection,
195
196    #[error("unsupported SQL SELECT DISTINCT")]
197    UnsupportedSelectDistinct,
198
199    #[error(
200        "unsupported global aggregate SQL projection; supported forms are aggregate projections such as COUNT(*), SUM(field), AVG(expr), or scalar wrappers over aggregate results"
201    )]
202    UnsupportedGlobalAggregateProjection,
203
204    #[error("global aggregate SQL does not support GROUP BY")]
205    GlobalAggregateDoesNotSupportGroupBy,
206
207    #[error("unsupported SQL GROUP BY projection shape")]
208    UnsupportedSelectGroupBy,
209
210    #[error("grouped SELECT requires an explicit projection list")]
211    GroupedProjectionRequiresExplicitList,
212
213    #[error("grouped SELECT projection must include at least one aggregate expression")]
214    GroupedProjectionRequiresAggregate,
215
216    #[error(
217        "grouped projection expression at index={index} references fields outside GROUP BY keys"
218    )]
219    GroupedProjectionReferencesNonGroupField { index: usize },
220
221    #[error(
222        "grouped projection expression at index={index} appears after aggregate expressions started"
223    )]
224    GroupedProjectionScalarAfterAggregate { index: usize },
225
226    #[error("HAVING requires GROUP BY")]
227    HavingRequiresGroupBy,
228
229    #[error("unsupported SQL HAVING shape")]
230    UnsupportedSelectHaving,
231
232    #[error("aggregate input expressions are not executable in this release")]
233    UnsupportedAggregateInputExpressions,
234
235    #[error("unsupported SQL WHERE expression shape")]
236    UnsupportedWhereExpression,
237
238    #[error("unknown field '{field}'")]
239    UnknownField { field: String },
240
241    #[error("ORDER BY alias '{alias}' does not resolve to a supported order target")]
242    UnsupportedOrderByAlias { alias: String },
243
244    #[error("query-lane lowering reached a non query-compatible statement")]
245    UnexpectedQueryLaneStatement,
246}
247
248impl SqlLoweringError {
249    /// Construct one entity-mismatch SQL lowering error.
250    fn entity_mismatch(sql_entity: impl Into<String>, expected_entity: &'static str) -> Self {
251        Self::EntityMismatch {
252            sql_entity: sql_entity.into(),
253            expected_entity,
254        }
255    }
256
257    /// Construct one unsupported SELECT projection SQL lowering error.
258    const fn unsupported_select_projection() -> Self {
259        Self::UnsupportedSelectProjection
260    }
261
262    /// Construct one query-lane lowering misuse error.
263    pub(crate) const fn unexpected_query_lane_statement() -> Self {
264        Self::UnexpectedQueryLaneStatement
265    }
266
267    /// Construct one unsupported SELECT DISTINCT SQL lowering error.
268    const fn unsupported_select_distinct() -> Self {
269        Self::UnsupportedSelectDistinct
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    /// Construct one unsupported ORDER BY alias SQL lowering error.
335    fn unsupported_order_by_alias(alias: impl Into<String>) -> Self {
336        Self::UnsupportedOrderByAlias {
337            alias: alias.into(),
338        }
339    }
340}
341
342impl From<QueryError> for SqlLoweringError {
343    fn from(value: QueryError) -> Self {
344        Self::Query(Box::new(value))
345    }
346}
347
348///
349/// PreparedSqlStatement
350///
351/// SQL statement envelope after entity-scope normalization and
352/// entity-match validation for one target entity descriptor.
353///
354/// This pre-lowering contract is entity-agnostic and reusable across
355/// dynamic SQL route branches before typed `Query<E>` binding.
356///
357#[derive(Clone, Debug)]
358pub(crate) struct PreparedSqlStatement {
359    pub(in crate::db::sql::lowering) statement: SqlStatement,
360}
361
362impl PreparedSqlStatement {
363    /// Consume one prepared SQL statement back into its normalized parsed form.
364    #[must_use]
365    pub(in crate::db) fn into_statement(self) -> SqlStatement {
366        self.statement
367    }
368}
369
370#[derive(Clone, Copy, Debug, Eq, PartialEq)]
371pub(crate) enum LoweredSqlLaneKind {
372    Query,
373    Explain,
374    Describe,
375    ShowIndexes,
376    ShowColumns,
377    ShowEntities,
378}
379
380/// Parse and lower one SQL statement into canonical query intent for `E`.
381#[cfg(test)]
382pub(crate) fn compile_sql_command<E: EntityKind>(
383    sql: &str,
384    consistency: MissingRowPolicy,
385) -> Result<SqlCommand<E>, SqlLoweringError> {
386    let statement = crate::db::sql::parser::parse_sql(sql)?;
387    let prepared = prepare_sql_statement(statement, E::MODEL.name())?;
388    let lowered = lower_sql_command_from_prepared_statement(prepared, E::MODEL)?;
389
390    // Keep the test-only typed envelope local to the single public test entry
391    // point instead of preserving a private forwarding chain.
392    match lowered.0 {
393        LoweredSqlCommandInner::Query(query) => Ok(SqlCommand::Query(bind_lowered_sql_query::<E>(
394            query,
395            consistency,
396        )?)),
397        LoweredSqlCommandInner::Explain { mode, query } => Ok(SqlCommand::Explain {
398            mode,
399            query: bind_lowered_sql_query::<E>(query, consistency)?,
400        }),
401        LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command } => {
402            Ok(SqlCommand::ExplainGlobalAggregate {
403                mode,
404                command: aggregate::bind_lowered_sql_global_aggregate_command::<E>(
405                    command,
406                    consistency,
407                )?,
408            })
409        }
410        LoweredSqlCommandInner::DescribeEntity => Ok(SqlCommand::DescribeEntity),
411        LoweredSqlCommandInner::ShowIndexesEntity => Ok(SqlCommand::ShowIndexesEntity),
412        LoweredSqlCommandInner::ShowColumnsEntity => Ok(SqlCommand::ShowColumnsEntity),
413        LoweredSqlCommandInner::ShowEntities => Ok(SqlCommand::ShowEntities),
414    }
415}
416
417pub(crate) const fn lowered_sql_command_lane(command: &LoweredSqlCommand) -> LoweredSqlLaneKind {
418    match command.0 {
419        LoweredSqlCommandInner::Query(_) => LoweredSqlLaneKind::Query,
420        LoweredSqlCommandInner::Explain { .. }
421        | LoweredSqlCommandInner::ExplainGlobalAggregate { .. } => LoweredSqlLaneKind::Explain,
422        LoweredSqlCommandInner::DescribeEntity => LoweredSqlLaneKind::Describe,
423        LoweredSqlCommandInner::ShowIndexesEntity => LoweredSqlLaneKind::ShowIndexes,
424        LoweredSqlCommandInner::ShowColumnsEntity => LoweredSqlLaneKind::ShowColumns,
425        LoweredSqlCommandInner::ShowEntities => LoweredSqlLaneKind::ShowEntities,
426    }
427}