Skip to main content

hawdb_sql/
parser.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::ast::*;
16use hawdb_core::{HawDBError, Result, Value};
17use sqlparser::ast::Spanned;
18use sqlparser::ast::{
19    BinaryOperator, Distinct, DuplicateTreatment, Expr, FunctionArg, FunctionArgExpr,
20    FunctionArguments, GroupByExpr, Ident, JoinConstraint, JoinOperator, LimitClause, LockClause,
21    LockType, ObjectName, ObjectNamePart, OrderByKind, OrderBySort, SelectItem as ParserSelectItem,
22    SetExpr, Statement as ParserStatement, TableAlias, TableFactor, Value as ParserValue,
23    ValueWithSpan,
24};
25use sqlparser::dialect::PostgreSqlDialect;
26use sqlparser::parser::Parser;
27
28mod branch;
29mod expression_limit;
30mod materialized_view;
31mod mutation;
32mod owned;
33mod schema;
34
35#[cfg(test)]
36mod clause_tests;
37
38#[cfg(test)]
39mod compatibility_tests;
40#[cfg(test)]
41mod numeric_tests;
42
43fn reject_unsupported_clauses(
44    statement: &'static str,
45    clauses: &[(&'static str, bool)],
46) -> Result<()> {
47    if let Some((clause, _)) = clauses.iter().find(|(_, present)| *present) {
48        return Err(HawDBError::Semantic(format!(
49            "unsupported PostgreSQL {statement} clause: {clause}"
50        )));
51    }
52    Ok(())
53}
54
55use mutation::{lower_delete_statement, lower_insert_statement, lower_update_statement};
56use schema::{
57    lower_alter_table_statement, lower_create_index_statement, lower_create_table_statement,
58};
59
60pub fn parse_postgres_sql(input: &str) -> Result<SqlStatement> {
61    if let Some(statement) = branch::parse_branch_statement(input)? {
62        return Ok(statement);
63    }
64    let token = hawdb_sql_syntax::first_token(input)
65        .map_err(|error| HawDBError::Parse(error.to_string()))?;
66    if token.is_keyword(input, "SELECT") || token.is_keyword(input, "EXPLAIN") {
67        return owned::parse(input);
68    }
69    let dialect = PostgreSqlDialect {};
70    let statements = Parser::parse_sql(&dialect, input)
71        .map_err(|error| HawDBError::Parse(format!("failed to parse PostgreSQL SQL: {error}")))?;
72    let [statement] = statements.as_slice() else {
73        return Err(HawDBError::Parse(
74            "expected exactly one PostgreSQL SQL statement".to_string(),
75        ));
76    };
77    schema::validate_vector_type_syntax(input, statement)?;
78    lower_statement(statement)
79}
80
81fn lower_statement(statement: &ParserStatement) -> Result<SqlStatement> {
82    match statement {
83        ParserStatement::Explain {
84            describe_alias,
85            analyze,
86            verbose,
87            query_plan,
88            estimate,
89            statement,
90            format,
91            options,
92        } => {
93            if !matches!(describe_alias, sqlparser::ast::DescribeAlias::Explain)
94                || *verbose
95                || *query_plan
96                || *estimate
97                || format.is_some()
98                || options.as_ref().is_some_and(|options| !options.is_empty())
99            {
100                return Err(HawDBError::Semantic(
101                    "unsupported PostgreSQL EXPLAIN option".to_string(),
102                ));
103            }
104            let statement = lower_statement(statement)?;
105            if !matches!(statement, SqlStatement::Select(_)) {
106                return Err(HawDBError::Semantic(
107                    "EXPLAIN only supports relational SELECT".to_string(),
108                ));
109            }
110            Ok(SqlStatement::Explain(SqlExplainStatement {
111                analyze: *analyze,
112                statement: Box::new(statement),
113            }))
114        }
115        ParserStatement::Query(query) => lower_select_statement(query),
116        ParserStatement::Insert(insert) => lower_insert_statement(insert),
117        ParserStatement::Update(update) => lower_update_statement(update),
118        ParserStatement::Delete(delete) => lower_delete_statement(delete),
119        ParserStatement::CreateTable(create) => lower_create_table_statement(create),
120        ParserStatement::CreateIndex(create) => lower_create_index_statement(create),
121        ParserStatement::CreateView(create) => materialized_view::lower_create(create),
122        ParserStatement::Drop {
123            object_type: sqlparser::ast::ObjectType::MaterializedView,
124            ..
125        } => materialized_view::lower_drop(statement),
126        ParserStatement::AlterTable(alter) => lower_alter_table_statement(alter),
127        _ => Err(HawDBError::Semantic(
128            "unsupported PostgreSQL statement kind".to_string(),
129        )),
130    }
131}
132
133fn lower_select_statement(query: &sqlparser::ast::Query) -> Result<SqlStatement> {
134    if query.with.is_some()
135        || query.fetch.is_some()
136        || query.for_clause.is_some()
137        || query.settings.is_some()
138        || query.format_clause.is_some()
139        || !query.pipe_operators.is_empty()
140    {
141        return Err(HawDBError::Semantic(
142            "unsupported PostgreSQL SELECT clause".to_string(),
143        ));
144    }
145    let SetExpr::Select(select) = query.body.as_ref() else {
146        return Err(HawDBError::Semantic(
147            "set operations and nested queries are not supported".to_string(),
148        ));
149    };
150    if select.top.is_some()
151        || select.into.is_some()
152        || select.prewhere.is_some()
153        || !select.lateral_views.is_empty()
154        || !select.connect_by.is_empty()
155        || !select.cluster_by.is_empty()
156        || !select.distribute_by.is_empty()
157        || !select.sort_by.is_empty()
158        || !select.named_window.is_empty()
159        || select.qualify.is_some()
160        || select.value_table_mode.is_some()
161    {
162        return Err(HawDBError::Semantic(
163            "unsupported PostgreSQL SELECT feature".to_string(),
164        ));
165    }
166    let projection = lower_projection(&select.projection)?;
167    let distinct = lower_distinct(select.distinct.as_ref())?;
168
169    let vector_index = select
170        .from
171        .first()
172        .map(|from| vector_index::lower_source(&from.relation))
173        .transpose()?
174        .flatten();
175    let (from_name, from_alias, joins) = if select.from.is_empty() {
176        (None, None, Vec::new())
177    } else {
178        let from = &select.from[0];
179        let (from_name, from_alias) = if vector_index.is_some() {
180            let TableFactor::Table { name, alias, .. } = &from.relation else {
181                unreachable!()
182            };
183            (lower_table_name(name)?, lower_table_alias(alias.as_ref())?)
184        } else {
185            lower_table_factor(&from.relation)?
186        };
187        let mut joins = from
188            .joins
189            .iter()
190            .map(|join| lower_join(join, 0))
191            .collect::<Result<Vec<_>>>()?;
192        for from in &select.from[1..] {
193            let on_scope_start = joins.len() + 1;
194            let (table, alias) = lower_table_factor(&from.relation)?;
195            joins.push(SqlJoin {
196                kind: SqlJoinKind::Inner,
197                table,
198                alias,
199                on: crate::Expr::value(SqlValue::Literal(Value::Bool(true))),
200                on_scope_start,
201            });
202            for join in &from.joins {
203                joins.push(lower_join(join, on_scope_start)?);
204            }
205        }
206        (Some(from_name), from_alias, joins)
207    };
208
209    let statement = SelectStatement {
210        vector_index: vector_index.map(Box::new),
211        projection,
212        distinct,
213        from: from_name,
214        from_alias,
215        joins,
216        selection: select
217            .selection
218            .as_ref()
219            .map(|expr| lower_expression(expr, ExpressionPosition::Predicate))
220            .transpose()?,
221        group_by: lower_group_by(&select.group_by)?,
222        having: select
223            .having
224            .as_ref()
225            .map(|expression| lower_expression(expression, ExpressionPosition::Having))
226            .transpose()?,
227        order_by: lower_order_by(query.order_by.as_ref())?,
228        limit: lower_limit(query.limit_clause.as_ref())?,
229        offset: lower_offset(query.limit_clause.as_ref())?,
230        lock_strength: lower_lock_strength(&query.locks)?,
231    };
232
233    if statement.from.is_none()
234        && (statement.selection.is_some()
235            || !statement.group_by.is_empty()
236            || statement.having.is_some()
237            || !statement.order_by.is_empty()
238            || statement.limit.is_some()
239            || statement.offset.is_some()
240            || statement.lock_strength.is_some())
241    {
242        return Err(HawDBError::Semantic(
243            "PostgreSQL SELECT without FROM does not support WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET, or locking clauses".to_string(),
244        ));
245    }
246
247    Ok(SqlStatement::Select(statement))
248}
249
250fn lower_lock_strength(locks: &[LockClause]) -> Result<Option<SqlLockStrength>> {
251    let ([] | [_]) = locks else {
252        return Err(HawDBError::Semantic(
253            "PostgreSQL SELECT supports at most one locking clause".to_string(),
254        ));
255    };
256    let Some(lock) = locks.first() else {
257        return Ok(None);
258    };
259    if lock.of.is_some() || lock.nonblock.is_some() {
260        return Err(HawDBError::Semantic(
261            "FOR UPDATE/SHARE OF, NOWAIT, and SKIP LOCKED are not supported".to_string(),
262        ));
263    }
264    Ok(Some(match lock.lock_type {
265        LockType::Share => SqlLockStrength::Share,
266        LockType::Update => SqlLockStrength::Update,
267    }))
268}
269
270fn lower_projection(items: &[ParserSelectItem]) -> Result<Vec<SelectProjection>> {
271    items
272        .iter()
273        .map(|item| match item {
274            ParserSelectItem::Wildcard(_) => Ok(SelectProjection::Wildcard),
275            ParserSelectItem::UnnamedExpr(expr) => lower_projection_expression(expr, None),
276            ParserSelectItem::ExprWithAlias { expr, alias } => {
277                lower_projection_expression(expr, Some(normalize_ident(alias)))
278            }
279            ParserSelectItem::QualifiedWildcard(_, _) => Err(HawDBError::Semantic(
280                "qualified wildcards are not supported".to_string(),
281            )),
282            ParserSelectItem::ExprWithAliases { .. } => Err(HawDBError::Semantic(
283                "multiple projection aliases are not supported".to_string(),
284            )),
285        })
286        .collect()
287}
288
289fn lower_projection_expression(expr: &Expr, alias: Option<String>) -> Result<SelectProjection> {
290    Ok(SelectProjection::Expression {
291        expression: lower_expression(expr, ExpressionPosition::Scalar)?,
292        alias,
293    })
294}
295
296fn lower_order_by(order_by: Option<&sqlparser::ast::OrderBy>) -> Result<Vec<SqlOrderItem>> {
297    let Some(order_by) = order_by else {
298        return Ok(Vec::new());
299    };
300    let OrderByKind::Expressions(expressions) = &order_by.kind else {
301        return Err(HawDBError::Semantic(
302            "ORDER BY ALL is not supported".to_string(),
303        ));
304    };
305    expressions
306        .iter()
307        .map(|item| {
308            Ok(SqlOrderItem {
309                expression: lower_expression(&item.expr, ExpressionPosition::Column)?,
310                direction: lower_order_direction(item.options.sort.as_ref())?,
311                nulls: match item.options.nulls_first {
312                    Some(true) => SqlNullOrder::First,
313                    Some(false) => SqlNullOrder::Last,
314                    None => SqlNullOrder::DialectDefault,
315                },
316            })
317        })
318        .collect()
319}
320
321fn lower_order_direction(sort: Option<&OrderBySort>) -> Result<SqlOrderDirection> {
322    match sort {
323        Some(OrderBySort::Desc) => Ok(SqlOrderDirection::Desc),
324        Some(OrderBySort::Asc) | None => Ok(SqlOrderDirection::Asc),
325        Some(OrderBySort::Using(_)) => Err(HawDBError::Semantic(
326            "ORDER BY USING is not supported".to_string(),
327        )),
328    }
329}
330
331fn lower_limit(limit_clause: Option<&LimitClause>) -> Result<Option<SqlBound>> {
332    let Some(limit_clause) = limit_clause else {
333        return Ok(None);
334    };
335    match limit_clause {
336        LimitClause::LimitOffset { limit, .. } => limit
337            .as_ref()
338            .map(lower_nonnegative_integer_expr)
339            .transpose(),
340        LimitClause::OffsetCommaLimit { limit, .. } => {
341            Ok(Some(lower_nonnegative_integer_expr(limit)?))
342        }
343    }
344}
345
346fn lower_offset(limit_clause: Option<&LimitClause>) -> Result<Option<SqlBound>> {
347    let Some(limit_clause) = limit_clause else {
348        return Ok(None);
349    };
350    match limit_clause {
351        LimitClause::LimitOffset { offset, .. } => offset
352            .as_ref()
353            .map(|offset| lower_nonnegative_integer_expr(&offset.value))
354            .transpose(),
355        LimitClause::OffsetCommaLimit { offset, .. } => {
356            Ok(Some(lower_nonnegative_integer_expr(offset)?))
357        }
358    }
359}
360
361#[derive(Clone, Copy)]
362pub(super) enum ExpressionPosition {
363    Predicate,
364    Having,
365    HavingScalar,
366    PredicateScalar,
367    Scalar,
368    Column,
369    Value,
370}
371
372// These position checks preserve the existing language surface independently of
373// the shared representation. New expression shapes require separate semantics.
374pub(super) fn lower_expression(expr: &Expr, position: ExpressionPosition) -> Result<crate::Expr> {
375    expression_limit::validate(expr, 0)?;
376    use ExpressionPosition::{
377        Column, Having, HavingScalar, Predicate, PredicateScalar, Scalar, Value,
378    };
379    let having = matches!(position, Having | HavingScalar);
380    let operand = if having {
381        HavingScalar
382    } else {
383        PredicateScalar
384    };
385    let lower = |expr: &Expr, position| lower_expression(expr, position).map(Box::new);
386    let kind = match position {
387        Column => ExprKind::Column(lower_column_expr(expr)?),
388        Value => ExprKind::Value(lower_literal_expr(expr)?),
389        Scalar | HavingScalar | PredicateScalar => match expr {
390            Expr::Nested(inner) => lower_expression(inner, position)?.kind,
391            Expr::Identifier(_) | Expr::CompoundIdentifier(_) => {
392                ExprKind::Column(lower_column_expr(expr)?)
393            }
394            Expr::Value(_) => ExprKind::Value(lower_literal_expr(expr)?),
395            Expr::UnaryOp {
396                op: sqlparser::ast::UnaryOperator::Minus,
397                expr: inner,
398            } => {
399                if let Expr::Value(value) = inner.as_ref()
400                    && let ParserValue::Number(raw, _) = &value.value
401                {
402                    ExprKind::Value(SqlValue::Literal(lower_number(&format!("-{raw}"))?))
403                } else {
404                    ExprKind::Negate(lower(inner, position)?)
405                }
406            }
407            Expr::UnaryOp {
408                op: sqlparser::ast::UnaryOperator::Plus,
409                expr: inner,
410            } => lower_expression(inner, position)?.kind,
411            Expr::BinaryOp { left, op, right } => ExprKind::Arithmetic {
412                left: lower(left, position)?,
413                op: lower_numeric_operator(op)?,
414                right: lower(right, position)?,
415            },
416            Expr::Cast {
417                kind,
418                expr: inner,
419                data_type,
420                format,
421            } => {
422                if format.is_some()
423                    || !matches!(
424                        kind,
425                        sqlparser::ast::CastKind::Cast | sqlparser::ast::CastKind::DoubleColon
426                    )
427                {
428                    return Err(HawDBError::Semantic(
429                        "only CAST and :: numeric conversions are supported".into(),
430                    ));
431                }
432                let target = match schema::lower_data_type(data_type)? {
433                    SqlDataType::BigInt => hawdb_core::numeric::NumericType::BigInt,
434                    SqlDataType::DoublePrecision => {
435                        hawdb_core::numeric::NumericType::DoublePrecision
436                    }
437                    SqlDataType::Decimal(kind) => hawdb_core::numeric::NumericType::Decimal(kind),
438                    _ => {
439                        return Err(HawDBError::Semantic(
440                            "CAST requires a numeric target type".into(),
441                        ))
442                    }
443                };
444                ExprKind::Cast {
445                    expression: lower(inner, position)?,
446                    target,
447                }
448            }
449            Expr::Function(function) if !matches!(position, PredicateScalar) => {
450                lower_function_expression(function, position)?
451            }
452            _ => {
453                return Err(HawDBError::Semantic(format!(
454                    "unsupported PostgreSQL projection expression {expr}"
455                )))
456            }
457        },
458        Predicate | Having => match expr {
459            Expr::BinaryOp { left, op, right } => match op {
460                BinaryOperator::And => {
461                    ExprKind::And(lower(left, position)?, lower(right, position)?)
462                }
463                BinaryOperator::Or => ExprKind::Or(lower(left, position)?, lower(right, position)?),
464                BinaryOperator::Eq
465                | BinaryOperator::NotEq
466                | BinaryOperator::Lt
467                | BinaryOperator::LtEq
468                | BinaryOperator::Gt
469                | BinaryOperator::GtEq => ExprKind::Compare {
470                    left: lower(left, operand)?,
471                    op: lower_comparison_op(op),
472                    right: lower(right, operand)?,
473                },
474                _ => {
475                    return Err(HawDBError::Semantic(format!(
476                        "unsupported PostgreSQL predicate operator {op}"
477                    )))
478                }
479            },
480            Expr::Nested(inner) => lower_expression(inner, position)?.kind,
481            Expr::UnaryOp {
482                op: sqlparser::ast::UnaryOperator::Not,
483                expr,
484            } => ExprKind::Not(lower(expr, position)?),
485            Expr::InList {
486                expr,
487                list,
488                negated,
489            } => ExprKind::InList {
490                left: lower(expr, operand)?,
491                values: list
492                    .iter()
493                    .map(|value| lower_expression(value, operand))
494                    .collect::<Result<_>>()?,
495                negated: *negated,
496            },
497            Expr::Like {
498                negated,
499                any,
500                expr,
501                pattern,
502                escape_char,
503            } => lower_like_expression(
504                expr,
505                pattern,
506                *negated,
507                *any,
508                escape_char.as_deref(),
509                false,
510                having,
511            )?,
512            Expr::ILike {
513                negated,
514                any,
515                expr,
516                pattern,
517                escape_char,
518            } => lower_like_expression(
519                expr,
520                pattern,
521                *negated,
522                *any,
523                escape_char.as_deref(),
524                true,
525                having,
526            )?,
527            Expr::IsNull(expr) => ExprKind::IsNull {
528                expression: lower(expr, operand)?,
529                negated: false,
530            },
531            Expr::IsNotNull(expr) => ExprKind::IsNull {
532                expression: lower(expr, operand)?,
533                negated: true,
534            },
535            _ if having => lower_expression(expr, HavingScalar)?.kind,
536            _ => {
537                return Err(HawDBError::Semantic(format!(
538                    "unsupported PostgreSQL predicate expression {expr}"
539                )))
540            }
541        },
542    };
543    let span = expr.span();
544    Ok(crate::Expr {
545        kind,
546        span: SqlSourceSpan {
547            start: SqlSourceLocation {
548                line: span.start.line,
549                column: span.start.column,
550            },
551            end: SqlSourceLocation {
552                line: span.end.line,
553                column: span.end.column,
554            },
555        },
556    })
557}
558
559fn lower_like_expression(
560    expr: &Expr,
561    pattern: &Expr,
562    negated: bool,
563    any: bool,
564    escape_char: Option<&Expr>,
565    case_insensitive: bool,
566    having: bool,
567) -> Result<ExprKind> {
568    if any {
569        return Err(HawDBError::Semantic(
570            "PostgreSQL LIKE ANY is not supported".to_string(),
571        ));
572    }
573    Ok(ExprKind::Like {
574        left: Box::new(lower_expression(
575            expr,
576            if having {
577                ExpressionPosition::HavingScalar
578            } else {
579                ExpressionPosition::Column
580            },
581        )?),
582        pattern: Box::new(lower_expression(
583            pattern,
584            if having {
585                ExpressionPosition::HavingScalar
586            } else {
587                ExpressionPosition::Value
588            },
589        )?),
590        case_insensitive,
591        negated,
592        escape: lower_like_escape(escape_char)?,
593    })
594}
595
596fn lower_like_escape(escape_char: Option<&Expr>) -> Result<SqlLikeEscape> {
597    let Some(escape_char) = escape_char else {
598        return Ok(SqlLikeEscape::Character('\\'));
599    };
600    let escape = match escape_char {
601        Expr::Value(value) => value.value.clone().into_string(),
602        _ => None,
603    };
604    let Some(escape) = escape else {
605        return Err(HawDBError::Semantic(
606            "LIKE ESCAPE must be a string literal".to_string(),
607        ));
608    };
609    let mut characters = escape.chars();
610    let Some(character) = characters.next() else {
611        return Ok(SqlLikeEscape::Disabled);
612    };
613    if characters.next().is_some() {
614        return Err(HawDBError::Semantic(
615            "LIKE ESCAPE must contain at most one Unicode scalar".to_string(),
616        ));
617    }
618    Ok(SqlLikeEscape::Character(character))
619}
620
621fn lower_distinct(distinct: Option<&Distinct>) -> Result<bool> {
622    match distinct {
623        None | Some(Distinct::All) => Ok(false),
624        Some(Distinct::Distinct) => Ok(true),
625        Some(Distinct::On(_)) => Err(HawDBError::Semantic(
626            "PostgreSQL DISTINCT ON is not supported".to_string(),
627        )),
628    }
629}
630
631fn lower_group_by(group_by: &GroupByExpr) -> Result<Vec<SqlColumnRef>> {
632    match group_by {
633        GroupByExpr::Expressions(expressions, modifiers) if modifiers.is_empty() => expressions
634            .iter()
635            .map(lower_column_expr)
636            .collect::<Result<Vec<_>>>(),
637        GroupByExpr::Expressions(_, _) | GroupByExpr::All(_) => Err(HawDBError::Semantic(
638            "PostgreSQL GROUP BY modifiers and GROUP BY ALL are not supported".to_string(),
639        )),
640    }
641}
642
643mod vector_index;
644
645pub(super) fn lower_table_factor(table: &TableFactor) -> Result<(SqlTableName, Option<String>)> {
646    let TableFactor::Table {
647        name,
648        alias,
649        args: None,
650        ..
651    } = table
652    else {
653        return Err(HawDBError::Semantic(
654            "PostgreSQL relational SQL supports base tables only".to_string(),
655        ));
656    };
657    Ok((lower_table_name(name)?, lower_table_alias(alias.as_ref())?))
658}
659
660fn lower_table_alias(alias: Option<&TableAlias>) -> Result<Option<String>> {
661    let Some(alias) = alias else {
662        return Ok(None);
663    };
664    if !alias.columns.is_empty() {
665        return Err(HawDBError::Semantic(
666            "PostgreSQL table column aliases are not supported".to_string(),
667        ));
668    }
669    if alias.at.is_some() {
670        return Err(HawDBError::Semantic(
671            "PostgreSQL table AT aliases are not supported".to_string(),
672        ));
673    }
674    Ok(Some(normalize_ident(&alias.name)))
675}
676
677fn lower_join(join: &sqlparser::ast::Join, on_scope_start: usize) -> Result<SqlJoin> {
678    let (table, alias) = lower_table_factor(&join.relation)?;
679    if matches!(
680        join.join_operator,
681        JoinOperator::CrossJoin(JoinConstraint::None)
682    ) {
683        return Ok(SqlJoin {
684            kind: SqlJoinKind::Inner,
685            table,
686            alias,
687            on: crate::Expr::value(SqlValue::Literal(Value::Bool(true))),
688            on_scope_start,
689        });
690    }
691    let (kind, constraint) = match &join.join_operator {
692        JoinOperator::Join(constraint) | JoinOperator::Inner(constraint) => {
693            (SqlJoinKind::Inner, constraint)
694        }
695        JoinOperator::Left(constraint) | JoinOperator::LeftOuter(constraint) => {
696            (SqlJoinKind::Left, constraint)
697        }
698        other => {
699            return Err(HawDBError::Semantic(format!(
700                "unsupported PostgreSQL join operator {other:?}"
701            )));
702        }
703    };
704    let JoinConstraint::On(on) = constraint else {
705        return Err(HawDBError::Semantic(
706            "PostgreSQL joins require an ON predicate".to_string(),
707        ));
708    };
709    Ok(SqlJoin {
710        kind,
711        table,
712        alias,
713        on: lower_expression(on, ExpressionPosition::Predicate)?,
714        on_scope_start,
715    })
716}
717
718fn lower_function_expression(
719    function: &sqlparser::ast::Function,
720    argument_position: ExpressionPosition,
721) -> Result<ExprKind> {
722    if function.uses_odbc_syntax
723        || !matches!(function.parameters, FunctionArguments::None)
724        || function.null_treatment.is_some()
725        || function.over.is_some()
726        || !function.within_group.is_empty()
727    {
728        return Err(HawDBError::Semantic(
729            "unsupported PostgreSQL function clause".to_string(),
730        ));
731    }
732    let name_parts = object_name_parts(&function.name)?;
733    let [name] = name_parts.as_slice() else {
734        return Err(HawDBError::Semantic(
735            "qualified PostgreSQL function names are not supported".to_string(),
736        ));
737    };
738    let FunctionArguments::List(arguments) = &function.args else {
739        return Err(HawDBError::Semantic(
740            "PostgreSQL functions require an argument list".to_string(),
741        ));
742    };
743    if !arguments.clauses.is_empty() {
744        return Err(HawDBError::Semantic(
745            "PostgreSQL function argument clauses are not supported".to_string(),
746        ));
747    }
748    let distinct = match arguments.duplicate_treatment {
749        None | Some(DuplicateTreatment::All) => false,
750        Some(DuplicateTreatment::Distinct) => true,
751    };
752    let arguments = arguments
753        .args
754        .iter()
755        .map(|argument| match argument {
756            FunctionArg::Unnamed(FunctionArgExpr::Expr(expr)) => {
757                lower_expression(expr, argument_position).map(SqlFunctionArgument::Expression)
758            }
759            FunctionArg::Unnamed(FunctionArgExpr::Wildcard) => Ok(SqlFunctionArgument::Wildcard),
760            _ => Err(HawDBError::Semantic(
761                "named and qualified-wildcard PostgreSQL function arguments are not supported"
762                    .to_string(),
763            )),
764        })
765        .collect::<Result<Vec<_>>>()?;
766    let filter = function
767        .filter
768        .as_deref()
769        .map(|expr| lower_expression(expr, ExpressionPosition::Predicate).map(Box::new))
770        .transpose()?;
771    if hawdb_core::VectorDistance::from_function_name(name).is_some() {
772        if distinct
773            || filter.is_some()
774            || arguments.len() != 2
775            || arguments
776                .iter()
777                .any(|argument| matches!(argument, SqlFunctionArgument::Wildcard))
778        {
779            return Err(HawDBError::Semantic(
780                "vector distance requires exactly two scalar arguments without DISTINCT or FILTER"
781                    .into(),
782            ));
783        }
784        return Ok(ExprKind::Function {
785            name: name.clone(),
786            arguments,
787            distinct,
788            filter,
789        });
790    }
791    match name.as_str() {
792        "count" | "sum" => Ok(ExprKind::Function {
793            name: name.clone(),
794            arguments,
795            distinct,
796            filter,
797        }),
798        "min" | "max" | "coalesce" | "octet_length" | "uuidv7" | "version" if filter.is_none() => {
799            Ok(ExprKind::Function {
800                name: name.clone(),
801                arguments,
802                distinct,
803                filter,
804            })
805        }
806        "min" | "max" | "coalesce" | "octet_length" | "uuidv7" | "version" => {
807            Err(HawDBError::Semantic(
808                "FILTER is supported only for COUNT and SUM aggregates".to_string(),
809            ))
810        }
811        _ => Err(HawDBError::Semantic(format!(
812            "unsupported PostgreSQL function {name}"
813        ))),
814    }
815}
816
817fn lower_comparison_op(op: &BinaryOperator) -> SqlComparisonOp {
818    match op {
819        BinaryOperator::Eq => SqlComparisonOp::Eq,
820        BinaryOperator::NotEq => SqlComparisonOp::NotEq,
821        BinaryOperator::Lt => SqlComparisonOp::Lt,
822        BinaryOperator::LtEq => SqlComparisonOp::Lte,
823        BinaryOperator::Gt => SqlComparisonOp::Gt,
824        BinaryOperator::GtEq => SqlComparisonOp::Gte,
825        _ => unreachable!("checked by caller"),
826    }
827}
828
829pub(super) fn lower_table_name(name: &ObjectName) -> Result<SqlTableName> {
830    let parts = object_name_parts(name)?;
831    match parts.as_slice() {
832        [name] => Ok(SqlTableName {
833            schema: None,
834            name: name.clone(),
835        }),
836        [schema, name] => Ok(SqlTableName {
837            schema: Some(schema.clone()),
838            name: name.clone(),
839        }),
840        _ => Err(HawDBError::Semantic(
841            "PostgreSQL SELECT currently supports one- or two-part table names".to_string(),
842        )),
843    }
844}
845
846pub(super) fn lower_column_expr(expr: &Expr) -> Result<SqlColumnRef> {
847    match expr {
848        Expr::Identifier(ident) => Ok(SqlColumnRef {
849            qualifier: None,
850            name: normalize_ident(ident),
851        }),
852        Expr::CompoundIdentifier(parts) => match parts.as_slice() {
853            [qualifier, name] => Ok(SqlColumnRef {
854                qualifier: Some(normalize_ident(qualifier)),
855                name: normalize_ident(name),
856            }),
857            _ => Err(HawDBError::Semantic(
858                "PostgreSQL SELECT currently supports one- or two-part column names".to_string(),
859            )),
860        },
861        _ => Err(HawDBError::Semantic(format!(
862            "expected a column reference, got {expr}"
863        ))),
864    }
865}
866
867pub(super) fn lower_literal_expr(expr: &Expr) -> Result<SqlValue> {
868    match expr {
869        Expr::Value(value) => lower_value(value),
870        Expr::Nested(inner) => lower_literal_expr(inner),
871        _ => {
872            let expression = lower_expression(expr, ExpressionPosition::Scalar)?;
873            if let ExprKind::Value(value) = expression.kind {
874                return Ok(value);
875            }
876            let mut constant_numeric = true;
877            expression.visit(&mut |child| {
878                constant_numeric &= matches!(
879                    child.kind,
880                    ExprKind::Value(_)
881                        | ExprKind::Arithmetic { .. }
882                        | ExprKind::Negate(_)
883                        | ExprKind::Cast { .. }
884                );
885            });
886            if !constant_numeric {
887                return Err(HawDBError::Semantic(
888                    "expected a constant numeric expression".into(),
889                ));
890            }
891            Ok(SqlValue::Expression(Box::new(expression)))
892        }
893    }
894}
895
896fn lower_numeric_operator(op: &BinaryOperator) -> Result<hawdb_core::numeric::NumericBinaryOp> {
897    use hawdb_core::numeric::NumericBinaryOp;
898    match op {
899        BinaryOperator::Plus => Ok(NumericBinaryOp::Add),
900        BinaryOperator::Minus => Ok(NumericBinaryOp::Subtract),
901        BinaryOperator::Multiply => Ok(NumericBinaryOp::Multiply),
902        BinaryOperator::Divide => Ok(NumericBinaryOp::Divide),
903        BinaryOperator::Modulo => Ok(NumericBinaryOp::Remainder),
904        _ => Err(HawDBError::Semantic(format!(
905            "unsupported numeric operator {op}"
906        ))),
907    }
908}
909
910fn lower_value(value: &ValueWithSpan) -> Result<SqlValue> {
911    match &value.value {
912        ParserValue::Boolean(value) => Ok(SqlValue::Literal(Value::Bool(*value))),
913        ParserValue::Null => Ok(SqlValue::Literal(Value::Null)),
914        ParserValue::Number(raw, _) => lower_number(raw).map(SqlValue::Literal),
915        ParserValue::SingleQuotedString(value)
916        | ParserValue::DoubleQuotedString(value)
917        | ParserValue::TripleSingleQuotedString(value)
918        | ParserValue::TripleDoubleQuotedString(value)
919        | ParserValue::EscapedStringLiteral(value)
920        | ParserValue::UnicodeStringLiteral(value) => {
921            Ok(SqlValue::Literal(Value::String(value.clone())))
922        }
923        ParserValue::Placeholder(raw) => Ok(SqlValue::Parameter(postgres_parameter_position(raw)?)),
924        _ => Err(HawDBError::Semantic(format!(
925            "unsupported PostgreSQL literal {value}"
926        ))),
927    }
928}
929
930fn lower_number(raw: &str) -> Result<Value> {
931    if !raw.contains(['.', 'e', 'E'])
932        && let Ok(value) = raw.parse::<i64>()
933    {
934        return Ok(Value::Int(value));
935    }
936    raw.parse::<hawdb_core::Decimal>()
937        .map(Value::Decimal)
938        .map_err(|error| {
939            HawDBError::Semantic(format!("invalid exact PostgreSQL number {raw}: {error}"))
940        })
941}
942
943fn lower_nonnegative_integer_expr(expr: &Expr) -> Result<SqlBound> {
944    let value = lower_literal_expr(expr)?;
945    match value {
946        SqlValue::Literal(Value::Int(value)) if value >= 0 => Ok(SqlBound::Literal(value as u64)),
947        SqlValue::Parameter(position) => Ok(SqlBound::Parameter(position)),
948        _ => Err(HawDBError::Semantic(
949            "LIMIT/OFFSET must be non-negative integers or PostgreSQL parameters".to_string(),
950        )),
951    }
952}
953
954fn postgres_parameter_position(raw: &str) -> Result<usize> {
955    let Some(raw) = raw.strip_prefix('$') else {
956        return Err(HawDBError::Semantic(
957            "PostgreSQL parameters must use one-based $n syntax".to_string(),
958        ));
959    };
960    let position = raw.parse::<usize>().map_err(|_| {
961        HawDBError::Semantic("PostgreSQL parameters must use one-based $n syntax".to_string())
962    })?;
963    if position == 0 {
964        return Err(HawDBError::Semantic(
965            "PostgreSQL parameters are one-based".to_string(),
966        ));
967    }
968    Ok(position)
969}
970
971pub(super) fn object_name_parts(name: &ObjectName) -> Result<Vec<String>> {
972    name.0
973        .iter()
974        .map(|part| match part {
975            ObjectNamePart::Identifier(ident) => Ok(normalize_ident(ident)),
976            _ => Err(HawDBError::Semantic(
977                "object name functions are not supported".to_string(),
978            )),
979        })
980        .collect()
981}
982
983pub(super) fn normalize_ident(ident: &Ident) -> String {
984    if ident.quote_style.is_some() {
985        ident.value.clone()
986    } else {
987        ident.value.to_ascii_lowercase()
988    }
989}