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