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