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