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