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::{CoercionId, CompareOp, MissingRowPolicy, Predicate},
16        query::{
17            builder::aggregate::{avg, count, count_by, max_by, min_by, sum},
18            intent::{Query, QueryError, StructuralQuery},
19            plan::ExpressionOrderTerm,
20        },
21        sql::identifier::{
22            identifier_last_segment, identifiers_tail_match, normalize_identifier_to_scope,
23            rewrite_field_identifiers,
24        },
25        sql::parser::{
26            SqlAggregateCall, SqlAggregateKind, SqlDeleteStatement, SqlExplainMode,
27            SqlExplainStatement, SqlExplainTarget, SqlHavingClause, SqlHavingSymbol,
28            SqlOrderDirection, SqlOrderTerm, SqlProjection, SqlSelectItem, SqlSelectStatement,
29            SqlStatement, SqlTextFunctionCall,
30        },
31    },
32    model::{entity::EntityModel, field::FieldKind},
33    traits::EntityKind,
34    value::Value,
35};
36use thiserror::Error as ThisError;
37
38///
39/// LoweredSqlCommand
40///
41/// Generic-free SQL command shape after reduced SQL parsing and entity-route
42/// normalization.
43/// This keeps statement-shape lowering shared across entities before typed
44/// `Query<E>` binding happens at the execution boundary.
45///
46#[derive(Clone, Debug)]
47pub struct LoweredSqlCommand(LoweredSqlCommandInner);
48
49#[derive(Clone, Debug)]
50enum LoweredSqlCommandInner {
51    Query(LoweredSqlQuery),
52    Explain {
53        mode: SqlExplainMode,
54        query: LoweredSqlQuery,
55    },
56    ExplainGlobalAggregate {
57        mode: SqlExplainMode,
58        command: LoweredSqlGlobalAggregateCommand,
59    },
60    DescribeEntity,
61    ShowIndexesEntity,
62    ShowColumnsEntity,
63    ShowEntities,
64}
65
66///
67/// SqlCommand
68///
69/// Test-only typed SQL command shell over the shared lowered SQL surface.
70/// Runtime dispatch now consumes `LoweredSqlCommand` directly, but lowering
71/// tests still validate typed binding behavior on this local envelope.
72///
73#[cfg(test)]
74#[derive(Debug)]
75pub(crate) enum SqlCommand<E: EntityKind> {
76    Query(Query<E>),
77    Explain {
78        mode: SqlExplainMode,
79        query: Query<E>,
80    },
81    ExplainGlobalAggregate {
82        mode: SqlExplainMode,
83        command: SqlGlobalAggregateCommand<E>,
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    #[must_use]
106    pub(in crate::db) const fn explain_query(&self) -> Option<(SqlExplainMode, &LoweredSqlQuery)> {
107        match &self.0 {
108            LoweredSqlCommandInner::Explain { mode, query } => Some((*mode, query)),
109            LoweredSqlCommandInner::Query(_)
110            | LoweredSqlCommandInner::ExplainGlobalAggregate { .. }
111            | LoweredSqlCommandInner::DescribeEntity
112            | LoweredSqlCommandInner::ShowIndexesEntity
113            | LoweredSqlCommandInner::ShowColumnsEntity
114            | LoweredSqlCommandInner::ShowEntities => None,
115        }
116    }
117}
118
119///
120/// LoweredSqlQuery
121///
122/// Generic-free executable SQL query shape prepared before typed query binding.
123/// Select and delete lowering stay shared until the final `Query<E>` build.
124///
125#[derive(Clone, Debug)]
126pub(crate) enum LoweredSqlQuery {
127    Select(LoweredSelectShape),
128    Delete(LoweredBaseQueryShape),
129}
130
131impl LoweredSqlQuery {
132    // Report whether this lowered query carries grouped execution semantics.
133    pub(crate) const fn has_grouping(&self) -> bool {
134        match self {
135            Self::Select(select) => select.has_grouping(),
136            Self::Delete(_) => false,
137        }
138    }
139}
140
141///
142/// SqlGlobalAggregateTerminal
143///
144/// Global SQL aggregate terminals currently executable through dedicated
145/// aggregate SQL entrypoints.
146///
147
148#[derive(Clone, Debug, Eq, PartialEq)]
149pub(crate) enum SqlGlobalAggregateTerminal {
150    CountRows,
151    CountField(String),
152    SumField(String),
153    AvgField(String),
154    MinField(String),
155    MaxField(String),
156}
157
158///
159/// LoweredSqlGlobalAggregateCommand
160///
161/// Generic-free global aggregate command shape prepared before typed query
162/// binding.
163/// This keeps aggregate SQL lowering shared across entities until the final
164/// execution boundary converts the base query shape into `Query<E>`.
165///
166#[derive(Clone, Debug)]
167pub(crate) struct LoweredSqlGlobalAggregateCommand {
168    query: LoweredBaseQueryShape,
169    terminal: SqlGlobalAggregateTerminal,
170}
171
172///
173/// LoweredSqlAggregateShape
174///
175/// Locally validated aggregate-call shape used by SQL lowering to avoid
176/// duplicating `(SqlAggregateKind, field)` validation across lowering lanes.
177///
178
179enum LoweredSqlAggregateShape {
180    CountRows,
181    CountField(String),
182    FieldTarget {
183        kind: SqlAggregateKind,
184        field: String,
185    },
186}
187
188///
189/// SqlGlobalAggregateCommand
190///
191/// Lowered global SQL aggregate command carrying base query shape plus terminal.
192///
193
194#[derive(Debug)]
195pub(crate) struct SqlGlobalAggregateCommand<E: EntityKind> {
196    query: Query<E>,
197    terminal: SqlGlobalAggregateTerminal,
198}
199
200impl<E: EntityKind> SqlGlobalAggregateCommand<E> {
201    /// Borrow the lowered base query shape for aggregate execution.
202    #[must_use]
203    pub(crate) const fn query(&self) -> &Query<E> {
204        &self.query
205    }
206
207    /// Borrow the lowered aggregate terminal.
208    #[must_use]
209    pub(crate) const fn terminal(&self) -> &SqlGlobalAggregateTerminal {
210        &self.terminal
211    }
212}
213
214///
215/// SqlGlobalAggregateCommandCore
216///
217/// Generic-free lowered global aggregate command bound onto the structural
218/// query surface.
219/// This keeps global aggregate EXPLAIN on the shared query/explain path until
220/// a typed boundary is strictly required.
221///
222
223#[derive(Debug)]
224pub(crate) struct SqlGlobalAggregateCommandCore {
225    query: StructuralQuery,
226    terminal: SqlGlobalAggregateTerminal,
227}
228
229impl SqlGlobalAggregateCommandCore {
230    /// Borrow the structural query payload for aggregate explain/execution.
231    #[must_use]
232    pub(in crate::db) const fn query(&self) -> &StructuralQuery {
233        &self.query
234    }
235
236    /// Borrow the lowered aggregate terminal.
237    #[must_use]
238    pub(in crate::db) const fn terminal(&self) -> &SqlGlobalAggregateTerminal {
239        &self.terminal
240    }
241}
242
243///
244/// SqlLoweringError
245///
246/// SQL frontend lowering failures before planner validation/execution.
247///
248
249#[derive(Debug, ThisError)]
250pub(crate) enum SqlLoweringError {
251    #[error("{0}")]
252    Parse(#[from] crate::db::sql::parser::SqlParseError),
253
254    #[error("{0}")]
255    Query(#[from] QueryError),
256
257    #[error("SQL entity '{sql_entity}' does not match requested entity type '{expected_entity}'")]
258    EntityMismatch {
259        sql_entity: String,
260        expected_entity: &'static str,
261    },
262
263    #[error(
264        "unsupported SQL SELECT projection; supported forms are SELECT *, field lists, or grouped aggregate shapes"
265    )]
266    UnsupportedSelectProjection,
267
268    #[error("unsupported SQL SELECT DISTINCT")]
269    UnsupportedSelectDistinct,
270
271    #[error("unsupported SQL GROUP BY projection shape")]
272    UnsupportedSelectGroupBy,
273
274    #[error("unsupported SQL HAVING shape")]
275    UnsupportedSelectHaving,
276}
277
278impl SqlLoweringError {
279    /// Construct one entity-mismatch SQL lowering error.
280    fn entity_mismatch(sql_entity: impl Into<String>, expected_entity: &'static str) -> Self {
281        Self::EntityMismatch {
282            sql_entity: sql_entity.into(),
283            expected_entity,
284        }
285    }
286
287    /// Construct one unsupported SELECT projection SQL lowering error.
288    const fn unsupported_select_projection() -> Self {
289        Self::UnsupportedSelectProjection
290    }
291
292    /// Construct one unsupported SELECT DISTINCT SQL lowering error.
293    const fn unsupported_select_distinct() -> Self {
294        Self::UnsupportedSelectDistinct
295    }
296
297    /// Construct one unsupported SELECT GROUP BY shape SQL lowering error.
298    const fn unsupported_select_group_by() -> Self {
299        Self::UnsupportedSelectGroupBy
300    }
301
302    /// Construct one unsupported SELECT HAVING shape SQL lowering error.
303    const fn unsupported_select_having() -> Self {
304        Self::UnsupportedSelectHaving
305    }
306}
307
308///
309/// PreparedSqlStatement
310///
311/// SQL statement envelope after entity-scope normalization and
312/// entity-match validation for one target entity descriptor.
313///
314/// This pre-lowering contract is entity-agnostic and reusable across
315/// dynamic SQL route branches before typed `Query<E>` binding.
316///
317
318#[derive(Clone, Debug)]
319pub(crate) struct PreparedSqlStatement {
320    statement: SqlStatement,
321}
322
323#[derive(Clone, Copy, Debug, Eq, PartialEq)]
324pub(crate) enum LoweredSqlLaneKind {
325    Query,
326    Explain,
327    Describe,
328    ShowIndexes,
329    ShowColumns,
330    ShowEntities,
331}
332
333/// Parse and lower one SQL statement into canonical query intent for `E`.
334#[cfg(test)]
335pub(crate) fn compile_sql_command<E: EntityKind>(
336    sql: &str,
337    consistency: MissingRowPolicy,
338) -> Result<SqlCommand<E>, SqlLoweringError> {
339    let statement = crate::db::sql::parser::parse_sql(sql)?;
340    compile_sql_command_from_statement::<E>(statement, consistency)
341}
342
343/// Lower one parsed SQL statement into canonical query intent for `E`.
344#[cfg(test)]
345pub(crate) fn compile_sql_command_from_statement<E: EntityKind>(
346    statement: SqlStatement,
347    consistency: MissingRowPolicy,
348) -> Result<SqlCommand<E>, SqlLoweringError> {
349    let prepared = prepare_sql_statement(statement, E::MODEL.name())?;
350    compile_sql_command_from_prepared_statement::<E>(prepared, consistency)
351}
352
353/// Lower one prepared SQL statement into canonical query intent for `E`.
354#[cfg(test)]
355pub(crate) fn compile_sql_command_from_prepared_statement<E: EntityKind>(
356    prepared: PreparedSqlStatement,
357    consistency: MissingRowPolicy,
358) -> Result<SqlCommand<E>, SqlLoweringError> {
359    let lowered = lower_sql_command_from_prepared_statement(prepared, E::MODEL.primary_key.name)?;
360
361    bind_lowered_sql_command::<E>(lowered, consistency)
362}
363
364/// Lower one prepared SQL statement into one shared generic-free command shape.
365#[inline(never)]
366pub(crate) fn lower_sql_command_from_prepared_statement(
367    prepared: PreparedSqlStatement,
368    primary_key_field: &str,
369) -> Result<LoweredSqlCommand, SqlLoweringError> {
370    lower_prepared_statement(prepared.statement, primary_key_field)
371}
372
373pub(crate) const fn lowered_sql_command_lane(command: &LoweredSqlCommand) -> LoweredSqlLaneKind {
374    match command.0 {
375        LoweredSqlCommandInner::Query(_) => LoweredSqlLaneKind::Query,
376        LoweredSqlCommandInner::Explain { .. }
377        | LoweredSqlCommandInner::ExplainGlobalAggregate { .. } => LoweredSqlLaneKind::Explain,
378        LoweredSqlCommandInner::DescribeEntity => LoweredSqlLaneKind::Describe,
379        LoweredSqlCommandInner::ShowIndexesEntity => LoweredSqlLaneKind::ShowIndexes,
380        LoweredSqlCommandInner::ShowColumnsEntity => LoweredSqlLaneKind::ShowColumns,
381        LoweredSqlCommandInner::ShowEntities => LoweredSqlLaneKind::ShowEntities,
382    }
383}
384
385/// Return whether one parsed SQL statement is an executable constrained global
386/// aggregate shape owned by the dedicated aggregate lane.
387pub(in crate::db) fn is_sql_global_aggregate_statement(statement: &SqlStatement) -> bool {
388    let SqlStatement::Select(statement) = statement else {
389        return false;
390    };
391
392    is_sql_global_aggregate_select(statement)
393}
394
395// Detect one constrained global aggregate select shape without widening any
396// non-aggregate SQL surface onto the dedicated aggregate execution lane.
397fn is_sql_global_aggregate_select(statement: &SqlSelectStatement) -> bool {
398    if statement.distinct || !statement.group_by.is_empty() || !statement.having.is_empty() {
399        return false;
400    }
401
402    lower_global_aggregate_terminal(statement.projection.clone()).is_ok()
403}
404
405/// Render one lowered EXPLAIN command through the shared structural SQL path.
406#[inline(never)]
407pub(crate) fn render_lowered_sql_explain_plan_or_json(
408    lowered: &LoweredSqlCommand,
409    model: &'static crate::model::entity::EntityModel,
410    consistency: MissingRowPolicy,
411) -> Result<Option<String>, SqlLoweringError> {
412    let LoweredSqlCommandInner::Explain { mode, query } = &lowered.0 else {
413        return Ok(None);
414    };
415
416    let query = bind_lowered_sql_query_structural(model, query.clone(), consistency)?;
417    let rendered = match mode {
418        SqlExplainMode::Plan | SqlExplainMode::Json => {
419            let plan = query.build_plan()?;
420            let explain = plan.explain_with_model(model);
421
422            match mode {
423                SqlExplainMode::Plan => explain.render_text_canonical(),
424                SqlExplainMode::Json => explain.render_json_canonical(),
425                SqlExplainMode::Execution => unreachable!("execution mode handled above"),
426            }
427        }
428        SqlExplainMode::Execution => query.explain_execution_text()?,
429    };
430
431    Ok(Some(rendered))
432}
433
434/// Bind one lowered global aggregate EXPLAIN shape onto the structural query
435/// surface when the explain command carries that specialized form.
436pub(crate) fn bind_lowered_sql_explain_global_aggregate_structural(
437    lowered: &LoweredSqlCommand,
438    model: &'static crate::model::entity::EntityModel,
439    consistency: MissingRowPolicy,
440) -> Option<(SqlExplainMode, SqlGlobalAggregateCommandCore)> {
441    let LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command } = &lowered.0 else {
442        return None;
443    };
444
445    Some((
446        *mode,
447        bind_lowered_sql_global_aggregate_command_structural(model, command.clone(), consistency),
448    ))
449}
450
451/// Bind one shared generic-free SQL command shape to the typed query surface.
452#[cfg(test)]
453pub(crate) fn bind_lowered_sql_command<E: EntityKind>(
454    lowered: LoweredSqlCommand,
455    consistency: MissingRowPolicy,
456) -> Result<SqlCommand<E>, SqlLoweringError> {
457    match lowered.0 {
458        LoweredSqlCommandInner::Query(query) => Ok(SqlCommand::Query(bind_lowered_sql_query::<E>(
459            query,
460            consistency,
461        )?)),
462        LoweredSqlCommandInner::Explain { mode, query } => Ok(SqlCommand::Explain {
463            mode,
464            query: bind_lowered_sql_query::<E>(query, consistency)?,
465        }),
466        LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command } => {
467            Ok(SqlCommand::ExplainGlobalAggregate {
468                mode,
469                command: bind_lowered_sql_global_aggregate_command::<E>(command, consistency),
470            })
471        }
472        LoweredSqlCommandInner::DescribeEntity => Ok(SqlCommand::DescribeEntity),
473        LoweredSqlCommandInner::ShowIndexesEntity => Ok(SqlCommand::ShowIndexesEntity),
474        LoweredSqlCommandInner::ShowColumnsEntity => Ok(SqlCommand::ShowColumnsEntity),
475        LoweredSqlCommandInner::ShowEntities => Ok(SqlCommand::ShowEntities),
476    }
477}
478
479/// Prepare one parsed SQL statement for one expected entity route.
480#[inline(never)]
481pub(crate) fn prepare_sql_statement(
482    statement: SqlStatement,
483    expected_entity: &'static str,
484) -> Result<PreparedSqlStatement, SqlLoweringError> {
485    let statement = prepare_statement(statement, expected_entity)?;
486
487    Ok(PreparedSqlStatement { statement })
488}
489
490/// Parse and lower one SQL statement into global aggregate execution command for `E`.
491#[cfg(test)]
492pub(crate) fn compile_sql_global_aggregate_command<E: EntityKind>(
493    sql: &str,
494    consistency: MissingRowPolicy,
495) -> Result<SqlGlobalAggregateCommand<E>, SqlLoweringError> {
496    let statement = crate::db::sql::parser::parse_sql(sql)?;
497    let prepared = prepare_sql_statement(statement, E::MODEL.name())?;
498    compile_sql_global_aggregate_command_from_prepared::<E>(prepared, consistency)
499}
500
501// Lower one already-prepared SQL statement into the constrained global
502// aggregate command envelope so callers that already parsed and routed the
503// statement do not pay the parser again.
504pub(crate) fn compile_sql_global_aggregate_command_from_prepared<E: EntityKind>(
505    prepared: PreparedSqlStatement,
506    consistency: MissingRowPolicy,
507) -> Result<SqlGlobalAggregateCommand<E>, SqlLoweringError> {
508    let SqlStatement::Select(statement) = prepared.statement else {
509        return Err(SqlLoweringError::unsupported_select_projection());
510    };
511
512    Ok(bind_lowered_sql_global_aggregate_command::<E>(
513        lower_global_aggregate_select_shape(statement)?,
514        consistency,
515    ))
516}
517
518#[inline(never)]
519fn prepare_statement(
520    statement: SqlStatement,
521    expected_entity: &'static str,
522) -> Result<SqlStatement, SqlLoweringError> {
523    match statement {
524        SqlStatement::Select(statement) => Ok(SqlStatement::Select(prepare_select_statement(
525            statement,
526            expected_entity,
527        )?)),
528        SqlStatement::Delete(statement) => Ok(SqlStatement::Delete(prepare_delete_statement(
529            statement,
530            expected_entity,
531        )?)),
532        SqlStatement::Explain(statement) => Ok(SqlStatement::Explain(prepare_explain_statement(
533            statement,
534            expected_entity,
535        )?)),
536        SqlStatement::Describe(statement) => {
537            ensure_entity_matches_expected(statement.entity.as_str(), expected_entity)?;
538
539            Ok(SqlStatement::Describe(statement))
540        }
541        SqlStatement::ShowIndexes(statement) => {
542            ensure_entity_matches_expected(statement.entity.as_str(), expected_entity)?;
543
544            Ok(SqlStatement::ShowIndexes(statement))
545        }
546        SqlStatement::ShowColumns(statement) => {
547            ensure_entity_matches_expected(statement.entity.as_str(), expected_entity)?;
548
549            Ok(SqlStatement::ShowColumns(statement))
550        }
551        SqlStatement::ShowEntities(statement) => Ok(SqlStatement::ShowEntities(statement)),
552    }
553}
554
555fn prepare_explain_statement(
556    statement: SqlExplainStatement,
557    expected_entity: &'static str,
558) -> Result<SqlExplainStatement, SqlLoweringError> {
559    let target = match statement.statement {
560        SqlExplainTarget::Select(select_statement) => {
561            SqlExplainTarget::Select(prepare_select_statement(select_statement, expected_entity)?)
562        }
563        SqlExplainTarget::Delete(delete_statement) => {
564            SqlExplainTarget::Delete(prepare_delete_statement(delete_statement, expected_entity)?)
565        }
566    };
567
568    Ok(SqlExplainStatement {
569        mode: statement.mode,
570        statement: target,
571    })
572}
573
574fn prepare_select_statement(
575    statement: SqlSelectStatement,
576    expected_entity: &'static str,
577) -> Result<SqlSelectStatement, SqlLoweringError> {
578    ensure_entity_matches_expected(statement.entity.as_str(), expected_entity)?;
579
580    Ok(normalize_select_statement_to_expected_entity(
581        statement,
582        expected_entity,
583    ))
584}
585
586fn normalize_select_statement_to_expected_entity(
587    mut statement: SqlSelectStatement,
588    expected_entity: &'static str,
589) -> SqlSelectStatement {
590    // Re-scope parsed identifiers onto the resolved entity surface after the
591    // caller has already established entity ownership for this statement.
592    let entity_scope = sql_entity_scope_candidates(statement.entity.as_str(), expected_entity);
593    statement.projection =
594        normalize_projection_identifiers(statement.projection, entity_scope.as_slice());
595    statement.group_by = normalize_identifier_list(statement.group_by, entity_scope.as_slice());
596    statement.predicate = statement
597        .predicate
598        .map(|predicate| adapt_predicate_identifiers_to_scope(predicate, entity_scope.as_slice()));
599    statement.order_by = normalize_order_terms(statement.order_by, entity_scope.as_slice());
600    statement.having = normalize_having_clauses(statement.having, entity_scope.as_slice());
601
602    statement
603}
604
605fn prepare_delete_statement(
606    mut statement: SqlDeleteStatement,
607    expected_entity: &'static str,
608) -> Result<SqlDeleteStatement, SqlLoweringError> {
609    ensure_entity_matches_expected(statement.entity.as_str(), expected_entity)?;
610    let entity_scope = sql_entity_scope_candidates(statement.entity.as_str(), expected_entity);
611    statement.predicate = statement
612        .predicate
613        .map(|predicate| adapt_predicate_identifiers_to_scope(predicate, entity_scope.as_slice()));
614    statement.order_by = normalize_order_terms(statement.order_by, entity_scope.as_slice());
615
616    Ok(statement)
617}
618
619#[inline(never)]
620fn lower_prepared_statement(
621    statement: SqlStatement,
622    primary_key_field: &str,
623) -> Result<LoweredSqlCommand, SqlLoweringError> {
624    match statement {
625        SqlStatement::Select(statement) => Ok(LoweredSqlCommand(LoweredSqlCommandInner::Query(
626            LoweredSqlQuery::Select(lower_select_shape(statement, primary_key_field)?),
627        ))),
628        SqlStatement::Delete(statement) => Ok(LoweredSqlCommand(LoweredSqlCommandInner::Query(
629            LoweredSqlQuery::Delete(lower_delete_shape(statement)),
630        ))),
631        SqlStatement::Explain(statement) => lower_explain_prepared(statement, primary_key_field),
632        SqlStatement::Describe(_) => Ok(LoweredSqlCommand(LoweredSqlCommandInner::DescribeEntity)),
633        SqlStatement::ShowIndexes(_) => {
634            Ok(LoweredSqlCommand(LoweredSqlCommandInner::ShowIndexesEntity))
635        }
636        SqlStatement::ShowColumns(_) => {
637            Ok(LoweredSqlCommand(LoweredSqlCommandInner::ShowColumnsEntity))
638        }
639        SqlStatement::ShowEntities(_) => {
640            Ok(LoweredSqlCommand(LoweredSqlCommandInner::ShowEntities))
641        }
642    }
643}
644
645fn lower_explain_prepared(
646    statement: SqlExplainStatement,
647    primary_key_field: &str,
648) -> Result<LoweredSqlCommand, SqlLoweringError> {
649    let mode = statement.mode;
650
651    match statement.statement {
652        SqlExplainTarget::Select(select_statement) => {
653            lower_explain_select_prepared(select_statement, mode, primary_key_field)
654        }
655        SqlExplainTarget::Delete(delete_statement) => {
656            Ok(LoweredSqlCommand(LoweredSqlCommandInner::Explain {
657                mode,
658                query: LoweredSqlQuery::Delete(lower_delete_shape(delete_statement)),
659            }))
660        }
661    }
662}
663
664fn lower_explain_select_prepared(
665    statement: SqlSelectStatement,
666    mode: SqlExplainMode,
667    primary_key_field: &str,
668) -> Result<LoweredSqlCommand, SqlLoweringError> {
669    match lower_select_shape(statement.clone(), primary_key_field) {
670        Ok(query) => Ok(LoweredSqlCommand(LoweredSqlCommandInner::Explain {
671            mode,
672            query: LoweredSqlQuery::Select(query),
673        })),
674        Err(SqlLoweringError::UnsupportedSelectProjection) => {
675            let command = lower_global_aggregate_select_shape(statement)?;
676
677            Ok(LoweredSqlCommand(
678                LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command },
679            ))
680        }
681        Err(err) => Err(err),
682    }
683}
684
685fn lower_global_aggregate_select_shape(
686    statement: SqlSelectStatement,
687) -> Result<LoweredSqlGlobalAggregateCommand, SqlLoweringError> {
688    let SqlSelectStatement {
689        projection,
690        predicate,
691        distinct,
692        group_by,
693        having,
694        order_by,
695        limit,
696        offset,
697        entity: _,
698    } = statement;
699
700    if distinct {
701        return Err(SqlLoweringError::unsupported_select_distinct());
702    }
703    if !group_by.is_empty() {
704        return Err(SqlLoweringError::unsupported_select_group_by());
705    }
706    if !having.is_empty() {
707        return Err(SqlLoweringError::unsupported_select_having());
708    }
709
710    let terminal = lower_global_aggregate_terminal(projection)?;
711
712    Ok(LoweredSqlGlobalAggregateCommand {
713        query: LoweredBaseQueryShape {
714            predicate,
715            order_by,
716            limit,
717            offset,
718        },
719        terminal,
720    })
721}
722
723///
724/// ResolvedHavingClause
725///
726/// Pre-resolved HAVING clause shape after SQL projection aggregate index
727/// resolution. This keeps SQL shape analysis entity-agnostic before typed
728/// query binding.
729///
730#[derive(Clone, Debug)]
731enum ResolvedHavingClause {
732    GroupField {
733        field: String,
734        op: crate::db::predicate::CompareOp,
735        value: crate::value::Value,
736    },
737    Aggregate {
738        aggregate_index: usize,
739        op: crate::db::predicate::CompareOp,
740        value: crate::value::Value,
741    },
742}
743
744///
745/// LoweredSelectShape
746///
747/// Entity-agnostic lowered SQL SELECT shape prepared for typed `Query<E>`
748/// binding.
749///
750#[derive(Clone, Debug)]
751pub(crate) struct LoweredSelectShape {
752    scalar_projection_fields: Option<Vec<String>>,
753    grouped_projection_aggregates: Vec<SqlAggregateCall>,
754    group_by_fields: Vec<String>,
755    distinct: bool,
756    having: Vec<ResolvedHavingClause>,
757    predicate: Option<Predicate>,
758    order_by: Vec<crate::db::sql::parser::SqlOrderTerm>,
759    limit: Option<u32>,
760    offset: Option<u32>,
761}
762
763impl LoweredSelectShape {
764    // Report whether this lowered select shape carries grouped execution state.
765    const fn has_grouping(&self) -> bool {
766        !self.group_by_fields.is_empty()
767    }
768}
769
770///
771/// LoweredBaseQueryShape
772///
773/// Generic-free filter/order/window query modifiers shared by delete and
774/// global-aggregate SQL lowering.
775/// This keeps common SQL query-shape lowering shared before typed query
776/// binding.
777///
778#[derive(Clone, Debug)]
779pub(crate) struct LoweredBaseQueryShape {
780    predicate: Option<Predicate>,
781    order_by: Vec<SqlOrderTerm>,
782    limit: Option<u32>,
783    offset: Option<u32>,
784}
785
786#[inline(never)]
787fn lower_select_shape(
788    statement: SqlSelectStatement,
789    primary_key_field: &str,
790) -> Result<LoweredSelectShape, SqlLoweringError> {
791    let SqlSelectStatement {
792        projection,
793        predicate,
794        distinct,
795        group_by,
796        having,
797        order_by,
798        limit,
799        offset,
800        entity: _,
801    } = statement;
802    let projection_for_having = projection.clone();
803
804    // Phase 1: resolve scalar/grouped projection shape.
805    let (scalar_projection_fields, grouped_projection_aggregates) = if group_by.is_empty() {
806        let scalar_projection_fields =
807            lower_scalar_projection_fields(projection, distinct, primary_key_field)?;
808        (scalar_projection_fields, Vec::new())
809    } else {
810        if distinct {
811            return Err(SqlLoweringError::unsupported_select_distinct());
812        }
813        let grouped_projection_aggregates =
814            grouped_projection_aggregate_calls(&projection, group_by.as_slice())?;
815        (None, grouped_projection_aggregates)
816    };
817
818    // Phase 2: resolve HAVING symbols against grouped projection authority.
819    let having = lower_having_clauses(
820        having,
821        &projection_for_having,
822        group_by.as_slice(),
823        grouped_projection_aggregates.as_slice(),
824    )?;
825
826    Ok(LoweredSelectShape {
827        scalar_projection_fields,
828        grouped_projection_aggregates,
829        group_by_fields: group_by,
830        distinct,
831        having,
832        predicate,
833        order_by,
834        limit,
835        offset,
836    })
837}
838
839fn lower_scalar_projection_fields(
840    projection: SqlProjection,
841    distinct: bool,
842    primary_key_field: &str,
843) -> Result<Option<Vec<String>>, SqlLoweringError> {
844    let SqlProjection::Items(items) = projection else {
845        if distinct {
846            return Ok(None);
847        }
848
849        return Ok(None);
850    };
851
852    let has_aggregate = items
853        .iter()
854        .any(|item| matches!(item, SqlSelectItem::Aggregate(_)));
855    if has_aggregate {
856        return Err(SqlLoweringError::unsupported_select_projection());
857    }
858
859    let fields = items
860        .into_iter()
861        .map(|item| match item {
862            SqlSelectItem::Field(field) => Ok(field),
863            SqlSelectItem::Aggregate(_) | SqlSelectItem::TextFunction(_) => {
864                Err(SqlLoweringError::unsupported_select_projection())
865            }
866        })
867        .collect::<Result<Vec<_>, _>>()?;
868
869    validate_scalar_distinct_projection(distinct, fields.as_slice(), primary_key_field)?;
870
871    Ok(Some(fields))
872}
873
874fn validate_scalar_distinct_projection(
875    distinct: bool,
876    projection_fields: &[String],
877    primary_key_field: &str,
878) -> Result<(), SqlLoweringError> {
879    if !distinct {
880        return Ok(());
881    }
882
883    if projection_fields.is_empty() {
884        return Ok(());
885    }
886
887    let has_primary_key_field = projection_fields
888        .iter()
889        .any(|field| field == primary_key_field);
890    if !has_primary_key_field {
891        return Err(SqlLoweringError::unsupported_select_distinct());
892    }
893
894    Ok(())
895}
896
897fn lower_having_clauses(
898    having_clauses: Vec<SqlHavingClause>,
899    projection: &SqlProjection,
900    group_by_fields: &[String],
901    grouped_projection_aggregates: &[SqlAggregateCall],
902) -> Result<Vec<ResolvedHavingClause>, SqlLoweringError> {
903    if having_clauses.is_empty() {
904        return Ok(Vec::new());
905    }
906    if group_by_fields.is_empty() {
907        return Err(SqlLoweringError::unsupported_select_having());
908    }
909
910    let projection_aggregates = grouped_projection_aggregate_calls(projection, group_by_fields)
911        .map_err(|_| SqlLoweringError::unsupported_select_having())?;
912    if projection_aggregates.as_slice() != grouped_projection_aggregates {
913        return Err(SqlLoweringError::unsupported_select_having());
914    }
915
916    let mut lowered = Vec::with_capacity(having_clauses.len());
917    for clause in having_clauses {
918        match clause.symbol {
919            SqlHavingSymbol::Field(field) => lowered.push(ResolvedHavingClause::GroupField {
920                field,
921                op: clause.op,
922                value: clause.value,
923            }),
924            SqlHavingSymbol::Aggregate(aggregate) => {
925                let aggregate_index =
926                    resolve_having_aggregate_index(&aggregate, grouped_projection_aggregates)?;
927                lowered.push(ResolvedHavingClause::Aggregate {
928                    aggregate_index,
929                    op: clause.op,
930                    value: clause.value,
931                });
932            }
933        }
934    }
935
936    Ok(lowered)
937}
938
939// Canonicalize strict numeric SQL predicate literals onto the resolved model
940// field kind so unsigned-width fields keep strict/indexable semantics even
941// though reduced SQL integer tokens parse through one generic numeric value
942// variant first.
943fn canonicalize_sql_predicate_for_model(
944    model: &'static EntityModel,
945    predicate: Predicate,
946) -> Predicate {
947    match predicate {
948        Predicate::And(children) => Predicate::And(
949            children
950                .into_iter()
951                .map(|child| canonicalize_sql_predicate_for_model(model, child))
952                .collect(),
953        ),
954        Predicate::Or(children) => Predicate::Or(
955            children
956                .into_iter()
957                .map(|child| canonicalize_sql_predicate_for_model(model, child))
958                .collect(),
959        ),
960        Predicate::Not(inner) => Predicate::Not(Box::new(canonicalize_sql_predicate_for_model(
961            model, *inner,
962        ))),
963        Predicate::Compare(mut cmp) => {
964            canonicalize_sql_compare_for_model(model, &mut cmp);
965            Predicate::Compare(cmp)
966        }
967        Predicate::True
968        | Predicate::False
969        | Predicate::IsNull { .. }
970        | Predicate::IsNotNull { .. }
971        | Predicate::IsMissing { .. }
972        | Predicate::IsEmpty { .. }
973        | Predicate::IsNotEmpty { .. }
974        | Predicate::TextContains { .. }
975        | Predicate::TextContainsCi { .. } => predicate,
976    }
977}
978
979// Resolve one lowered predicate field onto the runtime model kind that owns
980// its strict literal compatibility rules.
981fn model_field_kind(model: &'static EntityModel, field: &str) -> Option<FieldKind> {
982    model
983        .fields()
984        .iter()
985        .find(|candidate| candidate.name() == field)
986        .map(crate::model::field::FieldModel::kind)
987}
988
989// Keep SQL-only literal widening narrow:
990// - only strict equality-style numeric predicates are eligible
991// - ordering already uses `NumericWiden`
992// - text and expression-wrapped predicates stay untouched
993fn canonicalize_sql_compare_for_model(
994    model: &'static EntityModel,
995    cmp: &mut crate::db::predicate::ComparePredicate,
996) {
997    if cmp.coercion.id != CoercionId::Strict {
998        return;
999    }
1000
1001    let Some(field_kind) = model_field_kind(model, &cmp.field) else {
1002        return;
1003    };
1004
1005    match cmp.op {
1006        CompareOp::Eq | CompareOp::Ne => {
1007            if let Some(value) =
1008                canonicalize_strict_sql_numeric_value_for_kind(&field_kind, &cmp.value)
1009            {
1010                cmp.value = value;
1011            }
1012        }
1013        CompareOp::In | CompareOp::NotIn => {
1014            let Value::List(items) = &cmp.value else {
1015                return;
1016            };
1017
1018            let items = items
1019                .iter()
1020                .map(|item| {
1021                    canonicalize_strict_sql_numeric_value_for_kind(&field_kind, item)
1022                        .unwrap_or_else(|| item.clone())
1023                })
1024                .collect();
1025            cmp.value = Value::List(items);
1026        }
1027        CompareOp::Lt
1028        | CompareOp::Lte
1029        | CompareOp::Gt
1030        | CompareOp::Gte
1031        | CompareOp::Contains
1032        | CompareOp::StartsWith
1033        | CompareOp::EndsWith => {}
1034    }
1035}
1036
1037// Convert one parsed SQL numeric literal into the exact runtime `Value` variant
1038// required by the field kind when that conversion is lossless and unambiguous.
1039// This preserves strict equality semantics while still letting SQL express
1040// unsigned-width comparisons such as `Nat16`/`u64` fields.
1041fn canonicalize_strict_sql_numeric_value_for_kind(
1042    kind: &FieldKind,
1043    value: &Value,
1044) -> Option<Value> {
1045    match kind {
1046        FieldKind::Relation { key_kind, .. } => {
1047            canonicalize_strict_sql_numeric_value_for_kind(key_kind, value)
1048        }
1049        FieldKind::Int => match value {
1050            Value::Int(inner) => Some(Value::Int(*inner)),
1051            Value::Uint(inner) => i64::try_from(*inner).ok().map(Value::Int),
1052            _ => None,
1053        },
1054        FieldKind::Uint => match value {
1055            Value::Int(inner) => u64::try_from(*inner).ok().map(Value::Uint),
1056            Value::Uint(inner) => Some(Value::Uint(*inner)),
1057            _ => None,
1058        },
1059        FieldKind::Account
1060        | FieldKind::Blob
1061        | FieldKind::Bool
1062        | FieldKind::Date
1063        | FieldKind::Decimal { .. }
1064        | FieldKind::Duration
1065        | FieldKind::Enum { .. }
1066        | FieldKind::Float32
1067        | FieldKind::Float64
1068        | FieldKind::Int128
1069        | FieldKind::IntBig
1070        | FieldKind::List(_)
1071        | FieldKind::Map { .. }
1072        | FieldKind::Principal
1073        | FieldKind::Set(_)
1074        | FieldKind::Structured { .. }
1075        | FieldKind::Subaccount
1076        | FieldKind::Text
1077        | FieldKind::Timestamp
1078        | FieldKind::Uint128
1079        | FieldKind::UintBig
1080        | FieldKind::Ulid
1081        | FieldKind::Unit => None,
1082    }
1083}
1084
1085#[inline(never)]
1086pub(in crate::db) fn apply_lowered_select_shape(
1087    mut query: StructuralQuery,
1088    lowered: LoweredSelectShape,
1089) -> Result<StructuralQuery, SqlLoweringError> {
1090    let LoweredSelectShape {
1091        scalar_projection_fields,
1092        grouped_projection_aggregates,
1093        group_by_fields,
1094        distinct,
1095        having,
1096        predicate,
1097        order_by,
1098        limit,
1099        offset,
1100    } = lowered;
1101    let model = query.model();
1102
1103    // Phase 1: apply grouped declaration semantics.
1104    for field in group_by_fields {
1105        query = query.group_by(field)?;
1106    }
1107
1108    // Phase 2: apply scalar DISTINCT and projection contracts.
1109    if distinct {
1110        query = query.distinct();
1111    }
1112    if let Some(fields) = scalar_projection_fields {
1113        query = query.select_fields(fields);
1114    }
1115    for aggregate in grouped_projection_aggregates {
1116        query = query.aggregate(lower_aggregate_call(aggregate)?);
1117    }
1118
1119    // Phase 3: bind resolved HAVING clauses against grouped terminals.
1120    for clause in having {
1121        match clause {
1122            ResolvedHavingClause::GroupField { field, op, value } => {
1123                let value = model_field_kind(model, &field)
1124                    .and_then(|field_kind| {
1125                        canonicalize_strict_sql_numeric_value_for_kind(&field_kind, &value)
1126                    })
1127                    .unwrap_or(value);
1128                query = query.having_group(field, op, value)?;
1129            }
1130            ResolvedHavingClause::Aggregate {
1131                aggregate_index,
1132                op,
1133                value,
1134            } => {
1135                query = query.having_aggregate(aggregate_index, op, value)?;
1136            }
1137        }
1138    }
1139
1140    // Phase 4: attach the shared filter/order/page tail through the base-query lane.
1141    Ok(apply_lowered_base_query_shape(
1142        query,
1143        LoweredBaseQueryShape {
1144            predicate: predicate
1145                .map(|predicate| canonicalize_sql_predicate_for_model(model, predicate)),
1146            order_by,
1147            limit,
1148            offset,
1149        },
1150    ))
1151}
1152
1153fn apply_lowered_base_query_shape(
1154    mut query: StructuralQuery,
1155    lowered: LoweredBaseQueryShape,
1156) -> StructuralQuery {
1157    if let Some(predicate) = lowered.predicate {
1158        query = query.filter(predicate);
1159    }
1160    query = apply_order_terms_structural(query, lowered.order_by);
1161    if let Some(limit) = lowered.limit {
1162        query = query.limit(limit);
1163    }
1164    if let Some(offset) = lowered.offset {
1165        query = query.offset(offset);
1166    }
1167
1168    query
1169}
1170
1171pub(in crate::db) fn bind_lowered_sql_query_structural(
1172    model: &'static crate::model::entity::EntityModel,
1173    lowered: LoweredSqlQuery,
1174    consistency: MissingRowPolicy,
1175) -> Result<StructuralQuery, SqlLoweringError> {
1176    match lowered {
1177        LoweredSqlQuery::Select(select) => {
1178            apply_lowered_select_shape(StructuralQuery::new(model, consistency), select)
1179        }
1180        LoweredSqlQuery::Delete(delete) => Ok(bind_lowered_sql_delete_query_structural(
1181            model,
1182            delete,
1183            consistency,
1184        )),
1185    }
1186}
1187
1188pub(in crate::db) fn bind_lowered_sql_delete_query_structural(
1189    model: &'static crate::model::entity::EntityModel,
1190    delete: LoweredBaseQueryShape,
1191    consistency: MissingRowPolicy,
1192) -> StructuralQuery {
1193    apply_lowered_base_query_shape(StructuralQuery::new(model, consistency).delete(), delete)
1194}
1195
1196pub(in crate::db) fn bind_lowered_sql_query<E: EntityKind>(
1197    lowered: LoweredSqlQuery,
1198    consistency: MissingRowPolicy,
1199) -> Result<Query<E>, SqlLoweringError> {
1200    let structural = bind_lowered_sql_query_structural(E::MODEL, lowered, consistency)?;
1201
1202    Ok(Query::from_inner(structural))
1203}
1204
1205fn bind_lowered_sql_global_aggregate_command<E: EntityKind>(
1206    lowered: LoweredSqlGlobalAggregateCommand,
1207    consistency: MissingRowPolicy,
1208) -> SqlGlobalAggregateCommand<E> {
1209    SqlGlobalAggregateCommand {
1210        query: Query::from_inner(apply_lowered_base_query_shape(
1211            StructuralQuery::new(E::MODEL, consistency),
1212            lowered.query,
1213        )),
1214        terminal: lowered.terminal,
1215    }
1216}
1217
1218fn bind_lowered_sql_global_aggregate_command_structural(
1219    model: &'static crate::model::entity::EntityModel,
1220    lowered: LoweredSqlGlobalAggregateCommand,
1221    consistency: MissingRowPolicy,
1222) -> SqlGlobalAggregateCommandCore {
1223    SqlGlobalAggregateCommandCore {
1224        query: apply_lowered_base_query_shape(
1225            StructuralQuery::new(model, consistency),
1226            lowered.query,
1227        ),
1228        terminal: lowered.terminal,
1229    }
1230}
1231
1232fn lower_global_aggregate_terminal(
1233    projection: SqlProjection,
1234) -> Result<SqlGlobalAggregateTerminal, SqlLoweringError> {
1235    let SqlProjection::Items(items) = projection else {
1236        return Err(SqlLoweringError::unsupported_select_projection());
1237    };
1238    if items.len() != 1 {
1239        return Err(SqlLoweringError::unsupported_select_projection());
1240    }
1241
1242    let Some(SqlSelectItem::Aggregate(aggregate)) = items.into_iter().next() else {
1243        return Err(SqlLoweringError::unsupported_select_projection());
1244    };
1245
1246    match lower_sql_aggregate_shape(aggregate)? {
1247        LoweredSqlAggregateShape::CountRows => Ok(SqlGlobalAggregateTerminal::CountRows),
1248        LoweredSqlAggregateShape::CountField(field) => {
1249            Ok(SqlGlobalAggregateTerminal::CountField(field))
1250        }
1251        LoweredSqlAggregateShape::FieldTarget {
1252            kind: SqlAggregateKind::Sum,
1253            field,
1254        } => Ok(SqlGlobalAggregateTerminal::SumField(field)),
1255        LoweredSqlAggregateShape::FieldTarget {
1256            kind: SqlAggregateKind::Avg,
1257            field,
1258        } => Ok(SqlGlobalAggregateTerminal::AvgField(field)),
1259        LoweredSqlAggregateShape::FieldTarget {
1260            kind: SqlAggregateKind::Min,
1261            field,
1262        } => Ok(SqlGlobalAggregateTerminal::MinField(field)),
1263        LoweredSqlAggregateShape::FieldTarget {
1264            kind: SqlAggregateKind::Max,
1265            field,
1266        } => Ok(SqlGlobalAggregateTerminal::MaxField(field)),
1267        LoweredSqlAggregateShape::FieldTarget {
1268            kind: SqlAggregateKind::Count,
1269            ..
1270        } => Err(SqlLoweringError::unsupported_select_projection()),
1271    }
1272}
1273
1274fn lower_sql_aggregate_shape(
1275    call: SqlAggregateCall,
1276) -> Result<LoweredSqlAggregateShape, SqlLoweringError> {
1277    match (call.kind, call.field) {
1278        (SqlAggregateKind::Count, None) => Ok(LoweredSqlAggregateShape::CountRows),
1279        (SqlAggregateKind::Count, Some(field)) => Ok(LoweredSqlAggregateShape::CountField(field)),
1280        (
1281            kind @ (SqlAggregateKind::Sum
1282            | SqlAggregateKind::Avg
1283            | SqlAggregateKind::Min
1284            | SqlAggregateKind::Max),
1285            Some(field),
1286        ) => Ok(LoweredSqlAggregateShape::FieldTarget { kind, field }),
1287        _ => Err(SqlLoweringError::unsupported_select_projection()),
1288    }
1289}
1290
1291fn grouped_projection_aggregate_calls(
1292    projection: &SqlProjection,
1293    group_by_fields: &[String],
1294) -> Result<Vec<SqlAggregateCall>, SqlLoweringError> {
1295    if group_by_fields.is_empty() {
1296        return Err(SqlLoweringError::unsupported_select_group_by());
1297    }
1298
1299    let SqlProjection::Items(items) = projection else {
1300        return Err(SqlLoweringError::unsupported_select_group_by());
1301    };
1302
1303    let mut projected_group_fields = Vec::<String>::new();
1304    let mut aggregate_calls = Vec::<SqlAggregateCall>::new();
1305    let mut seen_aggregate = false;
1306
1307    for item in items {
1308        match item {
1309            SqlSelectItem::Field(field) => {
1310                // Keep grouped projection deterministic and mappable to grouped
1311                // response contracts: group keys must be declared first.
1312                if seen_aggregate {
1313                    return Err(SqlLoweringError::unsupported_select_group_by());
1314                }
1315                projected_group_fields.push(field.clone());
1316            }
1317            SqlSelectItem::Aggregate(aggregate) => {
1318                seen_aggregate = true;
1319                aggregate_calls.push(aggregate.clone());
1320            }
1321            SqlSelectItem::TextFunction(_) => {
1322                return Err(SqlLoweringError::unsupported_select_group_by());
1323            }
1324        }
1325    }
1326
1327    if aggregate_calls.is_empty() || projected_group_fields.as_slice() != group_by_fields {
1328        return Err(SqlLoweringError::unsupported_select_group_by());
1329    }
1330
1331    Ok(aggregate_calls)
1332}
1333
1334fn lower_aggregate_call(
1335    call: SqlAggregateCall,
1336) -> Result<crate::db::query::builder::AggregateExpr, SqlLoweringError> {
1337    match lower_sql_aggregate_shape(call)? {
1338        LoweredSqlAggregateShape::CountRows => Ok(count()),
1339        LoweredSqlAggregateShape::CountField(field) => Ok(count_by(field)),
1340        LoweredSqlAggregateShape::FieldTarget {
1341            kind: SqlAggregateKind::Sum,
1342            field,
1343        } => Ok(sum(field)),
1344        LoweredSqlAggregateShape::FieldTarget {
1345            kind: SqlAggregateKind::Avg,
1346            field,
1347        } => Ok(avg(field)),
1348        LoweredSqlAggregateShape::FieldTarget {
1349            kind: SqlAggregateKind::Min,
1350            field,
1351        } => Ok(min_by(field)),
1352        LoweredSqlAggregateShape::FieldTarget {
1353            kind: SqlAggregateKind::Max,
1354            field,
1355        } => Ok(max_by(field)),
1356        LoweredSqlAggregateShape::FieldTarget {
1357            kind: SqlAggregateKind::Count,
1358            ..
1359        } => Err(SqlLoweringError::unsupported_select_projection()),
1360    }
1361}
1362
1363fn resolve_having_aggregate_index(
1364    target: &SqlAggregateCall,
1365    grouped_projection_aggregates: &[SqlAggregateCall],
1366) -> Result<usize, SqlLoweringError> {
1367    let mut matched = grouped_projection_aggregates
1368        .iter()
1369        .enumerate()
1370        .filter_map(|(index, aggregate)| (aggregate == target).then_some(index));
1371    let Some(index) = matched.next() else {
1372        return Err(SqlLoweringError::unsupported_select_having());
1373    };
1374    if matched.next().is_some() {
1375        return Err(SqlLoweringError::unsupported_select_having());
1376    }
1377
1378    Ok(index)
1379}
1380
1381fn lower_delete_shape(statement: SqlDeleteStatement) -> LoweredBaseQueryShape {
1382    let SqlDeleteStatement {
1383        predicate,
1384        order_by,
1385        limit,
1386        entity: _,
1387    } = statement;
1388
1389    LoweredBaseQueryShape {
1390        predicate,
1391        order_by,
1392        limit,
1393        offset: None,
1394    }
1395}
1396
1397fn apply_order_terms_structural(
1398    mut query: StructuralQuery,
1399    order_by: Vec<crate::db::sql::parser::SqlOrderTerm>,
1400) -> StructuralQuery {
1401    for term in order_by {
1402        query = match term.direction {
1403            SqlOrderDirection::Asc => query.order_by(term.field),
1404            SqlOrderDirection::Desc => query.order_by_desc(term.field),
1405        };
1406    }
1407
1408    query
1409}
1410
1411fn normalize_having_clauses(
1412    clauses: Vec<SqlHavingClause>,
1413    entity_scope: &[String],
1414) -> Vec<SqlHavingClause> {
1415    clauses
1416        .into_iter()
1417        .map(|clause| SqlHavingClause {
1418            symbol: normalize_having_symbol(clause.symbol, entity_scope),
1419            op: clause.op,
1420            value: clause.value,
1421        })
1422        .collect()
1423}
1424
1425fn normalize_having_symbol(symbol: SqlHavingSymbol, entity_scope: &[String]) -> SqlHavingSymbol {
1426    match symbol {
1427        SqlHavingSymbol::Field(field) => {
1428            SqlHavingSymbol::Field(normalize_identifier_to_scope(field, entity_scope))
1429        }
1430        SqlHavingSymbol::Aggregate(aggregate) => SqlHavingSymbol::Aggregate(
1431            normalize_aggregate_call_identifiers(aggregate, entity_scope),
1432        ),
1433    }
1434}
1435
1436fn normalize_aggregate_call_identifiers(
1437    aggregate: SqlAggregateCall,
1438    entity_scope: &[String],
1439) -> SqlAggregateCall {
1440    SqlAggregateCall {
1441        kind: aggregate.kind,
1442        field: aggregate
1443            .field
1444            .map(|field| normalize_identifier_to_scope(field, entity_scope)),
1445    }
1446}
1447
1448// Build one identifier scope used for reducing SQL-qualified field references
1449// (`entity.field`, `schema.entity.field`) into canonical planner field names.
1450fn sql_entity_scope_candidates(sql_entity: &str, expected_entity: &'static str) -> Vec<String> {
1451    let mut out = Vec::new();
1452    out.push(sql_entity.to_string());
1453    out.push(expected_entity.to_string());
1454
1455    if let Some(last) = identifier_last_segment(sql_entity) {
1456        out.push(last.to_string());
1457    }
1458    if let Some(last) = identifier_last_segment(expected_entity) {
1459        out.push(last.to_string());
1460    }
1461
1462    out
1463}
1464
1465fn normalize_projection_identifiers(
1466    projection: SqlProjection,
1467    entity_scope: &[String],
1468) -> SqlProjection {
1469    match projection {
1470        SqlProjection::All => SqlProjection::All,
1471        SqlProjection::Items(items) => SqlProjection::Items(
1472            items
1473                .into_iter()
1474                .map(|item| match item {
1475                    SqlSelectItem::Field(field) => {
1476                        SqlSelectItem::Field(normalize_identifier(field, entity_scope))
1477                    }
1478                    SqlSelectItem::Aggregate(aggregate) => {
1479                        SqlSelectItem::Aggregate(SqlAggregateCall {
1480                            kind: aggregate.kind,
1481                            field: aggregate
1482                                .field
1483                                .map(|field| normalize_identifier(field, entity_scope)),
1484                        })
1485                    }
1486                    SqlSelectItem::TextFunction(SqlTextFunctionCall {
1487                        function,
1488                        field,
1489                        literal,
1490                        literal2,
1491                        literal3,
1492                    }) => SqlSelectItem::TextFunction(SqlTextFunctionCall {
1493                        function,
1494                        field: normalize_identifier(field, entity_scope),
1495                        literal,
1496                        literal2,
1497                        literal3,
1498                    }),
1499                })
1500                .collect(),
1501        ),
1502    }
1503}
1504
1505fn normalize_order_terms(
1506    terms: Vec<crate::db::sql::parser::SqlOrderTerm>,
1507    entity_scope: &[String],
1508) -> Vec<crate::db::sql::parser::SqlOrderTerm> {
1509    terms
1510        .into_iter()
1511        .map(|term| crate::db::sql::parser::SqlOrderTerm {
1512            field: normalize_order_term_identifier(term.field, entity_scope),
1513            direction: term.direction,
1514        })
1515        .collect()
1516}
1517
1518fn normalize_order_term_identifier(identifier: String, entity_scope: &[String]) -> String {
1519    let Some(expression) = ExpressionOrderTerm::parse(identifier.as_str()) else {
1520        return normalize_identifier(identifier, entity_scope);
1521    };
1522    let normalized_field = normalize_identifier(expression.field().to_string(), entity_scope);
1523
1524    expression.canonical_text_with_field(normalized_field.as_str())
1525}
1526
1527fn normalize_identifier_list(fields: Vec<String>, entity_scope: &[String]) -> Vec<String> {
1528    fields
1529        .into_iter()
1530        .map(|field| normalize_identifier(field, entity_scope))
1531        .collect()
1532}
1533
1534// SQL lowering only adapts identifier qualification (`entity.field` -> `field`)
1535// and delegates predicate-tree traversal ownership to `db::predicate`.
1536fn adapt_predicate_identifiers_to_scope(
1537    predicate: Predicate,
1538    entity_scope: &[String],
1539) -> Predicate {
1540    rewrite_field_identifiers(predicate, |field| normalize_identifier(field, entity_scope))
1541}
1542
1543fn normalize_identifier(identifier: String, entity_scope: &[String]) -> String {
1544    normalize_identifier_to_scope(identifier, entity_scope)
1545}
1546
1547fn ensure_entity_matches_expected(
1548    sql_entity: &str,
1549    expected_entity: &'static str,
1550) -> Result<(), SqlLoweringError> {
1551    if identifiers_tail_match(sql_entity, expected_entity) {
1552        return Ok(());
1553    }
1554
1555    Err(SqlLoweringError::entity_mismatch(
1556        sql_entity,
1557        expected_entity,
1558    ))
1559}