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