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