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