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