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    GlobalAggregate(SqlGlobalAggregateCommand<E>),
97    Explain {
98        mode: SqlExplainMode,
99        query: Query<E>,
100    },
101    ExplainGlobalAggregate {
102        mode: SqlExplainMode,
103        command: SqlGlobalAggregateCommand<E>,
104    },
105    DescribeEntity,
106    ShowIndexesEntity,
107    ShowColumnsEntity,
108    ShowEntities,
109}
110
111impl LoweredSqlCommand {
112    #[must_use]
113    #[allow(dead_code)]
114    pub(in crate::db) const fn query(&self) -> Option<&LoweredSqlQuery> {
115        match &self.0 {
116            LoweredSqlCommandInner::Query(query) => Some(query),
117            LoweredSqlCommandInner::Explain { .. }
118            | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
119            | LoweredSqlCommandInner::DescribeEntity
120            | LoweredSqlCommandInner::ShowIndexesEntity
121            | LoweredSqlCommandInner::ShowColumnsEntity
122            | LoweredSqlCommandInner::ShowEntities => None,
123        }
124    }
125
126    #[must_use]
127    pub(in crate::db) fn into_query(self) -> Option<LoweredSqlQuery> {
128        match self.0 {
129            LoweredSqlCommandInner::Query(query) => Some(query),
130            LoweredSqlCommandInner::Explain { .. }
131            | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
132            | LoweredSqlCommandInner::DescribeEntity
133            | LoweredSqlCommandInner::ShowIndexesEntity
134            | LoweredSqlCommandInner::ShowColumnsEntity
135            | LoweredSqlCommandInner::ShowEntities => None,
136        }
137    }
138
139    #[must_use]
140    pub(in crate::db) const fn explain_query(&self) -> Option<(SqlExplainMode, &LoweredSqlQuery)> {
141        match &self.0 {
142            LoweredSqlCommandInner::Explain { mode, query } => Some((*mode, query)),
143            LoweredSqlCommandInner::Query(_)
144            | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
145            | LoweredSqlCommandInner::DescribeEntity
146            | LoweredSqlCommandInner::ShowIndexesEntity
147            | LoweredSqlCommandInner::ShowColumnsEntity
148            | LoweredSqlCommandInner::ShowEntities => None,
149        }
150    }
151}
152
153///
154/// LoweredSqlQuery
155///
156/// Generic-free executable SQL query shape prepared before typed query binding.
157/// Select and delete lowering stay shared until the final `Query<E>` build.
158///
159#[derive(Clone, Debug)]
160pub(crate) enum LoweredSqlQuery {
161    Select(LoweredSelectShape),
162    Delete(LoweredBaseQueryShape),
163}
164
165///
166/// SqlLoweringError
167///
168/// SQL frontend lowering failures before planner validation/execution.
169///
170#[derive(Debug, ThisError)]
171pub(crate) enum SqlLoweringError {
172    #[error("{0}")]
173    Parse(#[from] crate::db::sql::parser::SqlParseError),
174
175    #[error("{0}")]
176    Query(Box<QueryError>),
177
178    #[error("SQL entity '{sql_entity}' does not match requested entity type '{expected_entity}'")]
179    EntityMismatch {
180        sql_entity: String,
181        expected_entity: &'static str,
182    },
183
184    #[error(
185        "unsupported SQL SELECT projection; supported forms are SELECT *, field lists, global aggregate terminal lists, or grouped aggregate shapes"
186    )]
187    UnsupportedSelectProjection,
188
189    #[error("unsupported SQL SELECT DISTINCT")]
190    UnsupportedSelectDistinct,
191
192    #[error(
193        "unsupported global aggregate SQL projection; supported forms are aggregate projections such as COUNT(*), SUM(field), AVG(expr), or scalar wrappers over aggregate results"
194    )]
195    UnsupportedGlobalAggregateProjection,
196
197    #[error("global aggregate SQL does not support GROUP BY")]
198    GlobalAggregateDoesNotSupportGroupBy,
199
200    #[error("unsupported SQL GROUP BY projection shape")]
201    UnsupportedSelectGroupBy,
202
203    #[error("grouped SELECT requires an explicit projection list")]
204    GroupedProjectionRequiresExplicitList,
205
206    #[error("grouped SELECT projection must include at least one aggregate expression")]
207    GroupedProjectionRequiresAggregate,
208
209    #[error(
210        "grouped projection expression at index={index} references fields outside GROUP BY keys"
211    )]
212    GroupedProjectionReferencesNonGroupField { index: usize },
213
214    #[error(
215        "grouped projection expression at index={index} appears after aggregate expressions started"
216    )]
217    GroupedProjectionScalarAfterAggregate { index: usize },
218
219    #[error("HAVING requires GROUP BY")]
220    HavingRequiresGroupBy,
221
222    #[error("unsupported SQL HAVING shape")]
223    UnsupportedSelectHaving,
224
225    #[error("aggregate input expressions are not executable in this release")]
226    UnsupportedAggregateInputExpressions,
227
228    #[error("unsupported SQL WHERE expression shape")]
229    UnsupportedWhereExpression,
230
231    #[error("unknown field '{field}'")]
232    UnknownField { field: String },
233
234    #[error("query-lane lowering reached a non query-compatible statement")]
235    UnexpectedQueryLaneStatement,
236}
237
238impl SqlLoweringError {
239    /// Construct one entity-mismatch SQL lowering error.
240    fn entity_mismatch(sql_entity: impl Into<String>, expected_entity: &'static str) -> Self {
241        Self::EntityMismatch {
242            sql_entity: sql_entity.into(),
243            expected_entity,
244        }
245    }
246
247    /// Construct one unsupported SELECT projection SQL lowering error.
248    const fn unsupported_select_projection() -> Self {
249        Self::UnsupportedSelectProjection
250    }
251
252    /// Construct one query-lane lowering misuse error.
253    pub(crate) const fn unexpected_query_lane_statement() -> Self {
254        Self::UnexpectedQueryLaneStatement
255    }
256
257    /// Construct one unsupported SELECT DISTINCT SQL lowering error.
258    const fn unsupported_select_distinct() -> Self {
259        Self::UnsupportedSelectDistinct
260    }
261
262    /// Construct one unsupported global aggregate projection SQL lowering error.
263    const fn unsupported_global_aggregate_projection() -> Self {
264        Self::UnsupportedGlobalAggregateProjection
265    }
266
267    /// Construct one unsupported SQL WHERE expression lowering error.
268    pub(crate) const fn unsupported_where_expression() -> Self {
269        Self::UnsupportedWhereExpression
270    }
271
272    /// Construct one global-aggregate-GROUP-BY SQL lowering error.
273    const fn global_aggregate_does_not_support_group_by() -> Self {
274        Self::GlobalAggregateDoesNotSupportGroupBy
275    }
276
277    /// Construct one unsupported SELECT GROUP BY shape SQL lowering error.
278    const fn unsupported_select_group_by() -> Self {
279        Self::UnsupportedSelectGroupBy
280    }
281
282    /// Construct one grouped-projection-explicit-list SQL lowering error.
283    const fn grouped_projection_requires_explicit_list() -> Self {
284        Self::GroupedProjectionRequiresExplicitList
285    }
286
287    /// Construct one grouped-projection-missing-aggregate SQL lowering error.
288    const fn grouped_projection_requires_aggregate() -> Self {
289        Self::GroupedProjectionRequiresAggregate
290    }
291
292    /// Construct one grouped projection non-group-field SQL lowering error.
293    const fn grouped_projection_references_non_group_field(index: usize) -> Self {
294        Self::GroupedProjectionReferencesNonGroupField { index }
295    }
296
297    /// Construct one grouped projection scalar-after-aggregate SQL lowering error.
298    const fn grouped_projection_scalar_after_aggregate(index: usize) -> Self {
299        Self::GroupedProjectionScalarAfterAggregate { index }
300    }
301
302    /// Construct one HAVING-requires-GROUP-BY SQL lowering error.
303    const fn having_requires_group_by() -> Self {
304        Self::HavingRequiresGroupBy
305    }
306
307    /// Construct one unsupported SELECT HAVING shape SQL lowering error.
308    const fn unsupported_select_having() -> Self {
309        Self::UnsupportedSelectHaving
310    }
311
312    /// Construct one aggregate-input execution seam SQL lowering error.
313    const fn unsupported_aggregate_input_expressions() -> Self {
314        Self::UnsupportedAggregateInputExpressions
315    }
316
317    /// Construct one unknown-field SQL lowering error.
318    fn unknown_field(field: impl Into<String>) -> Self {
319        Self::UnknownField {
320            field: field.into(),
321        }
322    }
323}
324
325impl From<QueryError> for SqlLoweringError {
326    fn from(value: QueryError) -> Self {
327        Self::Query(Box::new(value))
328    }
329}
330
331///
332/// PreparedSqlStatement
333///
334/// SQL statement envelope after entity-scope normalization and
335/// entity-match validation for one target entity descriptor.
336///
337/// This pre-lowering contract is entity-agnostic and reusable across
338/// dynamic SQL route branches before typed `Query<E>` binding.
339///
340#[derive(Clone, Debug)]
341pub(crate) struct PreparedSqlStatement {
342    pub(in crate::db::sql::lowering) statement: SqlStatement,
343}
344
345impl PreparedSqlStatement {
346    /// Borrow one prepared SQL statement in its normalized parsed form.
347    #[must_use]
348    pub(in crate::db) const fn statement(&self) -> &SqlStatement {
349        &self.statement
350    }
351
352    /// Consume one prepared SQL statement back into its normalized parsed form.
353    #[must_use]
354    pub(in crate::db) fn into_statement(self) -> SqlStatement {
355        self.statement
356    }
357}
358
359#[derive(Clone, Copy, Debug, Eq, PartialEq)]
360pub(crate) enum LoweredSqlLaneKind {
361    Query,
362    Explain,
363    Describe,
364    ShowIndexes,
365    ShowColumns,
366    ShowEntities,
367}
368
369/// Parse and lower one SQL statement into canonical query intent for `E`.
370#[cfg(test)]
371pub(crate) fn compile_sql_command<E: EntityKind>(
372    sql: &str,
373    consistency: MissingRowPolicy,
374) -> Result<SqlCommand<E>, SqlLoweringError> {
375    let statement = crate::db::sql::parser::parse_sql(sql)?;
376    let prepared = prepare_sql_statement(statement, E::MODEL.name())?;
377
378    if aggregate::is_sql_global_aggregate_statement(prepared.statement()) {
379        return Ok(SqlCommand::GlobalAggregate(
380            aggregate::compile_sql_global_aggregate_command_from_prepared::<E>(
381                prepared,
382                consistency,
383            )?,
384        ));
385    }
386
387    let lowered = lower_sql_command_from_prepared_statement(prepared, E::MODEL)?;
388
389    // Keep the test-only typed envelope local to the single public test entry
390    // point instead of preserving a private forwarding chain.
391    match lowered.0 {
392        LoweredSqlCommandInner::Query(query) => Ok(SqlCommand::Query(bind_lowered_sql_query::<E>(
393            query,
394            consistency,
395        )?)),
396        LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command } => {
397            Ok(SqlCommand::ExplainGlobalAggregate {
398                mode,
399                command: aggregate::bind_lowered_sql_global_aggregate_command::<E>(
400                    command,
401                    consistency,
402                )?,
403            })
404        }
405        LoweredSqlCommandInner::Explain { mode, query } => Ok(SqlCommand::Explain {
406            mode,
407            query: bind_lowered_sql_query::<E>(query, consistency)?,
408        }),
409        LoweredSqlCommandInner::DescribeEntity => Ok(SqlCommand::DescribeEntity),
410        LoweredSqlCommandInner::ShowIndexesEntity => Ok(SqlCommand::ShowIndexesEntity),
411        LoweredSqlCommandInner::ShowColumnsEntity => Ok(SqlCommand::ShowColumnsEntity),
412        LoweredSqlCommandInner::ShowEntities => Ok(SqlCommand::ShowEntities),
413    }
414}
415
416pub(crate) const fn lowered_sql_command_lane(command: &LoweredSqlCommand) -> LoweredSqlLaneKind {
417    match command.0 {
418        LoweredSqlCommandInner::Query(_) => LoweredSqlLaneKind::Query,
419        LoweredSqlCommandInner::Explain { .. }
420        | LoweredSqlCommandInner::ExplainGlobalAggregate { .. } => LoweredSqlLaneKind::Explain,
421        LoweredSqlCommandInner::DescribeEntity => LoweredSqlLaneKind::Describe,
422        LoweredSqlCommandInner::ShowIndexesEntity => LoweredSqlLaneKind::ShowIndexes,
423        LoweredSqlCommandInner::ShowColumnsEntity => LoweredSqlLaneKind::ShowColumns,
424        LoweredSqlCommandInner::ShowEntities => LoweredSqlLaneKind::ShowEntities,
425    }
426}