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