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