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