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