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