uqa-sql 0.2.1

PostgreSQL-compatible SQL compiler built on libpg_query
Documentation
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//

//! CREATE/ALTER SEQUENCE lowering and option validation.

use super::{
    compile_pg_type_name, extract_string, range_var_name, relation_persistence,
    render_relation_component, NodeEnum, Result, SQLError,
};
use crate::ast::ColumnType;

pub(super) fn compile_create_sequence(
    stmt: &pg_query::protobuf::CreateSeqStmt,
) -> Result<crate::ast::CreateSequence> {
    use crate::ast::{CreateSequence, SequenceDataType};
    let relation = stmt
        .sequence
        .as_ref()
        .ok_or_else(|| SQLError::Internal("CREATE SEQUENCE without name".into()))?;
    let persistence = relation_persistence(relation, "CREATE SEQUENCE")?;
    if stmt.owner_id != 0 || stmt.for_identity {
        return Err(SQLError::Unsupported(
            "CREATE SEQUENCE: identity-owned sequences are not supported".into(),
        ));
    }
    let name = range_var_name(relation);
    let mut start = None;
    let mut increment = 1_i64;
    let mut data_type = SequenceDataType::BigInt;
    let mut min_value = None;
    let mut max_value = None;
    let mut cycle = false;
    let mut cache_size = 1;
    let mut ownership = crate::ast::SequenceOwnership::Unchanged;
    let mut seen = std::collections::BTreeSet::new();
    for opt in &stmt.options {
        let Some(NodeEnum::DefElem(elem)) = opt.node.as_ref() else {
            return Err(SQLError::Internal(
                "CREATE SEQUENCE contains a malformed option".into(),
            ));
        };
        let key = elem.defname.to_ascii_lowercase();
        if !seen.insert(key.clone()) {
            return Err(conflicting_sequence_options());
        }
        match key.as_str() {
            "as" => data_type = compile_sequence_data_type(elem, "CREATE SEQUENCE")?,
            "start" => start = Some(compile_sequence_integer_option(elem, "CREATE SEQUENCE")?),
            "increment" => {
                increment = compile_sequence_integer_option(elem, "CREATE SEQUENCE")?;
            }
            "minvalue" => {
                min_value = elem
                    .arg
                    .as_ref()
                    .map(|_| compile_sequence_integer_option(elem, "CREATE SEQUENCE"))
                    .transpose()?;
            }
            "maxvalue" => {
                max_value = elem
                    .arg
                    .as_ref()
                    .map(|_| compile_sequence_integer_option(elem, "CREATE SEQUENCE"))
                    .transpose()?;
            }
            "cycle" => cycle = compile_sequence_boolean_option(elem, "CREATE SEQUENCE")?,
            "cache" => {
                cache_size = compile_sequence_integer_option(elem, "CREATE SEQUENCE")?;
            }
            "owned_by" => {
                ownership = compile_sequence_ownership(elem, "CREATE SEQUENCE")?;
            }
            other => {
                return Err(SQLError::Unsupported(format!(
                    "CREATE SEQUENCE option `{other}` is not supported"
                )));
            }
        }
    }
    let (type_min, type_max) = data_type.bounds();
    let min_value = min_value.unwrap_or(if increment > 0 { 1 } else { type_min });
    let max_value = max_value.unwrap_or(if increment > 0 { type_max } else { -1 });
    Ok(CreateSequence {
        name,
        if_not_exists: stmt.if_not_exists,
        start: start.unwrap_or(if increment > 0 { min_value } else { max_value }),
        increment,
        persistence,
        data_type,
        min_value: Some(min_value),
        max_value: Some(max_value),
        cycle,
        cache_size,
        ownership,
    })
}

pub(super) fn compile_alter_sequence(
    stmt: &pg_query::protobuf::AlterSeqStmt,
) -> Result<crate::ast::AlterSequence> {
    use crate::ast::{AlterSequence, SequenceBound, SequenceRestart};
    if stmt.for_identity {
        return Err(SQLError::Unsupported(
            "ALTER SEQUENCE: identity-owned sequences are not supported".into(),
        ));
    }
    let name = stmt
        .sequence
        .as_ref()
        .map(range_var_name)
        .ok_or_else(|| SQLError::Internal("ALTER SEQUENCE without name".into()))?;
    let mut alter = AlterSequence {
        name,
        if_exists: stmt.missing_ok,
        ..Default::default()
    };
    let mut seen = std::collections::BTreeSet::new();
    for opt in &stmt.options {
        let Some(NodeEnum::DefElem(elem)) = opt.node.as_ref() else {
            return Err(SQLError::Internal(
                "ALTER SEQUENCE contains a malformed option".into(),
            ));
        };
        let key = elem.defname.to_ascii_lowercase();
        if !seen.insert(key.clone()) {
            return Err(conflicting_sequence_options());
        }
        match key.as_str() {
            "restart" => {
                alter.restart = elem
                    .arg
                    .as_ref()
                    .map(|_| compile_sequence_integer_option(elem, "ALTER SEQUENCE"))
                    .transpose()?
                    .map_or(SequenceRestart::FromStart, SequenceRestart::With);
            }
            "increment" => {
                alter.increment = Some(compile_sequence_integer_option(elem, "ALTER SEQUENCE")?);
            }
            "start" => {
                alter.start = Some(compile_sequence_integer_option(elem, "ALTER SEQUENCE")?);
            }
            "as" => alter.data_type = Some(compile_sequence_data_type(elem, "ALTER SEQUENCE")?),
            "minvalue" => {
                alter.min_value = elem.arg.as_ref().map_or(Ok(SequenceBound::Default), |_| {
                    compile_sequence_integer_option(elem, "ALTER SEQUENCE")
                        .map(SequenceBound::Value)
                })?;
            }
            "maxvalue" => {
                alter.max_value = elem.arg.as_ref().map_or(Ok(SequenceBound::Default), |_| {
                    compile_sequence_integer_option(elem, "ALTER SEQUENCE")
                        .map(SequenceBound::Value)
                })?;
            }
            "cycle" => {
                alter.cycle = Some(compile_sequence_boolean_option(elem, "ALTER SEQUENCE")?);
            }
            "cache" => {
                alter.cache_size = Some(compile_sequence_integer_option(elem, "ALTER SEQUENCE")?);
            }
            "owned_by" => {
                alter.ownership = compile_sequence_ownership(elem, "ALTER SEQUENCE")?;
            }
            other => {
                return Err(SQLError::Unsupported(format!(
                    "ALTER SEQUENCE option `{other}` is not supported"
                )));
            }
        }
    }
    Ok(alter)
}

