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