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