fn compile_sequence_ownership(
    elem: &pg_query::protobuf::DefElem,
    statement: &str,
) -> Result<crate::ast::SequenceOwnership> {
    let Some(NodeEnum::List(list)) = elem
        .arg
        .as_ref()
        .and_then(|argument| argument.node.as_ref())
    else {
        return Err(SQLError::Internal(format!(
            "{statement} contains a malformed OWNED BY option"
        )));
    };
    let parts = list
        .items
        .iter()
        .map(extract_string)
        .collect::<Result<Vec<_>>>()?;
    match parts.as_slice() {
        [none] if none == "none" => Ok(crate::ast::SequenceOwnership::Unowned),
        [_] | [] => Err(SQLError::Routine {
            sqlstate: "42601".into(),
            message: "invalid OWNED BY option; specify OWNED BY table.column or OWNED BY NONE"
                .into(),
        }),
        [table, column] => Ok(crate::ast::SequenceOwnership::Column {
            table: render_relation_component(table),
            column: column.clone(),
        }),
        [schema, table, column] => Ok(crate::ast::SequenceOwnership::Column {
            table: format!(
                "{}.{}",
                render_relation_component(schema),
                render_relation_component(table)
            ),
            column: column.clone(),
        }),
        _ => Err(SQLError::Unsupported(
            "cross-database references are not implemented: OWNED BY".into(),
        )),
    }
}

fn conflicting_sequence_options() -> SQLError {
    SQLError::Routine {
        sqlstate: "42601".into(),
        message: "conflicting or redundant options".into(),
    }
}

fn compile_sequence_data_type(
    elem: &pg_query::protobuf::DefElem,
    statement: &str,
) -> Result<crate::ast::SequenceDataType> {
    let Some(NodeEnum::TypeName(type_name)) =
        elem.arg.as_deref().and_then(|node| node.node.as_ref())
    else {
        return Err(SQLError::Internal(format!(
            "{statement} contains a malformed AS type"
        )));
    };
    match compile_pg_type_name(type_name, "sequence")? {
        ColumnType::SmallInteger => Ok(crate::ast::SequenceDataType::SmallInt),
        ColumnType::Integer => Ok(crate::ast::SequenceDataType::Integer),
        ColumnType::BigInteger => Ok(crate::ast::SequenceDataType::BigInt),
        _ => Err(SQLError::Routine {
            sqlstate: "22023".into(),
            message: "sequence type must be smallint, integer, or bigint".into(),
        }),
    }
}

fn compile_sequence_boolean_option(
    elem: &pg_query::protobuf::DefElem,
    statement: &str,
) -> Result<bool> {
    match elem.arg.as_deref().and_then(|node| node.node.as_ref()) {
        Some(NodeEnum::Boolean(value)) => Ok(value.boolval),
        Some(NodeEnum::Integer(value)) if matches!(value.ival, 0 | 1) => Ok(value.ival != 0),
        other => Err(SQLError::Internal(format!(
            "{statement} option `{}` has malformed Boolean value {other:?}",
            elem.defname
        ))),
    }
}

pub(super) fn compile_sequence_integer_option(
    elem: &pg_query::protobuf::DefElem,
    statement: &str,
) -> Result<i64> {
    let raw = match elem
        .arg
        .as_ref()
        .and_then(|argument| argument.node.as_ref())
    {
        Some(NodeEnum::Integer(value)) => return Ok(i64::from(value.ival)),
        Some(NodeEnum::Float(value)) => value.fval.as_str(),
        Some(NodeEnum::String(value)) => value.sval.as_str(),
        other => {
            return Err(SQLError::TypeMismatch(format!(
                "{statement} option `{}` expects an integer, got {other:?}",
                elem.defname
            )));
        }
    };
    raw.parse::<i64>().map_err(|_| {
        let integer_syntax = raw
            .strip_prefix(['+', '-'])
            .unwrap_or(raw)
            .bytes()
            .all(|byte| byte.is_ascii_digit());
        SQLError::Routine {
            sqlstate: if integer_syntax { "22003" } else { "22P02" }.into(),
            message: if integer_syntax {
                format!("value \"{raw}\" is out of range for type bigint")
            } else {
                format!("invalid input syntax for type bigint: \"{raw}\"")
            },
        }
    })
}