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.
323#[inline(never)]
324pub(crate) fn lower_sql_command_from_prepared_statement(
325    prepared: PreparedSqlStatement,
326    primary_key_field: &str,
327) -> Result<LoweredSqlCommand, SqlLoweringError> {
328    lower_prepared_statement(prepared.statement, primary_key_field)
329}
330
331pub(crate) const fn lowered_sql_command_lane(command: &LoweredSqlCommand) -> LoweredSqlLaneKind {
332    match command.0 {
333        LoweredSqlCommandInner::Query(_) => LoweredSqlLaneKind::Query,
334        LoweredSqlCommandInner::Explain { .. }
335        | LoweredSqlCommandInner::ExplainGlobalAggregate { .. } => LoweredSqlLaneKind::Explain,
336        LoweredSqlCommandInner::DescribeEntity => LoweredSqlLaneKind::Describe,
337        LoweredSqlCommandInner::ShowIndexesEntity => LoweredSqlLaneKind::ShowIndexes,
338        LoweredSqlCommandInner::ShowColumnsEntity => LoweredSqlLaneKind::ShowColumns,
339        LoweredSqlCommandInner::ShowEntities => LoweredSqlLaneKind::ShowEntities,
340    }
341}
342
343/// Render one lowered EXPLAIN command through the shared structural SQL path.
344#[inline(never)]
345pub(crate) fn render_lowered_sql_explain_plan_or_json(
346    lowered: &LoweredSqlCommand,
347    model: &'static crate::model::entity::EntityModel,
348    consistency: MissingRowPolicy,
349) -> Result<Option<String>, SqlLoweringError> {
350    let LoweredSqlCommandInner::Explain { mode, query } = &lowered.0 else {
351        return Ok(None);
352    };
353
354    let query = bind_lowered_sql_query_structural(model, query.clone(), consistency)?;
355    let rendered = match mode {
356        SqlExplainMode::Plan | SqlExplainMode::Json => {
357            let plan = query.build_plan()?;
358            let explain = plan.explain_with_model(model);
359
360            match mode {
361                SqlExplainMode::Plan => explain.render_text_canonical(),
362                SqlExplainMode::Json => explain.render_json_canonical(),
363                SqlExplainMode::Execution => unreachable!("execution mode handled above"),
364            }
365        }
366        SqlExplainMode::Execution => query.explain_execution_text()?,
367    };
368
369    Ok(Some(rendered))
370}
371
372/// Bind one lowered global aggregate EXPLAIN shape onto the structural query
373/// surface when the explain command carries that specialized form.
374pub(crate) fn bind_lowered_sql_explain_global_aggregate_structural(
375    lowered: &LoweredSqlCommand,
376    model: &'static crate::model::entity::EntityModel,
377    consistency: MissingRowPolicy,
378) -> Option<(SqlExplainMode, StructuralSqlGlobalAggregateCommand)> {
379    let LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command } = &lowered.0 else {
380        return None;
381    };
382
383    Some((
384        *mode,
385        bind_lowered_sql_global_aggregate_command_structural(model, command.clone(), consistency),
386    ))
387}
388
389/// Bind one shared generic-free SQL command shape to the typed query surface.
390#[cfg(test)]
391pub(crate) fn bind_lowered_sql_command<E: EntityKind>(
392    lowered: LoweredSqlCommand,
393    consistency: MissingRowPolicy,
394) -> Result<SqlCommand<E>, SqlLoweringError> {
395    match lowered.0 {
396        LoweredSqlCommandInner::Query(query) => Ok(SqlCommand::Query(bind_lowered_sql_query::<E>(
397            query,
398            consistency,
399        )?)),
400        LoweredSqlCommandInner::Explain { mode, query } => Ok(SqlCommand::Explain {
401            mode,
402            query: bind_lowered_sql_query::<E>(query, consistency)?,
403        }),
404        LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command } => {
405            Ok(SqlCommand::ExplainGlobalAggregate {
406                mode,
407                command: bind_lowered_sql_global_aggregate_command::<E>(command, consistency),
408            })
409        }
410        LoweredSqlCommandInner::DescribeEntity => Ok(SqlCommand::DescribeEntity),
411        LoweredSqlCommandInner::ShowIndexesEntity => Ok(SqlCommand::ShowIndexesEntity),
412        LoweredSqlCommandInner::ShowColumnsEntity => Ok(SqlCommand::ShowColumnsEntity),
413        LoweredSqlCommandInner::ShowEntities => Ok(SqlCommand::ShowEntities),
414    }
415}
416
417/// Prepare one parsed SQL statement for one expected entity route.
418#[inline(never)]
419pub(crate) fn prepare_sql_statement(
420    statement: SqlStatement,
421    expected_entity: &'static str,
422) -> Result<PreparedSqlStatement, SqlLoweringError> {
423    let statement = prepare_statement(statement, expected_entity)?;
424
425    Ok(PreparedSqlStatement { statement })
426}
427
428/// Parse and lower one SQL statement into global aggregate execution command for `E`.
429pub(crate) fn compile_sql_global_aggregate_command<E: EntityKind>(
430    sql: &str,
431    consistency: MissingRowPolicy,
432) -> Result<SqlGlobalAggregateCommand<E>, SqlLoweringError> {
433    let statement = parse_sql(sql)?;
434    let prepared = prepare_sql_statement(statement, E::MODEL.name())?;
435    compile_sql_global_aggregate_command_from_prepared::<E>(prepared, consistency)
436}
437
438fn compile_sql_global_aggregate_command_from_prepared<E: EntityKind>(
439    prepared: PreparedSqlStatement,
440    consistency: MissingRowPolicy,
441) -> Result<SqlGlobalAggregateCommand<E>, SqlLoweringError> {
442    let SqlStatement::Select(statement) = prepared.statement else {
443        return Err(SqlLoweringError::unsupported_select_projection());
444    };
445
446    Ok(bind_lowered_sql_global_aggregate_command::<E>(
447        lower_global_aggregate_select_shape(statement)?,
448        consistency,
449    ))
450}
451
452#[inline(never)]
453fn prepare_statement(
454    statement: SqlStatement,
455    expected_entity: &'static str,
456) -> Result<SqlStatement, SqlLoweringError> {
457    match statement {
458        SqlStatement::Select(statement) => Ok(SqlStatement::Select(prepare_select_statement(
459            statement,
460            expected_entity,
461        )?)),
462        SqlStatement::Delete(statement) => Ok(SqlStatement::Delete(prepare_delete_statement(
463            statement,
464            expected_entity,
465        )?)),
466        SqlStatement::Explain(statement) => Ok(SqlStatement::Explain(prepare_explain_statement(
467            statement,
468            expected_entity,
469        )?)),
470        SqlStatement::Describe(statement) => {
471            ensure_entity_matches_expected(statement.entity.as_str(), expected_entity)?;
472
473            Ok(SqlStatement::Describe(statement))
474        }
475        SqlStatement::ShowIndexes(statement) => {
476            ensure_entity_matches_expected(statement.entity.as_str(), expected_entity)?;
477
478            Ok(SqlStatement::ShowIndexes(statement))
479        }
480        SqlStatement::ShowColumns(statement) => {
481            ensure_entity_matches_expected(statement.entity.as_str(), expected_entity)?;
482
483            Ok(SqlStatement::ShowColumns(statement))
484        }
485        SqlStatement::ShowEntities(statement) => Ok(SqlStatement::ShowEntities(statement)),
486    }
487}
488
489fn prepare_explain_statement(
490    statement: SqlExplainStatement,
491    expected_entity: &'static str,
492) -> Result<SqlExplainStatement, SqlLoweringError> {
493    let target = match statement.statement {
494        SqlExplainTarget::Select(select_statement) => {
495            SqlExplainTarget::Select(prepare_select_statement(select_statement, expected_entity)?)
496        }
497        SqlExplainTarget::Delete(delete_statement) => {
498            SqlExplainTarget::Delete(prepare_delete_statement(delete_statement, expected_entity)?)
499        }
500    };
501
502    Ok(SqlExplainStatement {
503        mode: statement.mode,
504        statement: target,
505    })
506}
507
508fn prepare_select_statement(
509    mut statement: SqlSelectStatement,
510    expected_entity: &'static str,
511) -> Result<SqlSelectStatement, SqlLoweringError> {
512    ensure_entity_matches_expected(statement.entity.as_str(), expected_entity)?;
513    let entity_scope = sql_entity_scope_candidates(statement.entity.as_str(), expected_entity);
514    statement.projection =
515        normalize_projection_identifiers(statement.projection, entity_scope.as_slice());
516    statement.group_by = normalize_identifier_list(statement.group_by, entity_scope.as_slice());
517    statement.predicate = statement
518        .predicate
519        .map(|predicate| adapt_predicate_identifiers_to_scope(predicate, entity_scope.as_slice()));
520    statement.order_by = normalize_order_terms(statement.order_by, entity_scope.as_slice());
521    statement.having = normalize_having_clauses(statement.having, entity_scope.as_slice());
522
523    Ok(statement)
524}
525
526fn prepare_delete_statement(
527    mut statement: SqlDeleteStatement,
528    expected_entity: &'static str,
529) -> Result<SqlDeleteStatement, SqlLoweringError> {
530    ensure_entity_matches_expected(statement.entity.as_str(), expected_entity)?;
531    let entity_scope = sql_entity_scope_candidates(statement.entity.as_str(), expected_entity);
532    statement.predicate = statement
533        .predicate
534        .map(|predicate| adapt_predicate_identifiers_to_scope(predicate, entity_scope.as_slice()));
535    statement.order_by = normalize_order_terms(statement.order_by, entity_scope.as_slice());
536
537    Ok(statement)
538}
539
540#[inline(never)]
541fn lower_prepared_statement(
542    statement: SqlStatement,
543    primary_key_field: &str,
544) -> Result<LoweredSqlCommand, SqlLoweringError> {
545    match statement {
546        SqlStatement::Select(statement) => Ok(LoweredSqlCommand(LoweredSqlCommandInner::Query(
547            LoweredSqlQuery::Select(lower_select_shape(statement, primary_key_field)?),
548        ))),
549        SqlStatement::Delete(statement) => Ok(LoweredSqlCommand(LoweredSqlCommandInner::Query(
550            LoweredSqlQuery::Delete(lower_delete_shape(statement)),
551        ))),
552        SqlStatement::Explain(statement) => lower_explain_prepared(statement, primary_key_field),
553        SqlStatement::Describe(_) => Ok(LoweredSqlCommand(LoweredSqlCommandInner::DescribeEntity)),
554        SqlStatement::ShowIndexes(_) => {
555            Ok(LoweredSqlCommand(LoweredSqlCommandInner::ShowIndexesEntity))
556        }
557        SqlStatement::ShowColumns(_) => {
558            Ok(LoweredSqlCommand(LoweredSqlCommandInner::ShowColumnsEntity))
559        }
560        SqlStatement::ShowEntities(_) => {
561            Ok(LoweredSqlCommand(LoweredSqlCommandInner::ShowEntities))
562        }
563    }
564}
565
566fn lower_explain_prepared(
567    statement: SqlExplainStatement,
568    primary_key_field: &str,
569) -> Result<LoweredSqlCommand, SqlLoweringError> {
570    let mode = statement.mode;
571
572    match statement.statement {
573        SqlExplainTarget::Select(select_statement) => {
574            lower_explain_select_prepared(select_statement, mode, primary_key_field)
575        }
576        SqlExplainTarget::Delete(delete_statement) => {
577            Ok(LoweredSqlCommand(LoweredSqlCommandInner::Explain {
578                mode,
579                query: LoweredSqlQuery::Delete(lower_delete_shape(delete_statement)),
580            }))
581        }
582    }
583}
584
585fn lower_explain_select_prepared(
586    statement: SqlSelectStatement,
587    mode: SqlExplainMode,
588    primary_key_field: &str,
589) -> Result<LoweredSqlCommand, SqlLoweringError> {
590    match lower_select_shape(statement.clone(), primary_key_field) {
591        Ok(query) => Ok(LoweredSqlCommand(LoweredSqlCommandInner::Explain {
592            mode,
593            query: LoweredSqlQuery::Select(query),
594        })),
595        Err(SqlLoweringError::UnsupportedSelectProjection) => {
596            let command = lower_global_aggregate_select_shape(statement)?;
597
598            Ok(LoweredSqlCommand(
599                LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command },
600            ))
601        }
602        Err(err) => Err(err),
603    }
604}
605
606fn lower_global_aggregate_select_shape(
607    statement: SqlSelectStatement,
608) -> Result<LoweredSqlGlobalAggregateCommand, SqlLoweringError> {
609    let SqlSelectStatement {
610        projection,
611        predicate,
612        distinct,
613        group_by,
614        having,
615        order_by,
616        limit,
617        offset,
618        entity: _,
619    } = statement;
620
621    if distinct {
622        return Err(SqlLoweringError::unsupported_select_distinct());
623    }
624    if !group_by.is_empty() {
625        return Err(SqlLoweringError::unsupported_select_group_by());
626    }
627    if !having.is_empty() {
628        return Err(SqlLoweringError::unsupported_select_having());
629    }
630
631    let terminal = lower_global_aggregate_terminal(projection)?;
632
633    Ok(LoweredSqlGlobalAggregateCommand {
634        query: LoweredBaseQueryShape {
635            predicate,
636            order_by,
637            limit,
638            offset,
639        },
640        terminal,
641    })
642}
643
644///
645/// ResolvedHavingClause
646///
647/// Pre-resolved HAVING clause shape after SQL projection aggregate index
648/// resolution. This keeps SQL shape analysis entity-agnostic before typed
649/// query binding.
650///
651#[derive(Clone, Debug)]
652enum ResolvedHavingClause {
653    GroupField {
654        field: String,
655        op: crate::db::predicate::CompareOp,
656        value: crate::value::Value,
657    },
658    Aggregate {
659        aggregate_index: usize,
660        op: crate::db::predicate::CompareOp,
661        value: crate::value::Value,
662    },
663}
664
665///
666/// LoweredSelectShape
667///
668/// Entity-agnostic lowered SQL SELECT shape prepared for typed `Query<E>`
669/// binding.
670///
671#[derive(Clone, Debug)]
672pub(crate) struct LoweredSelectShape {
673    scalar_projection_fields: Option<Vec<String>>,
674    grouped_projection_aggregates: Vec<SqlAggregateCall>,
675    group_by_fields: Vec<String>,
676    distinct: bool,
677    having: Vec<ResolvedHavingClause>,
678    predicate: Option<Predicate>,
679    order_by: Vec<crate::db::sql::parser::SqlOrderTerm>,
680    limit: Option<u32>,
681    offset: Option<u32>,
682}
683
684///
685/// LoweredBaseQueryShape
686///
687/// Generic-free filter/order/window query modifiers shared by delete and
688/// global-aggregate SQL lowering.
689/// This keeps common SQL query-shape lowering shared before typed query
690/// binding.
691///
692#[derive(Clone, Debug)]
693pub(crate) struct LoweredBaseQueryShape {
694    predicate: Option<Predicate>,
695    order_by: Vec<SqlOrderTerm>,
696    limit: Option<u32>,
697    offset: Option<u32>,
698}
699
700#[inline(never)]
701fn lower_select_shape(
702    statement: SqlSelectStatement,
703    primary_key_field: &str,
704) -> Result<LoweredSelectShape, SqlLoweringError> {
705    let SqlSelectStatement {
706        projection,
707        predicate,
708        distinct,
709        group_by,
710        having,
711        order_by,
712        limit,
713        offset,
714        entity: _,
715    } = statement;
716    let projection_for_having = projection.clone();
717
718    // Phase 1: resolve scalar/grouped projection shape.
719    let (scalar_projection_fields, grouped_projection_aggregates) = if group_by.is_empty() {
720        let scalar_projection_fields =
721            lower_scalar_projection_fields(projection, distinct, primary_key_field)?;
722        (scalar_projection_fields, Vec::new())
723    } else {
724        if distinct {
725            return Err(SqlLoweringError::unsupported_select_distinct());
726        }
727        let grouped_projection_aggregates =
728            grouped_projection_aggregate_calls(&projection, group_by.as_slice())?;
729        (None, grouped_projection_aggregates)
730    };
731
732    // Phase 2: resolve HAVING symbols against grouped projection authority.
733    let having = lower_having_clauses(
734        having,
735        &projection_for_having,
736        group_by.as_slice(),
737        grouped_projection_aggregates.as_slice(),
738    )?;
739
740    Ok(LoweredSelectShape {
741        scalar_projection_fields,
742        grouped_projection_aggregates,
743        group_by_fields: group_by,
744        distinct,
745        having,
746        predicate,
747        order_by,
748        limit,
749        offset,
750    })
751}
752
753fn lower_scalar_projection_fields(
754    projection: SqlProjection,
755    distinct: bool,
756    primary_key_field: &str,
757) -> Result<Option<Vec<String>>, SqlLoweringError> {
758    let SqlProjection::Items(items) = projection else {
759        if distinct {
760            return Ok(None);
761        }
762
763        return Ok(None);
764    };
765
766    let has_aggregate = items
767        .iter()
768        .any(|item| matches!(item, SqlSelectItem::Aggregate(_)));
769    if has_aggregate {
770        return Err(SqlLoweringError::unsupported_select_projection());
771    }
772
773    let fields = items
774        .into_iter()
775        .map(|item| match item {
776            SqlSelectItem::Field(field) => Ok(field),
777            SqlSelectItem::Aggregate(_) => Err(SqlLoweringError::unsupported_select_projection()),
778        })
779        .collect::<Result<Vec<_>, _>>()?;
780
781    validate_scalar_distinct_projection(distinct, fields.as_slice(), primary_key_field)?;
782
783    Ok(Some(fields))
784}
785
786fn validate_scalar_distinct_projection(
787    distinct: bool,
788    projection_fields: &[String],
789    primary_key_field: &str,
790) -> Result<(), SqlLoweringError> {
791    if !distinct {
792        return Ok(());
793    }
794
795    if projection_fields.is_empty() {
796        return Ok(());
797    }
798
799    let has_primary_key_field = projection_fields
800        .iter()
801        .any(|field| field == primary_key_field);
802    if !has_primary_key_field {
803        return Err(SqlLoweringError::unsupported_select_distinct());
804    }
805
806    Ok(())
807}
808
809fn lower_having_clauses(
810    having_clauses: Vec<SqlHavingClause>,
811    projection: &SqlProjection,
812    group_by_fields: &[String],
813    grouped_projection_aggregates: &[SqlAggregateCall],
814) -> Result<Vec<ResolvedHavingClause>, SqlLoweringError> {
815    if having_clauses.is_empty() {
816        return Ok(Vec::new());
817    }
818    if group_by_fields.is_empty() {
819        return Err(SqlLoweringError::unsupported_select_having());
820    }
821
822    let projection_aggregates = grouped_projection_aggregate_calls(projection, group_by_fields)
823        .map_err(|_| SqlLoweringError::unsupported_select_having())?;
824    if projection_aggregates.as_slice() != grouped_projection_aggregates {
825        return Err(SqlLoweringError::unsupported_select_having());
826    }
827
828    let mut lowered = Vec::with_capacity(having_clauses.len());
829    for clause in having_clauses {
830        match clause.symbol {
831            SqlHavingSymbol::Field(field) => lowered.push(ResolvedHavingClause::GroupField {
832                field,
833                op: clause.op,
834                value: clause.value,
835            }),
836            SqlHavingSymbol::Aggregate(aggregate) => {
837                let aggregate_index =
838                    resolve_having_aggregate_index(&aggregate, grouped_projection_aggregates)?;
839                lowered.push(ResolvedHavingClause::Aggregate {
840                    aggregate_index,
841                    op: clause.op,
842                    value: clause.value,
843                });
844            }
845        }
846    }
847
848    Ok(lowered)
849}
850
851#[inline(never)]
852pub(in crate::db) fn apply_lowered_select_shape(
853    mut query: StructuralQuery,
854    lowered: LoweredSelectShape,
855) -> Result<StructuralQuery, SqlLoweringError> {
856    let LoweredSelectShape {
857        scalar_projection_fields,
858        grouped_projection_aggregates,
859        group_by_fields,
860        distinct,
861        having,
862        predicate,
863        order_by,
864        limit,
865        offset,
866    } = lowered;
867
868    // Phase 1: apply grouped declaration semantics.
869    for field in group_by_fields {
870        query = query.group_by(field)?;
871    }
872
873    // Phase 2: apply scalar DISTINCT and projection contracts.
874    if distinct {
875        query = query.distinct();
876    }
877    if let Some(fields) = scalar_projection_fields {
878        query = query.select_fields(fields);
879    }
880    for aggregate in grouped_projection_aggregates {
881        query = query.aggregate(lower_aggregate_call(aggregate)?);
882    }
883
884    // Phase 3: bind resolved HAVING clauses against grouped terminals.
885    for clause in having {
886        match clause {
887            ResolvedHavingClause::GroupField { field, op, value } => {
888                query = query.having_group(field, op, value)?;
889            }
890            ResolvedHavingClause::Aggregate {
891                aggregate_index,
892                op,
893                value,
894            } => {
895                query = query.having_aggregate(aggregate_index, op, value)?;
896            }
897        }
898    }
899
900    // Phase 4: attach the shared filter/order/page tail through the base-query lane.
901    Ok(apply_lowered_base_query_shape(
902        query,
903        LoweredBaseQueryShape {
904            predicate,
905            order_by,
906            limit,
907            offset,
908        },
909    ))
910}
911
912fn apply_lowered_base_query_shape(
913    mut query: StructuralQuery,
914    lowered: LoweredBaseQueryShape,
915) -> StructuralQuery {
916    if let Some(predicate) = lowered.predicate {
917        query = query.filter(predicate);
918    }
919    query = apply_order_terms_structural(query, lowered.order_by);
920    if let Some(limit) = lowered.limit {
921        query = query.limit(limit);
922    }
923    if let Some(offset) = lowered.offset {
924        query = query.offset(offset);
925    }
926
927    query
928}
929
930pub(in crate::db) fn bind_lowered_sql_query_structural(
931    model: &'static crate::model::entity::EntityModel,
932    lowered: LoweredSqlQuery,
933    consistency: MissingRowPolicy,
934) -> Result<StructuralQuery, SqlLoweringError> {
935    match lowered {
936        LoweredSqlQuery::Select(select) => {
937            apply_lowered_select_shape(StructuralQuery::new(model, consistency), select)
938        }
939        LoweredSqlQuery::Delete(delete) => Ok(bind_lowered_sql_delete_query_structural(
940            model,
941            delete,
942            consistency,
943        )),
944    }
945}
946
947pub(in crate::db) fn bind_lowered_sql_delete_query_structural(
948    model: &'static crate::model::entity::EntityModel,
949    delete: LoweredBaseQueryShape,
950    consistency: MissingRowPolicy,
951) -> StructuralQuery {
952    apply_lowered_base_query_shape(StructuralQuery::new(model, consistency).delete(), delete)
953}
954
955pub(in crate::db) fn bind_lowered_sql_query<E: EntityKind>(
956    lowered: LoweredSqlQuery,
957    consistency: MissingRowPolicy,
958) -> Result<Query<E>, SqlLoweringError> {
959    let structural = bind_lowered_sql_query_structural(E::MODEL, lowered, consistency)?;
960
961    Ok(Query::from_inner(structural))
962}
963
964fn bind_lowered_sql_global_aggregate_command<E: EntityKind>(
965    lowered: LoweredSqlGlobalAggregateCommand,
966    consistency: MissingRowPolicy,
967) -> SqlGlobalAggregateCommand<E> {
968    SqlGlobalAggregateCommand {
969        query: Query::from_inner(apply_lowered_base_query_shape(
970            StructuralQuery::new(E::MODEL, consistency),
971            lowered.query,
972        )),
973        terminal: lowered.terminal,
974    }
975}
976
977fn bind_lowered_sql_global_aggregate_command_structural(
978    model: &'static crate::model::entity::EntityModel,
979    lowered: LoweredSqlGlobalAggregateCommand,
980    consistency: MissingRowPolicy,
981) -> StructuralSqlGlobalAggregateCommand {
982    StructuralSqlGlobalAggregateCommand {
983        query: apply_lowered_base_query_shape(
984            StructuralQuery::new(model, consistency),
985            lowered.query,
986        ),
987        terminal: lowered.terminal,
988    }
989}
990
991fn lower_global_aggregate_terminal(
992    projection: SqlProjection,
993) -> Result<SqlGlobalAggregateTerminal, SqlLoweringError> {
994    let SqlProjection::Items(items) = projection else {
995        return Err(SqlLoweringError::unsupported_select_projection());
996    };
997    if items.len() != 1 {
998        return Err(SqlLoweringError::unsupported_select_projection());
999    }
1000
1001    let Some(SqlSelectItem::Aggregate(aggregate)) = items.into_iter().next() else {
1002        return Err(SqlLoweringError::unsupported_select_projection());
1003    };
1004
1005    match (aggregate.kind, aggregate.field) {
1006        (SqlAggregateKind::Count, None) => Ok(SqlGlobalAggregateTerminal::CountRows),
1007        (SqlAggregateKind::Count, Some(field)) => Ok(SqlGlobalAggregateTerminal::CountField(field)),
1008        (SqlAggregateKind::Sum, Some(field)) => Ok(SqlGlobalAggregateTerminal::SumField(field)),
1009        (SqlAggregateKind::Avg, Some(field)) => Ok(SqlGlobalAggregateTerminal::AvgField(field)),
1010        (SqlAggregateKind::Min, Some(field)) => Ok(SqlGlobalAggregateTerminal::MinField(field)),
1011        (SqlAggregateKind::Max, Some(field)) => Ok(SqlGlobalAggregateTerminal::MaxField(field)),
1012        _ => Err(SqlLoweringError::unsupported_select_projection()),
1013    }
1014}
1015
1016fn grouped_projection_aggregate_calls(
1017    projection: &SqlProjection,
1018    group_by_fields: &[String],
1019) -> Result<Vec<SqlAggregateCall>, SqlLoweringError> {
1020    if group_by_fields.is_empty() {
1021        return Err(SqlLoweringError::unsupported_select_group_by());
1022    }
1023
1024    let SqlProjection::Items(items) = projection else {
1025        return Err(SqlLoweringError::unsupported_select_group_by());
1026    };
1027
1028    let mut projected_group_fields = Vec::<String>::new();
1029    let mut aggregate_calls = Vec::<SqlAggregateCall>::new();
1030    let mut seen_aggregate = false;
1031
1032    for item in items {
1033        match item {
1034            SqlSelectItem::Field(field) => {
1035                // Keep grouped projection deterministic and mappable to grouped
1036                // response contracts: group keys must be declared first.
1037                if seen_aggregate {
1038                    return Err(SqlLoweringError::unsupported_select_group_by());
1039                }
1040                projected_group_fields.push(field.clone());
1041            }
1042            SqlSelectItem::Aggregate(aggregate) => {
1043                seen_aggregate = true;
1044                aggregate_calls.push(aggregate.clone());
1045            }
1046        }
1047    }
1048
1049    if aggregate_calls.is_empty() || projected_group_fields.as_slice() != group_by_fields {
1050        return Err(SqlLoweringError::unsupported_select_group_by());
1051    }
1052
1053    Ok(aggregate_calls)
1054}
1055
1056fn lower_aggregate_call(
1057    call: SqlAggregateCall,
1058) -> Result<crate::db::query::builder::AggregateExpr, SqlLoweringError> {
1059    match (call.kind, call.field) {
1060        (SqlAggregateKind::Count, None) => Ok(count()),
1061        (SqlAggregateKind::Count, Some(field)) => Ok(count_by(field)),
1062        (SqlAggregateKind::Sum, Some(field)) => Ok(sum(field)),
1063        (SqlAggregateKind::Avg, Some(field)) => Ok(avg(field)),
1064        (SqlAggregateKind::Min, Some(field)) => Ok(min_by(field)),
1065        (SqlAggregateKind::Max, Some(field)) => Ok(max_by(field)),
1066        _ => Err(SqlLoweringError::unsupported_select_projection()),
1067    }
1068}
1069
1070fn resolve_having_aggregate_index(
1071    target: &SqlAggregateCall,
1072    grouped_projection_aggregates: &[SqlAggregateCall],
1073) -> Result<usize, SqlLoweringError> {
1074    let mut matched = grouped_projection_aggregates
1075        .iter()
1076        .enumerate()
1077        .filter_map(|(index, aggregate)| (aggregate == target).then_some(index));
1078    let Some(index) = matched.next() else {
1079        return Err(SqlLoweringError::unsupported_select_having());
1080    };
1081    if matched.next().is_some() {
1082        return Err(SqlLoweringError::unsupported_select_having());
1083    }
1084
1085    Ok(index)
1086}
1087
1088fn lower_delete_shape(statement: SqlDeleteStatement) -> LoweredBaseQueryShape {
1089    let SqlDeleteStatement {
1090        predicate,
1091        order_by,
1092        limit,
1093        entity: _,
1094    } = statement;
1095
1096    LoweredBaseQueryShape {
1097        predicate,
1098        order_by,
1099        limit,
1100        offset: None,
1101    }
1102}
1103
1104fn apply_order_terms_structural(
1105    mut query: StructuralQuery,
1106    order_by: Vec<crate::db::sql::parser::SqlOrderTerm>,
1107) -> StructuralQuery {
1108    for term in order_by {
1109        query = match term.direction {
1110            SqlOrderDirection::Asc => query.order_by(term.field),
1111            SqlOrderDirection::Desc => query.order_by_desc(term.field),
1112        };
1113    }
1114
1115    query
1116}
1117
1118fn normalize_having_clauses(
1119    clauses: Vec<SqlHavingClause>,
1120    entity_scope: &[String],
1121) -> Vec<SqlHavingClause> {
1122    clauses
1123        .into_iter()
1124        .map(|clause| SqlHavingClause {
1125            symbol: normalize_having_symbol(clause.symbol, entity_scope),
1126            op: clause.op,
1127            value: clause.value,
1128        })
1129        .collect()
1130}
1131
1132fn normalize_having_symbol(symbol: SqlHavingSymbol, entity_scope: &[String]) -> SqlHavingSymbol {
1133    match symbol {
1134        SqlHavingSymbol::Field(field) => {
1135            SqlHavingSymbol::Field(normalize_identifier_to_scope(field, entity_scope))
1136        }
1137        SqlHavingSymbol::Aggregate(aggregate) => SqlHavingSymbol::Aggregate(
1138            normalize_aggregate_call_identifiers(aggregate, entity_scope),
1139        ),
1140    }
1141}
1142
1143fn normalize_aggregate_call_identifiers(
1144    aggregate: SqlAggregateCall,
1145    entity_scope: &[String],
1146) -> SqlAggregateCall {
1147    SqlAggregateCall {
1148        kind: aggregate.kind,
1149        field: aggregate
1150            .field
1151            .map(|field| normalize_identifier_to_scope(field, entity_scope)),
1152    }
1153}
1154
1155// Build one identifier scope used for reducing SQL-qualified field references
1156// (`entity.field`, `schema.entity.field`) into canonical planner field names.
1157fn sql_entity_scope_candidates(sql_entity: &str, expected_entity: &'static str) -> Vec<String> {
1158    let mut out = Vec::new();
1159    out.push(sql_entity.to_string());
1160    out.push(expected_entity.to_string());
1161
1162    if let Some(last) = identifier_last_segment(sql_entity) {
1163        out.push(last.to_string());
1164    }
1165    if let Some(last) = identifier_last_segment(expected_entity) {
1166        out.push(last.to_string());
1167    }
1168
1169    out
1170}
1171
1172fn normalize_projection_identifiers(
1173    projection: SqlProjection,
1174    entity_scope: &[String],
1175) -> SqlProjection {
1176    match projection {
1177        SqlProjection::All => SqlProjection::All,
1178        SqlProjection::Items(items) => SqlProjection::Items(
1179            items
1180                .into_iter()
1181                .map(|item| match item {
1182                    SqlSelectItem::Field(field) => {
1183                        SqlSelectItem::Field(normalize_identifier(field, entity_scope))
1184                    }
1185                    SqlSelectItem::Aggregate(aggregate) => {
1186                        SqlSelectItem::Aggregate(SqlAggregateCall {
1187                            kind: aggregate.kind,
1188                            field: aggregate
1189                                .field
1190                                .map(|field| normalize_identifier(field, entity_scope)),
1191                        })
1192                    }
1193                })
1194                .collect(),
1195        ),
1196    }
1197}
1198
1199fn normalize_order_terms(
1200    terms: Vec<crate::db::sql::parser::SqlOrderTerm>,
1201    entity_scope: &[String],
1202) -> Vec<crate::db::sql::parser::SqlOrderTerm> {
1203    terms
1204        .into_iter()
1205        .map(|term| crate::db::sql::parser::SqlOrderTerm {
1206            field: normalize_identifier(term.field, entity_scope),
1207            direction: term.direction,
1208        })
1209        .collect()
1210}
1211
1212fn normalize_identifier_list(fields: Vec<String>, entity_scope: &[String]) -> Vec<String> {
1213    fields
1214        .into_iter()
1215        .map(|field| normalize_identifier(field, entity_scope))
1216        .collect()
1217}
1218
1219// SQL lowering only adapts identifier qualification (`entity.field` -> `field`)
1220// and delegates predicate-tree traversal ownership to `db::predicate`.
1221fn adapt_predicate_identifiers_to_scope(
1222    predicate: Predicate,
1223    entity_scope: &[String],
1224) -> Predicate {
1225    rewrite_field_identifiers(predicate, |field| normalize_identifier(field, entity_scope))
1226}
1227
1228fn normalize_identifier(identifier: String, entity_scope: &[String]) -> String {
1229    normalize_identifier_to_scope(identifier, entity_scope)
1230}
1231
1232fn ensure_entity_matches_expected(
1233    sql_entity: &str,
1234    expected_entity: &'static str,
1235) -> Result<(), SqlLoweringError> {
1236    if identifiers_tail_match(sql_entity, expected_entity) {
1237        return Ok(());
1238    }
1239
1240    Err(SqlLoweringError::entity_mismatch(
1241        sql_entity,
1242        expected_entity,
1243    ))
1244}