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(
195        "unsupported global aggregate SQL projection; supported forms are aggregate projections such as COUNT(*), SUM(field), AVG(expr), or scalar wrappers over aggregate results"
196    )]
197    UnsupportedGlobalAggregateProjection,
198
199    #[error("global aggregate SQL does not support GROUP BY")]
200    GlobalAggregateDoesNotSupportGroupBy,
201
202    #[error("unsupported SQL GROUP BY projection shape")]
203    UnsupportedSelectGroupBy,
204
205    #[error("grouped SELECT requires an explicit projection list")]
206    GroupedProjectionRequiresExplicitList,
207
208    #[error("grouped SELECT projection must include at least one aggregate expression")]
209    GroupedProjectionRequiresAggregate,
210
211    #[error(
212        "grouped projection expression at index={index} references fields outside GROUP BY keys"
213    )]
214    GroupedProjectionReferencesNonGroupField { index: usize },
215
216    #[error(
217        "grouped projection expression at index={index} appears after aggregate expressions started"
218    )]
219    GroupedProjectionScalarAfterAggregate { index: usize },
220
221    #[error("HAVING requires GROUP BY")]
222    HavingRequiresGroupBy,
223
224    #[error("unsupported SQL HAVING shape")]
225    UnsupportedSelectHaving,
226
227    #[error("aggregate input expressions are not executable in this release")]
228    UnsupportedAggregateInputExpressions,
229
230    #[error("unsupported SQL WHERE expression shape")]
231    UnsupportedWhereExpression,
232
233    #[error("unknown field '{field}'")]
234    UnknownField { field: 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
327impl From<QueryError> for SqlLoweringError {
328    fn from(value: QueryError) -> Self {
329        Self::Query(Box::new(value))
330    }
331}
332
333///
334/// PreparedSqlStatement
335///
336/// SQL statement envelope after entity-scope normalization and
337/// entity-match validation for one target entity descriptor.
338///
339/// This pre-lowering contract is entity-agnostic and reusable across
340/// dynamic SQL route branches before typed `Query<E>` binding.
341///
342#[derive(Clone, Debug)]
343pub(crate) struct PreparedSqlStatement {
344    pub(in crate::db::sql::lowering) statement: SqlStatement,
345}
346
347impl PreparedSqlStatement {
348    /// Borrow one prepared SQL statement in its normalized parsed form.
349    #[must_use]
350    pub(in crate::db) const fn statement(&self) -> &SqlStatement {
351        &self.statement
352    }
353
354    /// Consume one prepared SQL statement back into its normalized parsed form.
355    #[must_use]
356    pub(in crate::db) fn into_statement(self) -> SqlStatement {
357        self.statement
358    }
359}
360
361#[derive(Clone, Copy, Debug, Eq, PartialEq)]
362pub(crate) enum LoweredSqlLaneKind {
363    Query,
364    Explain,
365    Describe,
366    ShowIndexes,
367    ShowColumns,
368    ShowEntities,
369}
370
371/// Parse and lower one SQL statement into canonical query intent for `E`.
372#[cfg(test)]
373pub(crate) fn compile_sql_command<E: EntityKind>(
374    sql: &str,
375    consistency: MissingRowPolicy,
376) -> Result<SqlCommand<E>, SqlLoweringError> {
377    let statement = crate::db::sql::parser::parse_sql(sql)?;
378    let prepared = prepare_sql_statement(statement, E::MODEL.name())?;
379
380    if aggregate::is_sql_global_aggregate_statement(prepared.statement()) {
381        return Ok(SqlCommand::GlobalAggregate(
382            aggregate::compile_sql_global_aggregate_command_from_prepared::<E>(
383                prepared,
384                consistency,
385            )?,
386        ));
387    }
388
389    let lowered = lower_sql_command_from_prepared_statement(prepared, E::MODEL)?;
390
391    // Keep the test-only typed envelope local to the single public test entry
392    // point instead of preserving a private forwarding chain.
393    match lowered.0 {
394        LoweredSqlCommandInner::Query(query) => Ok(SqlCommand::Query(bind_lowered_sql_query::<E>(
395            query,
396            consistency,
397        )?)),
398        LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command } => {
399            Ok(SqlCommand::ExplainGlobalAggregate {
400                mode,
401                command: aggregate::bind_lowered_sql_global_aggregate_command::<E>(
402                    command,
403                    consistency,
404                )?,
405            })
406        }
407        LoweredSqlCommandInner::Explain { mode, query } => Ok(SqlCommand::Explain {
408            mode,
409            query: bind_lowered_sql_query::<E>(query, consistency)?,
410        }),
411        LoweredSqlCommandInner::DescribeEntity => Ok(SqlCommand::DescribeEntity),
412        LoweredSqlCommandInner::ShowIndexesEntity => Ok(SqlCommand::ShowIndexesEntity),
413        LoweredSqlCommandInner::ShowColumnsEntity => Ok(SqlCommand::ShowColumnsEntity),
414        LoweredSqlCommandInner::ShowEntities => Ok(SqlCommand::ShowEntities),
415    }
416}
417
418pub(crate) const fn lowered_sql_command_lane(command: &LoweredSqlCommand) -> LoweredSqlLaneKind {
419    match command.0 {
420        LoweredSqlCommandInner::Query(_) => LoweredSqlLaneKind::Query,
421        LoweredSqlCommandInner::Explain { .. }
422        | LoweredSqlCommandInner::ExplainGlobalAggregate { .. } => LoweredSqlLaneKind::Explain,
423        LoweredSqlCommandInner::DescribeEntity => LoweredSqlLaneKind::Describe,
424        LoweredSqlCommandInner::ShowIndexesEntity => LoweredSqlLaneKind::ShowIndexes,
425        LoweredSqlCommandInner::ShowColumnsEntity => LoweredSqlLaneKind::ShowColumns,
426        LoweredSqlCommandInner::ShowEntities => LoweredSqlLaneKind::ShowEntities,
427    }
428}