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