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