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