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