hawdb-sql 0.10.0

An embedded Rust graph database for local data-plane workloads
Documentation
// Copyright 2026 Nowledge
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use super::{
    lower_expression, lower_literal_expr, lower_table_factor, lower_table_name, normalize_ident,
    object_name_parts, ExpressionPosition,
};
use crate::ast::*;
use hawdb_core::{HawDBError, Result};
use sqlparser::ast::{
    Assignment, AssignmentTarget, BinaryOperator, Expr, FromTable, OnConflictAction, OnInsert,
    SelectItem, SetExpr, TableObject,
};

pub(super) fn lower_insert_statement(insert: &sqlparser::ast::Insert) -> Result<SqlStatement> {
    super::reject_unsupported_clauses(
        "INSERT",
        &[
            ("optimizer hint", !insert.optimizer_hints.is_empty()),
            ("OR conflict action", insert.or.is_some()),
            ("IGNORE", insert.ignore),
            ("missing INTO", !insert.into),
            ("table alias", insert.table_alias.is_some()),
            ("OVERWRITE", insert.overwrite),
            ("SET assignments", !insert.assignments.is_empty()),
            ("PARTITION", insert.partitioned.is_some()),
            ("columns after PARTITION", !insert.after_columns.is_empty()),
            ("TABLE keyword", insert.has_table_keyword),
            ("REPLACE INTO", insert.replace_into),
            ("priority", insert.priority.is_some()),
            ("row alias", insert.insert_alias.is_some()),
            ("SETTINGS", insert.settings.is_some()),
            ("FORMAT", insert.format_clause.is_some()),
            ("OUTPUT", insert.output.is_some()),
            ("multi-table type", insert.multi_table_insert_type.is_some()),
            (
                "multi-table INTO",
                !insert.multi_table_into_clauses.is_empty(),
            ),
            (
                "multi-table WHEN",
                !insert.multi_table_when_clauses.is_empty(),
            ),
            ("multi-table ELSE", insert.multi_table_else_clause.is_some()),
        ],
    )?;
    let TableObject::TableName(table) = &insert.table else {
        return Err(HawDBError::Semantic(
            "INSERT table functions are not supported".to_string(),
        ));
    };
    let Some(source) = &insert.source else {
        return Err(HawDBError::Semantic(
            "INSERT requires an explicit VALUES clause".to_string(),
        ));
    };
    if source.with.is_some()
        || source.order_by.is_some()
        || source.limit_clause.is_some()
        || source.fetch.is_some()
        || !source.locks.is_empty()
        || source.for_clause.is_some()
        || source.settings.is_some()
        || source.format_clause.is_some()
        || !source.pipe_operators.is_empty()
    {
        return Err(HawDBError::Semantic(
            "INSERT supports VALUES rows only".to_string(),
        ));
    }
    let SetExpr::Values(values) = source.body.as_ref() else {
        return Err(HawDBError::Semantic(
            "INSERT supports VALUES rows only".to_string(),
        ));
    };
    if values.explicit_row || values.value_keyword {
        return Err(HawDBError::Semantic(
            "INSERT requires the PostgreSQL VALUES form".to_string(),
        ));
    }
    let columns = insert
        .columns
        .iter()
        .map(|name| {
            let parts = object_name_parts(name)?;
            let [column] = parts.as_slice() else {
                return Err(HawDBError::Semantic(
                    "INSERT columns must be unqualified identifiers".to_string(),
                ));
            };
            Ok(column.clone())
        })
        .collect::<Result<Vec<_>>>()?;
    let rows = values
        .rows
        .iter()
        .map(|row| {
            row.iter()
                .map(lower_literal_expr)
                .collect::<Result<Vec<_>>>()
        })
        .collect::<Result<Vec<_>>>()?;
    if rows.is_empty() || rows.iter().any(|row| row.len() != columns.len()) {
        return Err(HawDBError::Semantic(
            "INSERT VALUES rows must match the explicit column list".to_string(),
        ));
    }
    Ok(SqlStatement::Insert(InsertStatement {
        table: lower_table_name(table)?,
        columns,
        rows,
        on_conflict: insert.on.as_ref().map(lower_on_conflict).transpose()?,
        returning: insert
            .returning
            .as_deref()
            .unwrap_or_default()
            .iter()
            .map(|item| match item {
                SelectItem::UnnamedExpr(
                    expr @ (Expr::Identifier(_) | Expr::CompoundIdentifier(_)),
                ) => super::lower_column_expr(expr),
                SelectItem::UnnamedExpr(_)
                | SelectItem::ExprWithAlias { .. }
                | SelectItem::ExprWithAliases { .. }
                | SelectItem::QualifiedWildcard(_, _)
                | SelectItem::Wildcard(_) => Err(HawDBError::Semantic(
                    "INSERT RETURNING supports column references only".to_string(),
                )),
            })
            .collect::<Result<Vec<_>>>()?,
    }))
}

fn lower_on_conflict(on_insert: &OnInsert) -> Result<SqlOnConflict> {
    let OnInsert::OnConflict(conflict) = on_insert else {
        return Err(HawDBError::Semantic(
            "only PostgreSQL ON CONFLICT is supported".to_string(),
        ));
    };
    let Some(sqlparser::ast::ConflictTarget::Columns(columns)) = &conflict.conflict_target else {
        return Err(HawDBError::Semantic(
            "ON CONFLICT requires an explicit column target".to_string(),
        ));
    };
    let action = match &conflict.action {
        OnConflictAction::DoNothing => SqlConflictAction::DoNothing,
        OnConflictAction::DoUpdate(update) => {
            if update.selection.is_some() {
                return Err(HawDBError::Semantic(
                    "ON CONFLICT DO UPDATE WHERE is not supported".to_string(),
                ));
            }
            SqlConflictAction::DoUpdate(lower_assignments(&update.assignments)?)
        }
    };
    Ok(SqlOnConflict {
        columns: columns.iter().map(normalize_ident).collect(),
        action,
    })
}

