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