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