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 expr::{
51    PreparedSqlPredicateTemplateShape, sql_expr_is_compound_boolean_shape,
52    sql_expr_prepared_predicate_template_shape,
53};
54pub(in crate::db) use predicate::lower_sql_where_expr;
55pub(in crate::db) use prepare::prepared_sql_simple_range_slots;
56pub(in crate::db) use prepare::sql_statement_contains_any_literal;
57pub(crate) use prepare::{lower_sql_command_from_prepared_statement, prepare_sql_statement};
58pub(in crate::db::sql::lowering) use select::apply_lowered_base_query_shape;
59#[cfg(test)]
60pub(in crate::db) use select::apply_lowered_select_shape;
61pub(crate) use select::{LoweredBaseQueryShape, LoweredSelectShape};
62pub(in crate::db) use select::{
63    bind_lowered_sql_query, 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    #[must_use]
128    #[allow(dead_code)]
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    #[must_use]
155    pub(in crate::db) const fn explain_query(
156        &self,
157    ) -> Option<(SqlExplainMode, bool, &LoweredSqlQuery)> {
158        match &self.0 {
159            LoweredSqlCommandInner::Explain {
160                mode,
161                verbose,
162                query,
163            } => Some((*mode, *verbose, query)),
164            LoweredSqlCommandInner::Query(_)
165            | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
166            | LoweredSqlCommandInner::DescribeEntity
167            | LoweredSqlCommandInner::ShowIndexesEntity
168            | LoweredSqlCommandInner::ShowColumnsEntity
169            | LoweredSqlCommandInner::ShowEntities => None,
170        }
171    }
172}
173
174///
175/// LoweredSqlQuery
176///
177/// Generic-free executable SQL query shape prepared before typed query binding.
178/// Select and delete lowering stay shared until the final `Query<E>` build.
179///
180#[derive(Clone, Debug)]
181pub(crate) enum LoweredSqlQuery {
182    Select(LoweredSelectShape),
183    Delete(LoweredBaseQueryShape),
184}
185
186///
187/// SqlLoweringError
188///
189/// SQL frontend lowering failures before planner validation/execution.
190///
191#[derive(Debug, ThisError)]
192pub(crate) enum SqlLoweringError {
193    #[error("{0}")]
194    Parse(#[from] crate::db::sql::parser::SqlParseError),
195
196    #[error("{0}")]
197    Query(Box<QueryError>),
198
199    #[error("SQL entity '{sql_entity}' does not match requested entity type '{expected_entity}'")]
200    EntityMismatch {
201        sql_entity: String,
202        expected_entity: &'static str,
203    },
204
205    #[error(
206        "unsupported SQL SELECT projection; supported forms are SELECT *, field lists, global aggregate terminal lists, or grouped aggregate shapes"
207    )]
208    UnsupportedSelectProjection,
209
210    #[error("unsupported SQL SELECT DISTINCT")]
211    UnsupportedSelectDistinct,
212
213    #[error("SELECT DISTINCT ORDER BY terms must be derivable from the projected distinct tuple")]
214    DistinctOrderByRequiresProjectedTuple,
215
216    #[error(
217        "unsupported global aggregate SQL projection; supported forms are aggregate projections such as COUNT(*), SUM(field), AVG(expr), or scalar wrappers over aggregate results"
218    )]
219    UnsupportedGlobalAggregateProjection,
220
221    #[error("global aggregate SQL does not support GROUP BY")]
222    GlobalAggregateDoesNotSupportGroupBy,
223
224    #[error("unsupported SQL GROUP BY projection shape")]
225    UnsupportedSelectGroupBy,
226
227    #[error("grouped SELECT requires an explicit projection list")]
228    GroupedProjectionRequiresExplicitList,
229
230    #[error("grouped SELECT projection must include at least one aggregate expression")]
231    GroupedProjectionRequiresAggregate,
232
233    #[error(
234        "grouped projection expression at index={index} references fields outside GROUP BY keys"
235    )]
236    GroupedProjectionReferencesNonGroupField { index: usize },
237
238    #[error(
239        "grouped projection expression at index={index} appears after aggregate expressions started"
240    )]
241    GroupedProjectionScalarAfterAggregate { index: usize },
242
243    #[error("HAVING requires GROUP BY")]
244    HavingRequiresGroupBy,
245
246    #[error("unsupported SQL HAVING shape")]
247    UnsupportedSelectHaving,
248
249    #[error("aggregate input expressions are not executable in this release")]
250    UnsupportedAggregateInputExpressions,
251
252    #[error("unsupported SQL WHERE expression shape")]
253    UnsupportedWhereExpression,
254
255    #[error("unknown field '{field}'")]
256    UnknownField { field: String },
257
258    #[error("{message}")]
259    UnsupportedParameterPlacement { message: String },
260
261    #[error("query-lane lowering reached a non query-compatible statement")]
262    UnexpectedQueryLaneStatement,
263}
264
265impl SqlLoweringError {
266    /// Construct one entity-mismatch SQL lowering error.
267    fn entity_mismatch(sql_entity: impl Into<String>, expected_entity: &'static str) -> Self {
268        Self::EntityMismatch {
269            sql_entity: sql_entity.into(),
270            expected_entity,
271        }
272    }
273
274    /// Construct one unsupported SELECT projection SQL lowering error.
275    const fn unsupported_select_projection() -> Self {
276        Self::UnsupportedSelectProjection
277    }
278
279    /// Construct one query-lane lowering misuse error.
280    pub(crate) const fn unexpected_query_lane_statement() -> Self {
281        Self::UnexpectedQueryLaneStatement
282    }
283
284    /// Construct one unsupported SELECT DISTINCT SQL lowering error.
285    const fn unsupported_select_distinct() -> Self {
286        Self::UnsupportedSelectDistinct
287    }
288
289    /// Construct one DISTINCT ORDER BY projection-derivability SQL lowering error.
290    const fn distinct_order_by_requires_projected_tuple() -> Self {
291        Self::DistinctOrderByRequiresProjectedTuple
292    }
293
294    /// Construct one unsupported global aggregate projection SQL lowering error.
295    const fn unsupported_global_aggregate_projection() -> Self {
296        Self::UnsupportedGlobalAggregateProjection
297    }
298
299    /// Construct one unsupported SQL WHERE expression lowering error.
300    pub(crate) const fn unsupported_where_expression() -> Self {
301        Self::UnsupportedWhereExpression
302    }
303
304    /// Construct one global-aggregate-GROUP-BY SQL lowering error.
305    const fn global_aggregate_does_not_support_group_by() -> Self {
306        Self::GlobalAggregateDoesNotSupportGroupBy
307    }
308
309    /// Construct one unsupported SELECT GROUP BY shape SQL lowering error.
310    const fn unsupported_select_group_by() -> Self {
311        Self::UnsupportedSelectGroupBy
312    }
313
314    /// Construct one grouped-projection-explicit-list SQL lowering error.
315    const fn grouped_projection_requires_explicit_list() -> Self {
316        Self::GroupedProjectionRequiresExplicitList
317    }
318
319    /// Construct one grouped-projection-missing-aggregate SQL lowering error.
320    const fn grouped_projection_requires_aggregate() -> Self {
321        Self::GroupedProjectionRequiresAggregate
322    }
323
324    /// Construct one grouped projection non-group-field SQL lowering error.
325    const fn grouped_projection_references_non_group_field(index: usize) -> Self {
326        Self::GroupedProjectionReferencesNonGroupField { index }
327    }
328
329    /// Construct one grouped projection scalar-after-aggregate SQL lowering error.
330    const fn grouped_projection_scalar_after_aggregate(index: usize) -> Self {
331        Self::GroupedProjectionScalarAfterAggregate { index }
332    }
333
334    /// Construct one HAVING-requires-GROUP-BY SQL lowering error.
335    const fn having_requires_group_by() -> Self {
336        Self::HavingRequiresGroupBy
337    }
338
339    /// Construct one unsupported SELECT HAVING shape SQL lowering error.
340    const fn unsupported_select_having() -> Self {
341        Self::UnsupportedSelectHaving
342    }
343
344    /// Construct one aggregate-input execution seam SQL lowering error.
345    const fn unsupported_aggregate_input_expressions() -> Self {
346        Self::UnsupportedAggregateInputExpressions
347    }
348
349    /// Construct one unknown-field SQL lowering error.
350    pub(crate) fn unknown_field(field: impl Into<String>) -> Self {
351        Self::UnknownField {
352            field: field.into(),
353        }
354    }
355
356    /// Construct one unsupported parameter placement SQL lowering error.
357    pub(crate) fn unsupported_parameter_placement(
358        index: Option<usize>,
359        message: impl Into<String>,
360    ) -> Self {
361        let message = match index {
362            Some(index) => format!("parameter slot ${index}: {}", message.into()),
363            None => message.into(),
364        };
365
366        Self::UnsupportedParameterPlacement { message }
367    }
368}
369
370impl From<QueryError> for SqlLoweringError {
371    fn from(value: QueryError) -> Self {
372        Self::Query(Box::new(value))
373    }
374}
375
376///
377/// PreparedSqlStatement
378///
379/// SQL statement envelope after entity-scope normalization and
380/// entity-match validation for one target entity descriptor.
381///
382/// This pre-lowering contract is entity-agnostic and reusable across
383/// dynamic SQL route branches before typed `Query<E>` binding.
384///
385#[derive(Clone, Debug)]
386pub(crate) struct PreparedSqlStatement {
387    pub(in crate::db::sql::lowering) statement: SqlStatement,
388}
389
390///
391/// PreparedSqlParameterTypeFamily
392///
393/// Stable bind-time type family for one prepared SQL parameter slot.
394/// This keeps v1 validation coarse and deterministic while the prepared SQL
395/// surface remains restricted to compare-family value-insensitive positions.
396///
397
398#[derive(Clone, Copy, Debug, Eq, PartialEq)]
399pub(in crate::db) enum PreparedSqlParameterTypeFamily {
400    Numeric,
401    Text,
402    Bool,
403}
404
405///
406/// PreparedSqlParameterContract
407///
408/// Frozen bind contract for one prepared SQL parameter slot.
409/// The contract is inferred once during prepare and reused unchanged for every
410/// execution of the prepared SQL query shape.
411///
412
413#[derive(Clone, Debug, Eq, PartialEq)]
414pub(in crate::db) struct PreparedSqlParameterContract {
415    index: usize,
416    type_family: PreparedSqlParameterTypeFamily,
417    null_allowed: bool,
418    template_binding: Option<Value>,
419}
420
421impl PreparedSqlParameterContract {
422    #[must_use]
423    pub(in crate::db) const fn new(
424        index: usize,
425        type_family: PreparedSqlParameterTypeFamily,
426        null_allowed: bool,
427        template_binding: Option<Value>,
428    ) -> Self {
429        Self {
430            index,
431            type_family,
432            null_allowed,
433            template_binding,
434        }
435    }
436
437    #[must_use]
438    pub(in crate::db) const fn index(&self) -> usize {
439        self.index
440    }
441
442    #[must_use]
443    pub(in crate::db) const fn type_family(&self) -> PreparedSqlParameterTypeFamily {
444        self.type_family
445    }
446
447    #[must_use]
448    pub(in crate::db) const fn null_allowed(&self) -> bool {
449        self.null_allowed
450    }
451
452    #[must_use]
453    pub(in crate::db) const fn template_binding(&self) -> Option<&Value> {
454        self.template_binding.as_ref()
455    }
456}
457
458impl PreparedSqlStatement {
459    /// Borrow one prepared SQL statement in its normalized parsed form.
460    #[must_use]
461    pub(in crate::db) const fn statement(&self) -> &SqlStatement {
462        &self.statement
463    }
464
465    /// Consume one prepared SQL statement back into its normalized parsed form.
466    #[must_use]
467    pub(in crate::db) fn into_statement(self) -> SqlStatement {
468        self.statement
469    }
470
471    /// Collect frozen parameter contracts from one prepared SQL statement.
472    pub(in crate::db) fn parameter_contracts(
473        &self,
474        model: &'static crate::model::entity::EntityModel,
475    ) -> Result<Vec<PreparedSqlParameterContract>, SqlLoweringError> {
476        prepare::collect_prepared_statement_parameter_contracts(&self.statement, model)
477    }
478
479    /// Report whether this prepared SQL statement uses parameterized general
480    /// expression shapes that must stay off template lanes.
481    #[must_use]
482    pub(in crate::db) fn uses_general_template_expr_parameters(&self) -> bool {
483        prepare::prepared_statement_uses_general_template_expr_parameters(&self.statement)
484    }
485
486    /// Rebind one prepared SQL statement back to a literal-backed SQL shape.
487    pub(in crate::db) fn bind_literals(
488        &self,
489        bindings: &[Value],
490    ) -> Result<SqlStatement, QueryError> {
491        prepare::bind_prepared_statement_literals(&self.statement, bindings)
492    }
493}
494
495#[derive(Clone, Copy, Debug, Eq, PartialEq)]
496pub(crate) enum LoweredSqlLaneKind {
497    Query,
498    Explain,
499    Describe,
500    ShowIndexes,
501    ShowColumns,
502    ShowEntities,
503}
504
505/// Parse and lower one SQL statement into canonical query intent for `E`.
506#[cfg(test)]
507pub(crate) fn compile_sql_command<E: EntityKind>(
508    sql: &str,
509    consistency: MissingRowPolicy,
510) -> Result<SqlCommand<E>, SqlLoweringError> {
511    let statement = crate::db::sql::parser::parse_sql(sql)?;
512    let prepared = prepare_sql_statement(statement, E::MODEL.name())?;
513
514    if aggregate::is_sql_global_aggregate_statement(prepared.statement()) {
515        return Ok(SqlCommand::GlobalAggregate(
516            aggregate::compile_sql_global_aggregate_command_from_prepared::<E>(
517                prepared,
518                consistency,
519            )?,
520        ));
521    }
522
523    let lowered = lower_sql_command_from_prepared_statement(prepared, E::MODEL)?;
524
525    // Keep the test-only typed envelope local to the single public test entry
526    // point instead of preserving a private forwarding chain.
527    match lowered.0 {
528        LoweredSqlCommandInner::Query(query) => Ok(SqlCommand::Query(bind_lowered_sql_query::<E>(
529            query,
530            consistency,
531        )?)),
532        LoweredSqlCommandInner::ExplainGlobalAggregate {
533            mode,
534            verbose,
535            command,
536        } => Ok(SqlCommand::ExplainGlobalAggregate {
537            mode,
538            verbose,
539            command: aggregate::bind_lowered_sql_global_aggregate_command::<E>(
540                command,
541                consistency,
542            )?,
543        }),
544        LoweredSqlCommandInner::Explain {
545            mode,
546            verbose,
547            query,
548        } => Ok(SqlCommand::Explain {
549            mode,
550            verbose,
551            query: bind_lowered_sql_query::<E>(query, consistency)?,
552        }),
553        LoweredSqlCommandInner::DescribeEntity => Ok(SqlCommand::DescribeEntity),
554        LoweredSqlCommandInner::ShowIndexesEntity => Ok(SqlCommand::ShowIndexesEntity),
555        LoweredSqlCommandInner::ShowColumnsEntity => Ok(SqlCommand::ShowColumnsEntity),
556        LoweredSqlCommandInner::ShowEntities => Ok(SqlCommand::ShowEntities),
557    }
558}
559
560pub(crate) const fn lowered_sql_command_lane(command: &LoweredSqlCommand) -> LoweredSqlLaneKind {
561    match command.0 {
562        LoweredSqlCommandInner::Query(_) => LoweredSqlLaneKind::Query,
563        LoweredSqlCommandInner::Explain { .. }
564        | LoweredSqlCommandInner::ExplainGlobalAggregate { .. } => LoweredSqlLaneKind::Explain,
565        LoweredSqlCommandInner::DescribeEntity => LoweredSqlLaneKind::Describe,
566        LoweredSqlCommandInner::ShowIndexesEntity => LoweredSqlLaneKind::ShowIndexes,
567        LoweredSqlCommandInner::ShowColumnsEntity => LoweredSqlLaneKind::ShowColumns,
568        LoweredSqlCommandInner::ShowEntities => LoweredSqlLaneKind::ShowEntities,
569    }
570}