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