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