pub(super) fn lower_update_statement(update: &sqlparser::ast::Update) -> Result<SqlStatement> {
    if !update.optimizer_hints.is_empty()
        || !update.order_by.is_empty()
        || update.output.is_some()
        || update.from.is_some()
        || update.returning.is_some()
        || update.or.is_some()
        || update.limit.is_some()
        || !update.table.joins.is_empty()
    {
        return Err(HawDBError::Semantic(
            "unsupported PostgreSQL UPDATE clause".to_string(),
        ));
    }
    let (table, alias) = lower_table_factor(&update.table.relation)?;
    Ok(SqlStatement::Update(UpdateStatement {
        table,
        alias,
        assignments: lower_assignments(&update.assignments)?,
        selection: update
            .selection
            .as_ref()
            .map(|expr| lower_expression(expr, ExpressionPosition::Predicate))
            .transpose()?,
    }))
}

fn lower_assignments(assignments: &[Assignment]) -> Result<Vec<SqlAssignment>> {
    assignments
        .iter()
        .map(|assignment| {
            let AssignmentTarget::ColumnName(name) = &assignment.target else {
                return Err(HawDBError::Semantic(
                    "tuple assignments are not supported".to_string(),
                ));
            };
            let parts = object_name_parts(name)?;
            let [column] = parts.as_slice() else {
                return Err(HawDBError::Semantic(
                    "assignment targets must be unqualified columns".to_string(),
                ));
            };
            Ok(SqlAssignment {
                column: column.clone(),
                value: lower_assignment_value(&assignment.value)?,
            })
        })
        .collect()
}

fn lower_assignment_value(expr: &Expr) -> Result<SqlAssignmentValue> {
    match expr {
        Expr::Identifier(_) | Expr::CompoundIdentifier(_) => {
            Ok(SqlAssignmentValue::Column(super::lower_column_expr(expr)?))
        }
        Expr::BinaryOp { left, op, right }
            if matches!(op, BinaryOperator::Plus | BinaryOperator::Minus) =>
        {
            let left = lower_arithmetic_operand(left)?;
            let right = lower_arithmetic_operand(right)?;
            match (op, &left, &right) {
                (
                    BinaryOperator::Plus,
                    SqlArithmeticOperand::Column(_),
                    SqlArithmeticOperand::Value(_),
                )
                | (
                    BinaryOperator::Plus,
                    SqlArithmeticOperand::Value(_),
                    SqlArithmeticOperand::Column(_),
                )
                | (
                    BinaryOperator::Minus,
                    SqlArithmeticOperand::Column(_),
                    SqlArithmeticOperand::Value(_),
                ) => {}
                _ => {
                    return Err(HawDBError::Semantic(
                        "UPDATE arithmetic supports column + value, value + column, and column - value"
                            .to_string(),
                    ));
                }
            }
            Ok(SqlAssignmentValue::Arithmetic {
                left,
                operator: match op {
                    BinaryOperator::Plus => SqlArithmeticOperator::Add,
                    BinaryOperator::Minus => SqlArithmeticOperator::Subtract,
                    _ => unreachable!("arithmetic operator was filtered before lowering"),
                },
                right,
            })
        }
        Expr::BinaryOp { .. } => Err(HawDBError::Semantic(
            "UPDATE arithmetic supports column + value, value + column, and column - value"
                .to_string(),
        )),
        _ => lower_literal_expr(expr).map(SqlAssignmentValue::Value),
    }
}

fn lower_arithmetic_operand(expr: &Expr) -> Result<SqlArithmeticOperand> {
    match expr {
        Expr::Identifier(_) | Expr::CompoundIdentifier(_) => Ok(SqlArithmeticOperand::Column(
            super::lower_column_expr(expr)?,
        )),
        _ => lower_literal_expr(expr).map(SqlArithmeticOperand::Value),
    }
}

pub(super) fn lower_delete_statement(delete: &sqlparser::ast::Delete) -> Result<SqlStatement> {
    if !delete.optimizer_hints.is_empty()
        || delete.output.is_some()
        || !delete.tables.is_empty()
        || delete.using.is_some()
        || delete.returning.is_some()
        || !delete.order_by.is_empty()
        || delete.limit.is_some()
    {
        return Err(HawDBError::Semantic(
            "unsupported PostgreSQL DELETE clause".to_string(),
        ));
    }
    let from = match &delete.from {
        FromTable::WithFromKeyword(from) | FromTable::WithoutKeyword(from) => from,
    };
    let [from] = from.as_slice() else {
        return Err(HawDBError::Semantic(
            "DELETE supports exactly one base table".to_string(),
        ));
    };
    if !from.joins.is_empty() {
        return Err(HawDBError::Semantic(
            "DELETE joins are not supported".to_string(),
        ));
    }
    let (table, alias) = lower_table_factor(&from.relation)?;
    Ok(SqlStatement::Delete(DeleteStatement {
        table,
        alias,
        selection: delete
            .selection
            .as_ref()
            .map(|expr| lower_expression(expr, ExpressionPosition::Predicate))
            .transpose()?,
    }))
}