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
6///
7/// TESTS
8///
9
10#[cfg(test)]
11mod tests;
12
13use crate::{
14    db::{
15        predicate::{MissingRowPolicy, Predicate},
16        query::{
17            builder::aggregate::{avg, count, count_by, max_by, min_by, sum},
18            intent::{Query, QueryError, StructuralQuery},
19        },
20        sql::identifier::{
21            identifier_last_segment, identifiers_tail_match, normalize_identifier_to_scope,
22            rewrite_field_identifiers,
23        },
24        sql::parser::{
25            SqlAggregateCall, SqlAggregateKind, SqlDeleteStatement, SqlExplainMode,
26            SqlExplainStatement, SqlExplainTarget, SqlHavingClause, SqlHavingSymbol,
27            SqlOrderDirection, SqlOrderTerm, SqlProjection, SqlSelectItem, SqlSelectStatement,
28            SqlStatement, parse_sql,
29        },
30    },
31    traits::EntityKind,
32};
33use thiserror::Error as ThisError;
34
35///
36/// SqlCommand
37///
38/// Lowered SQL command for one entity-typed query surface.
39///
40/// `Query` contains executable load/delete intent.
41/// `Explain` wraps one executable query intent plus requested explain mode.
42/// `ExplainGlobalAggregate` wraps one constrained global aggregate SQL command
43/// plus requested explain mode.
44///
45
46#[derive(Debug)]
47pub(crate) enum SqlCommand<E: EntityKind> {
48    Query(Query<E>),
49    Explain {
50        mode: SqlExplainMode,
51        query: Query<E>,
52    },
53    ExplainGlobalAggregate {
54        mode: SqlExplainMode,
55        command: SqlGlobalAggregateCommand<E>,
56    },
57    DescribeEntity,
58    ShowIndexesEntity,
59    ShowColumnsEntity,
60    ShowEntities,
61}
62
63///
64/// LoweredSqlCommand
65///
66/// Generic-free SQL command shape after reduced SQL parsing and entity-route
67/// normalization.
68/// This keeps statement-shape lowering shared across entities before typed
69/// `Query<E>` binding happens at the execution boundary.
70///
71#[derive(Clone, Debug)]
72pub struct LoweredSqlCommand(LoweredSqlCommandInner);
73
74#[derive(Clone, Debug)]
75enum LoweredSqlCommandInner {
76    Query(LoweredSqlQuery),
77    Explain {
78        mode: SqlExplainMode,
79        query: LoweredSqlQuery,
80    },
81    ExplainGlobalAggregate {
82        mode: SqlExplainMode,
83        command: LoweredSqlGlobalAggregateCommand,
84    },
85    DescribeEntity,
86    ShowIndexesEntity,
87    ShowColumnsEntity,
88    ShowEntities,
89}
90
91impl LoweredSqlCommand {
92    #[must_use]
93    pub(in crate::db) const fn query(&self) -> Option<&LoweredSqlQuery> {
94        match &self.0 {
95            LoweredSqlCommandInner::Query(query) => Some(query),
96            LoweredSqlCommandInner::Explain { .. }
97            | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
98            | LoweredSqlCommandInner::DescribeEntity
99            | LoweredSqlCommandInner::ShowIndexesEntity
100            | LoweredSqlCommandInner::ShowColumnsEntity
101            | LoweredSqlCommandInner::ShowEntities => None,
102        }
103    }
104}
105
106///
107/// LoweredSqlQuery
108///
109/// Generic-free executable SQL query shape prepared before typed query binding.
110/// Select and delete lowering stay shared until the final `Query<E>` build.
111///
112#[derive(Clone, Debug)]
113pub(crate) enum LoweredSqlQuery {
114    Select(LoweredSelectShape),
115    Delete(LoweredBaseQueryShape),
116}
117
118///
119/// SqlGlobalAggregateTerminal
120///
121/// Global SQL aggregate terminals currently executable through dedicated
122/// aggregate SQL entrypoints.
123///
124
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub(crate) enum SqlGlobalAggregateTerminal {
127    CountRows,
128    CountField(String),
129    SumField(String),
130    AvgField(String),
131    MinField(String),
132    MaxField(String),
133}
134
135///
136/// LoweredSqlGlobalAggregateCommand
137///
138/// Generic-free global aggregate command shape prepared before typed query
139/// binding.
140/// This keeps aggregate SQL lowering shared across entities until the final
141/// execution boundary converts the base query shape into `Query<E>`.
142///
143#[derive(Clone, Debug)]
144pub(crate) struct LoweredSqlGlobalAggregateCommand {
145    query: LoweredBaseQueryShape,
146    terminal: SqlGlobalAggregateTerminal,
147}
148
149///
150/// SqlGlobalAggregateCommand
151///
152/// Lowered global SQL aggregate command carrying base query shape plus terminal.
153///
154
155#[derive(Debug)]
156pub(crate) struct SqlGlobalAggregateCommand<E: EntityKind> {
157    query: Query<E>,
158    terminal: SqlGlobalAggregateTerminal,
159}
160
161impl<E: EntityKind> SqlGlobalAggregateCommand<E> {
162    /// Borrow the lowered base query shape for aggregate execution.
163    #[must_use]
164    pub(crate) const fn query(&self) -> &Query<E> {
165        &self.query
166    }
167
168    /// Borrow the lowered aggregate terminal.
169    #[must_use]
170    pub(crate) const fn terminal(&self) -> &SqlGlobalAggregateTerminal {
171        &self.terminal
172    }
173}
174
175///
176/// StructuralSqlGlobalAggregateCommand
177///
178/// Generic-free lowered global aggregate command bound onto the structural
179/// query surface.
180/// This keeps global aggregate EXPLAIN on the shared query/explain path until
181/// a typed boundary is strictly required.
182///
183
184#[derive(Debug)]
185pub(crate) struct StructuralSqlGlobalAggregateCommand {
186    query: StructuralQuery,
187    terminal: SqlGlobalAggregateTerminal,
188}
189
190impl StructuralSqlGlobalAggregateCommand {
191    /// Borrow the structural query payload for aggregate explain/execution.
192    #[must_use]
193    pub(in crate::db) const fn query(&self) -> &StructuralQuery {
194        &self.query
195    }
196
197    /// Borrow the lowered aggregate terminal.
198    #[must_use]
199    pub(in crate::db) const fn terminal(&self) -> &SqlGlobalAggregateTerminal {
200        &self.terminal
201    }
202}
203
204///
205/// SqlLoweringError
206///
207/// SQL frontend lowering failures before planner validation/execution.
208///
209
210#[derive(Debug, ThisError)]
211pub(crate) enum SqlLoweringError {
212    #[error("{0}")]
213    Parse(#[from] crate::db::sql::parser::SqlParseError),
214
215    #[error("{0}")]
216    Query(#[from] QueryError),
217
218    #[error("SQL entity '{sql_entity}' does not match requested entity type '{expected_entity}'")]
219    EntityMismatch {
220        sql_entity: String,
221        expected_entity: &'static str,
222    },
223
224    #[error(
225        "unsupported SQL SELECT projection in this release; executable forms are SELECT *, direct field lists, or constrained grouped aggregate projection shapes"
226    )]
227    UnsupportedSelectProjection,
228
229    #[error("unsupported SQL SELECT DISTINCT in this release")]
230    UnsupportedSelectDistinct,
231
232    #[error("unsupported SQL GROUP BY projection shape in this release")]
233    UnsupportedSelectGroupBy,
234
235    #[error("unsupported SQL HAVING shape in this release")]
236    UnsupportedSelectHaving,
237}
238
239impl SqlLoweringError {
240    /// Construct one entity-mismatch SQL lowering error.
241    fn entity_mismatch(sql_entity: impl Into<String>, expected_entity: &'static str) -> Self {
242        Self::EntityMismatch {
243            sql_entity: sql_entity.into(),
244            expected_entity,
245        }
246    }
247
248    /// Construct one unsupported SELECT projection SQL lowering error.
249    const fn unsupported_select_projection() -> Self {
250        Self::UnsupportedSelectProjection
251    }
252
253    /// Construct one unsupported SELECT DISTINCT SQL lowering error.
254    const fn unsupported_select_distinct() -> Self {
255        Self::UnsupportedSelectDistinct
256    }
257
258    /// Construct one unsupported SELECT GROUP BY shape SQL lowering error.
259    const fn unsupported_select_group_by() -> Self {
260        Self::UnsupportedSelectGroupBy
261    }
262
263    /// Construct one unsupported SELECT HAVING shape SQL lowering error.
264    const fn unsupported_select_having() -> Self {
265        Self::UnsupportedSelectHaving
266    }
267}
268
269///
270/// PreparedSqlStatement
271///
272/// SQL statement envelope after entity-scope normalization and
273/// entity-match validation for one target entity descriptor.
274///
275/// This pre-lowering contract is entity-agnostic and reusable across
276/// dynamic SQL route branches before typed `Query<E>` binding.
277///
278
279#[derive(Clone, Debug)]
280pub(crate) struct PreparedSqlStatement {
281    statement: SqlStatement,
282}
283
284#[derive(Clone, Copy, Debug, Eq, PartialEq)]
285pub(crate) enum LoweredSqlLaneKind {
286    Query,
287    Explain,
288    Describe,
289    ShowIndexes,
290    ShowColumns,
291    ShowEntities,
292}
293
294/// Parse and lower one SQL statement into canonical query intent for `E`.
295pub(crate) fn compile_sql_command<E: EntityKind>(
296    sql: &str,
297    consistency: MissingRowPolicy,
298) -> Result<SqlCommand<E>, SqlLoweringError> {
299    let statement = parse_sql(sql)?;
300    compile_sql_command_from_statement::<E>(statement, consistency)
301}
302
303/// Lower one parsed SQL statement into canonical query intent for `E`.
304pub(crate) fn compile_sql_command_from_statement<E: EntityKind>(
305    statement: SqlStatement,
306    consistency: MissingRowPolicy,
307) -> Result<SqlCommand<E>, SqlLoweringError> {
308    let prepared = prepare_sql_statement(statement, E::MODEL.name())?;
309    compile_sql_command_from_prepared_statement::<E>(prepared, consistency)
310}
311
312/// Lower one prepared SQL statement into canonical query intent for `E`.
313pub(crate) fn compile_sql_command_from_prepared_statement<E: EntityKind>(
314    prepared: PreparedSqlStatement,
315    consistency: MissingRowPolicy,
316) -> Result<SqlCommand<E>, SqlLoweringError> {
317    let lowered = lower_sql_command_from_prepared_statement(prepared, E::MODEL.primary_key.name)?;
318
319    bind_lowered_sql_command::<E>(lowered, consistency)
320}
321
322/// Lower one prepared SQL statement into one shared generic-free command shape.
323pub(crate) fn lower_sql_command_from_prepared_statement(
324    prepared: PreparedSqlStatement,
325    primary_key_field: &str,
326) -> Result<LoweredSqlCommand, SqlLoweringError> {
327    lower_prepared_statement(prepared.statement, primary_key_field)
328}
329
330pub(crate) const fn lowered_sql_command_lane(command: &LoweredSqlCommand) -> LoweredSqlLaneKind {
331    match command.0 {
332        LoweredSqlCommandInner::Query(_) => LoweredSqlLaneKind::Query,
333        LoweredSqlCommandInner::Explain { .. }
334        | LoweredSqlCommandInner::ExplainGlobalAggregate { .. } => LoweredSqlLaneKind::Explain,
335        LoweredSqlCommandInner::DescribeEntity => LoweredSqlLaneKind::Describe,
336        LoweredSqlCommandInner::ShowIndexesEntity => LoweredSqlLaneKind::ShowIndexes,
337        LoweredSqlCommandInner::ShowColumnsEntity => LoweredSqlLaneKind::ShowColumns,
338        LoweredSqlCommandInner::ShowEntities => LoweredSqlLaneKind::ShowEntities,
339    }
340}
341
342pub(crate) fn render_lowered_sql_explain_plan_or_json(
343    lowered: &LoweredSqlCommand,
344    model: &'static crate::model::entity::EntityModel,
345    consistency: MissingRowPolicy,
346) -> Result<Option<String>, SqlLoweringError> {
347    let LoweredSqlCommandInner::Explain { mode, query } = &lowered.0 else {
348        return Ok(None);
349    };
350
351    let query = bind_lowered_sql_query_structural(model, query.clone(), consistency)?;
352    let rendered = match mode {
353        SqlExplainMode::Plan | SqlExplainMode::Json => {
354            let plan = query.build_plan()?;
355            let explain = plan.explain_with_model(model);
356
357            match mode {
358                SqlExplainMode::Plan => explain.render_text_canonical(),
359                SqlExplainMode::Json => explain.render_json_canonical(),
360                SqlExplainMode::Execution => unreachable!("execution mode handled above"),
361            }
362        }
363        SqlExplainMode::Execution => query.explain_execution_text()?,
364    };
365
366    Ok(Some(rendered))
367}
368
369/// Bind one lowered global aggregate EXPLAIN shape onto the structural query
370/// surface when the explain command carries that specialized form.
371pub(crate) fn bind_lowered_sql_explain_global_aggregate_structural(
372    lowered: &LoweredSqlCommand,
373    model: &'static crate::model::entity::EntityModel,
374    consistency: MissingRowPolicy,
375) -> Option<(SqlExplainMode, StructuralSqlGlobalAggregateCommand)> {
376    let LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command } = &lowered.0 else {
377        return None;
378    };
379
380    Some((
381        *mode,
382        bind_lowered_sql_global_aggregate_command_structural(model, command.clone(), consistency),
383    ))
384}
385
386/// Bind one shared generic-free SQL command shape to the typed query surface.
387pub(crate) fn bind_lowered_sql_command<E: EntityKind>(
388    lowered: LoweredSqlCommand,
389    consistency: MissingRowPolicy,
390) -> Result<SqlCommand<E>, SqlLoweringError> {
391    match lowered.0 {
392        LoweredSqlCommandInner::Query(query) => Ok(SqlCommand::Query(bind_lowered_sql_query::<E>(
393            query,
394            consistency,
395        )?)),
396        LoweredSqlCommandInner::Explain { mode, query } => Ok(SqlCommand::Explain {
397            mode,
398            query: bind_lowered_sql_query::<E>(query, consistency)?,
399        }),
400        LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command } => {
401            Ok(SqlCommand::ExplainGlobalAggregate {
402                mode,
403                command: bind_lowered_sql_global_aggregate_command::<E>(command, consistency),
404            })
405        }
406        LoweredSqlCommandInner::DescribeEntity => Ok(SqlCommand::DescribeEntity),
407        LoweredSqlCommandInner::ShowIndexesEntity => Ok(SqlCommand::ShowIndexesEntity),
408        LoweredSqlCommandInner::ShowColumnsEntity => Ok(SqlCommand::ShowColumnsEntity),
409        LoweredSqlCommandInner::ShowEntities => Ok(SqlCommand::ShowEntities),
410    }
411}
412
413/// Prepare one parsed SQL statement for one expected entity route.
414pub(crate) fn prepare_sql_statement(
415    statement: SqlStatement,
416    expected_entity: &'static str,
417) -> Result<PreparedSqlStatement, SqlLoweringError> {
418    let statement = prepare_statement(statement, expected_entity)?;
419
420    Ok(PreparedSqlStatement { statement })
421}
422
423/// Parse and lower one SQL statement into global aggregate execution command for `E`.
424pub(crate) fn compile_sql_global_aggregate_command<E: EntityKind>(
425    sql: &str,
426    consistency: MissingRowPolicy,
427) -> Result<SqlGlobalAggregateCommand<E>, SqlLoweringError> {
428    let statement = parse_sql(sql)?;
429    let prepared = prepare_sql_statement(statement, E::MODEL.name())?;
430    compile_sql_global_aggregate_command_from_prepared::<E>(prepared, consistency)
431}
432
433fn compile_sql_global_aggregate_command_from_prepared<E: EntityKind>(
434    prepared: PreparedSqlStatement,
435    consistency: MissingRowPolicy,
436) -> Result<SqlGlobalAggregateCommand<E>, SqlLoweringError> {
437    let SqlStatement::Select(statement) = prepared.statement else {
438        return Err(SqlLoweringError::unsupported_select_projection());
439    };
440
441    Ok(bind_lowered_sql_global_aggregate_command::<E>(
442        lower_global_aggregate_select_shape(statement)?,
443        consistency,
444    ))
445}
446
447fn prepare_statement(
448    statement: SqlStatement,
449    expected_entity: &'static str,
450) -> Result<SqlStatement, SqlLoweringError> {
451    match statement {
452        SqlStatement::Select(statement) => Ok(SqlStatement::Select(prepare_select_statement(
453            statement,
454            expected_entity,
455        )?)),
456        SqlStatement::Delete(statement) => Ok(SqlStatement::Delete(prepare_delete_statement(
457            statement,
458            expected_entity,
459        )?)),
460        SqlStatement::Explain(statement) => Ok(SqlStatement::Explain(prepare_explain_statement(
461            statement,
462            expected_entity,
463        )?)),
464        SqlStatement::Describe(statement) => {
465            ensure_entity_matches_expected(statement.entity.as_str(), expected_entity)?;
466
467            Ok(SqlStatement::Describe(statement))
468        }
469        SqlStatement::ShowIndexes(statement) => {
470            ensure_entity_matches_expected(statement.entity.as_str(), expected_entity)?;
471
472            Ok(SqlStatement::ShowIndexes(statement))
473        }
474        SqlStatement::ShowColumns(statement) => {
475            ensure_entity_matches_expected(statement.entity.as_str(), expected_entity)?;
476
477            Ok(SqlStatement::ShowColumns(statement))
478        }
479        SqlStatement::ShowEntities(statement) => Ok(SqlStatement::ShowEntities(statement)),
480    }
481}
482
483fn prepare_explain_statement(
484    statement: SqlExplainStatement,
485    expected_entity: &'static str,
486) -> Result<SqlExplainStatement, SqlLoweringError> {
487    let target = match statement.statement {
488        SqlExplainTarget::Select(select_statement) => {
489            SqlExplainTarget::Select(prepare_select_statement(select_statement, expected_entity)?)
490        }
491        SqlExplainTarget::Delete(delete_statement) => {
492            SqlExplainTarget::Delete(prepare_delete_statement(delete_statement, expected_entity)?)
493        }
494    };
495
496    Ok(SqlExplainStatement {
497        mode: statement.mode,
498        statement: target,
499    })
500}
501
502fn prepare_select_statement(
503    mut statement: SqlSelectStatement,
504    expected_entity: &'static str,
505) -> Result<SqlSelectStatement, SqlLoweringError> {
506    ensure_entity_matches_expected(statement.entity.as_str(), expected_entity)?;
507    let entity_scope = sql_entity_scope_candidates(statement.entity.as_str(), expected_entity);
508    statement.projection =
509        normalize_projection_identifiers(statement.projection, entity_scope.as_slice());
510    statement.group_by = normalize_identifier_list(statement.group_by, entity_scope.as_slice());
511    statement.predicate = statement
512        .predicate
513        .map(|predicate| adapt_predicate_identifiers_to_scope(predicate, entity_scope.as_slice()));
514    statement.order_by = normalize_order_terms(statement.order_by, entity_scope.as_slice());
515    statement.having = normalize_having_clauses(statement.having, entity_scope.as_slice());
516
517    Ok(statement)
518}
519
520fn prepare_delete_statement(
521    mut statement: SqlDeleteStatement,
522    expected_entity: &'static str,
523) -> Result<SqlDeleteStatement, SqlLoweringError> {
524    ensure_entity_matches_expected(statement.entity.as_str(), expected_entity)?;
525    let entity_scope = sql_entity_scope_candidates(statement.entity.as_str(), expected_entity);
526    statement.predicate = statement
527        .predicate
528        .map(|predicate| adapt_predicate_identifiers_to_scope(predicate, entity_scope.as_slice()));
529    statement.order_by = normalize_order_terms(statement.order_by, entity_scope.as_slice());
530
531    Ok(statement)
532}
533
534fn lower_prepared_statement(
535    statement: SqlStatement,
536    primary_key_field: &str,
537) -> Result<LoweredSqlCommand, SqlLoweringError> {
538    match statement {
539        SqlStatement::Select(statement) => Ok(LoweredSqlCommand(LoweredSqlCommandInner::Query(
540            LoweredSqlQuery::Select(lower_select_shape(statement, primary_key_field)?),
541        ))),
542        SqlStatement::Delete(statement) => Ok(LoweredSqlCommand(LoweredSqlCommandInner::Query(
543            LoweredSqlQuery::Delete(lower_delete_shape(statement)),
544        ))),
545        SqlStatement::Explain(statement) => lower_explain_prepared(statement, primary_key_field),
546        SqlStatement::Describe(_) => Ok(LoweredSqlCommand(LoweredSqlCommandInner::DescribeEntity)),
547        SqlStatement::ShowIndexes(_) => {
548            Ok(LoweredSqlCommand(LoweredSqlCommandInner::ShowIndexesEntity))
549        }
550        SqlStatement::ShowColumns(_) => {
551            Ok(LoweredSqlCommand(LoweredSqlCommandInner::ShowColumnsEntity))
552        }
553        SqlStatement::ShowEntities(_) => {
554            Ok(LoweredSqlCommand(LoweredSqlCommandInner::ShowEntities))
555        }
556    }
557}
558
559fn lower_explain_prepared(
560    statement: SqlExplainStatement,
561    primary_key_field: &str,
562) -> Result<LoweredSqlCommand, SqlLoweringError> {
563    let mode = statement.mode;
564
565    match statement.statement {
566        SqlExplainTarget::Select(select_statement) => {
567            lower_explain_select_prepared(select_statement, mode, primary_key_field)
568        }
569        SqlExplainTarget::Delete(delete_statement) => {
570            Ok(LoweredSqlCommand(LoweredSqlCommandInner::Explain {
571                mode,
572                query: LoweredSqlQuery::Delete(lower_delete_shape(delete_statement)),
573            }))
574        }
575    }
576}
577
578fn lower_explain_select_prepared(
579    statement: SqlSelectStatement,
580    mode: SqlExplainMode,
581    primary_key_field: &str,
582) -> Result<LoweredSqlCommand, SqlLoweringError> {
583    match lower_select_shape(statement.clone(), primary_key_field) {
584        Ok(query) => Ok(LoweredSqlCommand(LoweredSqlCommandInner::Explain {
585            mode,
586            query: LoweredSqlQuery::Select(query),
587        })),
588        Err(SqlLoweringError::UnsupportedSelectProjection) => {
589            let command = lower_global_aggregate_select_shape(statement)?;
590
591            Ok(LoweredSqlCommand(
592                LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command },
593            ))
594        }
595        Err(err) => Err(err),
596    }
597}
598
599fn lower_global_aggregate_select_shape(
600    statement: SqlSelectStatement,
601) -> Result<LoweredSqlGlobalAggregateCommand, SqlLoweringError> {
602    let SqlSelectStatement {
603        projection,
604        predicate,
605        distinct,
606        group_by,
607        having,
608        order_by,
609        limit,
610        offset,
611        entity: _,
612    } = statement;
613
614    if distinct {
615        return Err(SqlLoweringError::unsupported_select_distinct());
616    }
617    if !group_by.is_empty() {
618        return Err(SqlLoweringError::unsupported_select_group_by());
619    }
620    if !having.is_empty() {
621        return Err(SqlLoweringError::unsupported_select_having());
622    }
623
624    let terminal = lower_global_aggregate_terminal(projection)?;
625
626    Ok(LoweredSqlGlobalAggregateCommand {
627        query: LoweredBaseQueryShape {
628            predicate,
629            order_by,
630            limit,
631            offset,
632        },
633        terminal,
634    })
635}
636
637///
638/// ResolvedHavingClause
639///
640/// Pre-resolved HAVING clause shape after SQL projection aggregate index
641/// resolution. This keeps SQL shape analysis entity-agnostic before typed
642/// query binding.
643///
644#[derive(Clone, Debug)]
645enum ResolvedHavingClause {
646    GroupField {
647        field: String,
648        op: crate::db::predicate::CompareOp,
649        value: crate::value::Value,
650    },
651    Aggregate {
652        aggregate_index: usize,
653        op: crate::db::predicate::CompareOp,
654        value: crate::value::Value,
655    },
656}
657
658///
659/// LoweredSelectShape
660///
661/// Entity-agnostic lowered SQL SELECT shape prepared for typed `Query<E>`
662/// binding.
663///
664#[derive(Clone, Debug)]
665pub(crate) struct LoweredSelectShape {
666    scalar_projection_fields: Option<Vec<String>>,
667    grouped_projection_aggregates: Vec<SqlAggregateCall>,
668    group_by_fields: Vec<String>,
669    distinct: bool,
670    having: Vec<ResolvedHavingClause>,
671    predicate: Option<Predicate>,
672    order_by: Vec<crate::db::sql::parser::SqlOrderTerm>,
673    limit: Option<u32>,
674    offset: Option<u32>,
675}
676
677///
678/// LoweredBaseQueryShape
679///
680/// Generic-free filter/order/window query modifiers shared by delete and
681/// global-aggregate SQL lowering.
682/// This keeps common SQL query-shape lowering shared before typed query
683/// binding.
684///
685#[derive(Clone, Debug)]
686pub(crate) struct LoweredBaseQueryShape {
687    predicate: Option<Predicate>,
688    order_by: Vec<SqlOrderTerm>,
689    limit: Option<u32>,
690    offset: Option<u32>,
691}
692
693fn lower_select_shape(
694    statement: SqlSelectStatement,
695    primary_key_field: &str,
696) -> Result<LoweredSelectShape, SqlLoweringError> {
697    let SqlSelectStatement {
698        projection,
699        predicate,
700        distinct,
701        group_by,
702        having,
703        order_by,
704        limit,
705        offset,
706        entity: _,
707    } = statement;
708    let projection_for_having = projection.clone();
709
710    // Phase 1: resolve scalar/grouped projection shape.
711    let (scalar_projection_fields, grouped_projection_aggregates) = if group_by.is_empty() {
712        let scalar_projection_fields =
713            lower_scalar_projection_fields(projection, distinct, primary_key_field)?;
714        (scalar_projection_fields, Vec::new())
715    } else {
716        if distinct {
717            return Err(SqlLoweringError::unsupported_select_distinct());
718        }
719        let grouped_projection_aggregates =
720            grouped_projection_aggregate_calls(&projection, group_by.as_slice())?;
721        (None, grouped_projection_aggregates)
722    };
723
724    // Phase 2: resolve HAVING symbols against grouped projection authority.
725    let having = lower_having_clauses(
726        having,
727        &projection_for_having,
728        group_by.as_slice(),
729        grouped_projection_aggregates.as_slice(),
730    )?;
731
732    Ok(LoweredSelectShape {
733        scalar_projection_fields,
734        grouped_projection_aggregates,
735        group_by_fields: group_by,
736        distinct,
737        having,
738        predicate,
739        order_by,
740        limit,
741        offset,
742    })
743}
744
745fn lower_scalar_projection_fields(
746    projection: SqlProjection,
747    distinct: bool,
748    primary_key_field: &str,
749) -> Result<Option<Vec<String>>, SqlLoweringError> {
750    let SqlProjection::Items(items) = projection else {
751        if distinct {
752            return Ok(None);
753        }
754
755        return Ok(None);
756    };
757
758    let has_aggregate = items
759        .iter()
760        .any(|item| matches!(item, SqlSelectItem::Aggregate(_)));
761    if has_aggregate {
762        return Err(SqlLoweringError::unsupported_select_projection());
763    }
764
765    let fields = items
766        .into_iter()
767        .map(|item| match item {
768            SqlSelectItem::Field(field) => Ok(field),
769            SqlSelectItem::Aggregate(_) => Err(SqlLoweringError::unsupported_select_projection()),
770        })
771        .collect::<Result<Vec<_>, _>>()?;
772
773    validate_scalar_distinct_projection(distinct, fields.as_slice(), primary_key_field)?;
774
775    Ok(Some(fields))
776}
777
778fn validate_scalar_distinct_projection(
779    distinct: bool,
780    projection_fields: &[String],
781    primary_key_field: &str,
782) -> Result<(), SqlLoweringError> {
783    if !distinct {
784        return Ok(());
785    }
786
787    if projection_fields.is_empty() {
788        return Ok(());
789    }
790
791    let has_primary_key_field = projection_fields
792        .iter()
793        .any(|field| field == primary_key_field);
794    if !has_primary_key_field {
795        return Err(SqlLoweringError::unsupported_select_distinct());
796    }
797
798    Ok(())
799}
800
801fn lower_having_clauses(
802    having_clauses: Vec<SqlHavingClause>,
803    projection: &SqlProjection,
804    group_by_fields: &[String],
805    grouped_projection_aggregates: &[SqlAggregateCall],
806) -> Result<Vec<ResolvedHavingClause>, SqlLoweringError> {
807    if having_clauses.is_empty() {
808        return Ok(Vec::new());
809    }
810    if group_by_fields.is_empty() {
811        return Err(SqlLoweringError::unsupported_select_having());
812    }
813
814    let projection_aggregates = grouped_projection_aggregate_calls(projection, group_by_fields)
815        .map_err(|_| SqlLoweringError::unsupported_select_having())?;
816    if projection_aggregates.as_slice() != grouped_projection_aggregates {
817        return Err(SqlLoweringError::unsupported_select_having());
818    }
819
820    let mut lowered = Vec::with_capacity(having_clauses.len());
821    for clause in having_clauses {
822        match clause.symbol {
823            SqlHavingSymbol::Field(field) => lowered.push(ResolvedHavingClause::GroupField {
824                field,
825                op: clause.op,
826                value: clause.value,
827            }),
828            SqlHavingSymbol::Aggregate(aggregate) => {
829                let aggregate_index =
830                    resolve_having_aggregate_index(&aggregate, grouped_projection_aggregates)?;
831                lowered.push(ResolvedHavingClause::Aggregate {
832                    aggregate_index,
833                    op: clause.op,
834                    value: clause.value,
835                });
836            }
837        }
838    }
839
840    Ok(lowered)
841}
842
843pub(in crate::db) fn apply_lowered_select_shape(
844    mut query: StructuralQuery,
845    lowered: LoweredSelectShape,
846) -> Result<StructuralQuery, SqlLoweringError> {
847    let LoweredSelectShape {
848        scalar_projection_fields,
849        grouped_projection_aggregates,
850        group_by_fields,
851        distinct,
852        having,
853        predicate,
854        order_by,
855        limit,
856        offset,
857    } = lowered;
858
859    // Phase 1: apply grouped declaration semantics.
860    for field in group_by_fields {
861        query = query.group_by(field)?;
862    }
863
864    // Phase 2: apply scalar DISTINCT and projection contracts.
865    if distinct {
866        query = query.distinct();
867    }
868    if let Some(fields) = scalar_projection_fields {
869        query = query.select_fields(fields);
870    }
871    for aggregate in grouped_projection_aggregates {
872        query = query.aggregate(lower_aggregate_call(aggregate)?);
873    }
874
875    // Phase 3: bind resolved HAVING clauses against grouped terminals.
876    for clause in having {
877        match clause {
878            ResolvedHavingClause::GroupField { field, op, value } => {
879                query = query.having_group(field, op, value)?;
880            }
881            ResolvedHavingClause::Aggregate {
882                aggregate_index,
883                op,
884                value,
885            } => {
886                query = query.having_aggregate(aggregate_index, op, value)?;
887            }
888        }
889    }
890
891    // Phase 4: attach the shared filter/order/page tail through the base-query lane.
892    Ok(apply_lowered_base_query_shape(
893        query,
894        LoweredBaseQueryShape {
895            predicate,
896            order_by,
897            limit,
898            offset,
899        },
900    ))
901}
902
903fn apply_lowered_base_query_shape(
904    mut query: StructuralQuery,
905    lowered: LoweredBaseQueryShape,
906) -> StructuralQuery {
907    if let Some(predicate) = lowered.predicate {
908        query = query.filter(predicate);
909    }
910    query = apply_order_terms_structural(query, lowered.order_by);
911    if let Some(limit) = lowered.limit {
912        query = query.limit(limit);
913    }
914    if let Some(offset) = lowered.offset {
915        query = query.offset(offset);
916    }
917
918    query
919}
920
921pub(in crate::db) fn bind_lowered_sql_query_structural(
922    model: &'static crate::model::entity::EntityModel,
923    lowered: LoweredSqlQuery,
924    consistency: MissingRowPolicy,
925) -> Result<StructuralQuery, SqlLoweringError> {
926    match lowered {
927        LoweredSqlQuery::Select(select) => {
928            apply_lowered_select_shape(StructuralQuery::new(model, consistency), select)
929        }
930        LoweredSqlQuery::Delete(delete) => Ok(bind_lowered_sql_delete_query_structural(
931            model,
932            delete,
933            consistency,
934        )),
935    }
936}
937
938pub(in crate::db) fn bind_lowered_sql_delete_query_structural(
939    model: &'static crate::model::entity::EntityModel,
940    delete: LoweredBaseQueryShape,
941    consistency: MissingRowPolicy,
942) -> StructuralQuery {
943    apply_lowered_base_query_shape(StructuralQuery::new(model, consistency).delete(), delete)
944}
945
946pub(in crate::db) fn bind_lowered_sql_query<E: EntityKind>(
947    lowered: LoweredSqlQuery,
948    consistency: MissingRowPolicy,
949) -> Result<Query<E>, SqlLoweringError> {
950    let structural = bind_lowered_sql_query_structural(E::MODEL, lowered, consistency)?;
951
952    Ok(Query::from_inner(structural))
953}
954
955fn bind_lowered_sql_global_aggregate_command<E: EntityKind>(
956    lowered: LoweredSqlGlobalAggregateCommand,
957    consistency: MissingRowPolicy,
958) -> SqlGlobalAggregateCommand<E> {
959    SqlGlobalAggregateCommand {
960        query: Query::from_inner(apply_lowered_base_query_shape(
961            StructuralQuery::new(E::MODEL, consistency),
962            lowered.query,
963        )),
964        terminal: lowered.terminal,
965    }
966}
967
968fn bind_lowered_sql_global_aggregate_command_structural(
969    model: &'static crate::model::entity::EntityModel,
970    lowered: LoweredSqlGlobalAggregateCommand,
971    consistency: MissingRowPolicy,
972) -> StructuralSqlGlobalAggregateCommand {
973    StructuralSqlGlobalAggregateCommand {
974        query: apply_lowered_base_query_shape(
975            StructuralQuery::new(model, consistency),
976            lowered.query,
977        ),
978        terminal: lowered.terminal,
979    }
980}
981
982fn lower_global_aggregate_terminal(
983    projection: SqlProjection,
984) -> Result<SqlGlobalAggregateTerminal, SqlLoweringError> {
985    let SqlProjection::Items(items) = projection else {
986        return Err(SqlLoweringError::unsupported_select_projection());
987    };
988    if items.len() != 1 {
989        return Err(SqlLoweringError::unsupported_select_projection());
990    }
991
992    let Some(SqlSelectItem::Aggregate(aggregate)) = items.into_iter().next() else {
993        return Err(SqlLoweringError::unsupported_select_projection());
994    };
995
996    match (aggregate.kind, aggregate.field) {
997        (SqlAggregateKind::Count, None) => Ok(SqlGlobalAggregateTerminal::CountRows),
998        (SqlAggregateKind::Count, Some(field)) => Ok(SqlGlobalAggregateTerminal::CountField(field)),
999        (SqlAggregateKind::Sum, Some(field)) => Ok(SqlGlobalAggregateTerminal::SumField(field)),
1000        (SqlAggregateKind::Avg, Some(field)) => Ok(SqlGlobalAggregateTerminal::AvgField(field)),
1001        (SqlAggregateKind::Min, Some(field)) => Ok(SqlGlobalAggregateTerminal::MinField(field)),
1002        (SqlAggregateKind::Max, Some(field)) => Ok(SqlGlobalAggregateTerminal::MaxField(field)),
1003        _ => Err(SqlLoweringError::unsupported_select_projection()),
1004    }
1005}
1006
1007fn grouped_projection_aggregate_calls(
1008    projection: &SqlProjection,
1009    group_by_fields: &[String],
1010) -> Result<Vec<SqlAggregateCall>, SqlLoweringError> {
1011    if group_by_fields.is_empty() {
1012        return Err(SqlLoweringError::unsupported_select_group_by());
1013    }
1014
1015    let SqlProjection::Items(items) = projection else {
1016        return Err(SqlLoweringError::unsupported_select_group_by());
1017    };
1018
1019    let mut projected_group_fields = Vec::<String>::new();
1020    let mut aggregate_calls = Vec::<SqlAggregateCall>::new();
1021    let mut seen_aggregate = false;
1022
1023    for item in items {
1024        match item {
1025            SqlSelectItem::Field(field) => {
1026                // Keep grouped projection deterministic and mappable to grouped
1027                // response contracts: group keys must be declared first.
1028                if seen_aggregate {
1029                    return Err(SqlLoweringError::unsupported_select_group_by());
1030                }
1031                projected_group_fields.push(field.clone());
1032            }
1033            SqlSelectItem::Aggregate(aggregate) => {
1034                seen_aggregate = true;
1035                aggregate_calls.push(aggregate.clone());
1036            }
1037        }
1038    }
1039
1040    if aggregate_calls.is_empty() || projected_group_fields.as_slice() != group_by_fields {
1041        return Err(SqlLoweringError::unsupported_select_group_by());
1042    }
1043
1044    Ok(aggregate_calls)
1045}
1046
1047fn lower_aggregate_call(
1048    call: SqlAggregateCall,
1049) -> Result<crate::db::query::builder::AggregateExpr, SqlLoweringError> {
1050    match (call.kind, call.field) {
1051        (SqlAggregateKind::Count, None) => Ok(count()),
1052        (SqlAggregateKind::Count, Some(field)) => Ok(count_by(field)),
1053        (SqlAggregateKind::Sum, Some(field)) => Ok(sum(field)),
1054        (SqlAggregateKind::Avg, Some(field)) => Ok(avg(field)),
1055        (SqlAggregateKind::Min, Some(field)) => Ok(min_by(field)),
1056        (SqlAggregateKind::Max, Some(field)) => Ok(max_by(field)),
1057        _ => Err(SqlLoweringError::unsupported_select_projection()),
1058    }
1059}
1060
1061fn resolve_having_aggregate_index(
1062    target: &SqlAggregateCall,
1063    grouped_projection_aggregates: &[SqlAggregateCall],
1064) -> Result<usize, SqlLoweringError> {
1065    let mut matched = grouped_projection_aggregates
1066        .iter()
1067        .enumerate()
1068        .filter_map(|(index, aggregate)| (aggregate == target).then_some(index));
1069    let Some(index) = matched.next() else {
1070        return Err(SqlLoweringError::unsupported_select_having());
1071    };
1072    if matched.next().is_some() {
1073        return Err(SqlLoweringError::unsupported_select_having());
1074    }
1075
1076    Ok(index)
1077}
1078
1079fn lower_delete_shape(statement: SqlDeleteStatement) -> LoweredBaseQueryShape {
1080    let SqlDeleteStatement {
1081        predicate,
1082        order_by,
1083        limit,
1084        entity: _,
1085    } = statement;
1086
1087    LoweredBaseQueryShape {
1088        predicate,
1089        order_by,
1090        limit,
1091        offset: None,
1092    }
1093}
1094
1095fn apply_order_terms_structural(
1096    mut query: StructuralQuery,
1097    order_by: Vec<crate::db::sql::parser::SqlOrderTerm>,
1098) -> StructuralQuery {
1099    for term in order_by {
1100        query = match term.direction {
1101            SqlOrderDirection::Asc => query.order_by(term.field),
1102            SqlOrderDirection::Desc => query.order_by_desc(term.field),
1103        };
1104    }
1105
1106    query
1107}
1108
1109fn normalize_having_clauses(
1110    clauses: Vec<SqlHavingClause>,
1111    entity_scope: &[String],
1112) -> Vec<SqlHavingClause> {
1113    clauses
1114        .into_iter()
1115        .map(|clause| SqlHavingClause {
1116            symbol: normalize_having_symbol(clause.symbol, entity_scope),
1117            op: clause.op,
1118            value: clause.value,
1119        })
1120        .collect()
1121}
1122
1123fn normalize_having_symbol(symbol: SqlHavingSymbol, entity_scope: &[String]) -> SqlHavingSymbol {
1124    match symbol {
1125        SqlHavingSymbol::Field(field) => {
1126            SqlHavingSymbol::Field(normalize_identifier_to_scope(field, entity_scope))
1127        }
1128        SqlHavingSymbol::Aggregate(aggregate) => SqlHavingSymbol::Aggregate(
1129            normalize_aggregate_call_identifiers(aggregate, entity_scope),
1130        ),
1131    }
1132}
1133
1134fn normalize_aggregate_call_identifiers(
1135    aggregate: SqlAggregateCall,
1136    entity_scope: &[String],
1137) -> SqlAggregateCall {
1138    SqlAggregateCall {
1139        kind: aggregate.kind,
1140        field: aggregate
1141            .field
1142            .map(|field| normalize_identifier_to_scope(field, entity_scope)),
1143    }
1144}
1145
1146// Build one identifier scope used for reducing SQL-qualified field references
1147// (`entity.field`, `schema.entity.field`) into canonical planner field names.
1148fn sql_entity_scope_candidates(sql_entity: &str, expected_entity: &'static str) -> Vec<String> {
1149    let mut out = Vec::new();
1150    out.push(sql_entity.to_string());
1151    out.push(expected_entity.to_string());
1152
1153    if let Some(last) = identifier_last_segment(sql_entity) {
1154        out.push(last.to_string());
1155    }
1156    if let Some(last) = identifier_last_segment(expected_entity) {
1157        out.push(last.to_string());
1158    }
1159
1160    out
1161}
1162
1163fn normalize_projection_identifiers(
1164    projection: SqlProjection,
1165    entity_scope: &[String],
1166) -> SqlProjection {
1167    match projection {
1168        SqlProjection::All => SqlProjection::All,
1169        SqlProjection::Items(items) => SqlProjection::Items(
1170            items
1171                .into_iter()
1172                .map(|item| match item {
1173                    SqlSelectItem::Field(field) => {
1174                        SqlSelectItem::Field(normalize_identifier(field, entity_scope))
1175                    }
1176                    SqlSelectItem::Aggregate(aggregate) => {
1177                        SqlSelectItem::Aggregate(SqlAggregateCall {
1178                            kind: aggregate.kind,
1179                            field: aggregate
1180                                .field
1181                                .map(|field| normalize_identifier(field, entity_scope)),
1182                        })
1183                    }
1184                })
1185                .collect(),
1186        ),
1187    }
1188}
1189
1190fn normalize_order_terms(
1191    terms: Vec<crate::db::sql::parser::SqlOrderTerm>,
1192    entity_scope: &[String],
1193) -> Vec<crate::db::sql::parser::SqlOrderTerm> {
1194    terms
1195        .into_iter()
1196        .map(|term| crate::db::sql::parser::SqlOrderTerm {
1197            field: normalize_identifier(term.field, entity_scope),
1198            direction: term.direction,
1199        })
1200        .collect()
1201}
1202
1203fn normalize_identifier_list(fields: Vec<String>, entity_scope: &[String]) -> Vec<String> {
1204    fields
1205        .into_iter()
1206        .map(|field| normalize_identifier(field, entity_scope))
1207        .collect()
1208}
1209
1210// SQL lowering only adapts identifier qualification (`entity.field` -> `field`)
1211// and delegates predicate-tree traversal ownership to `db::predicate`.
1212fn adapt_predicate_identifiers_to_scope(
1213    predicate: Predicate,
1214    entity_scope: &[String],
1215) -> Predicate {
1216    rewrite_field_identifiers(predicate, |field| normalize_identifier(field, entity_scope))
1217}
1218
1219fn normalize_identifier(identifier: String, entity_scope: &[String]) -> String {
1220    normalize_identifier_to_scope(identifier, entity_scope)
1221}
1222
1223fn ensure_entity_matches_expected(
1224    sql_entity: &str,
1225    expected_entity: &'static str,
1226) -> Result<(), SqlLoweringError> {
1227    if identifiers_tail_match(sql_entity, expected_entity) {
1228        return Ok(());
1229    }
1230
1231    Err(SqlLoweringError::entity_mismatch(
1232        sql_entity,
1233        expected_entity,
1234    ))
1235}