uqa-sql 0.5.4

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

//! CREATE TABLE column validation and CREATE TABLE AS result declarations.
use crate::{
    ast::{ColumnDef, CreateTable},
    type_resolution::FunctionTypeResolver,
    ColumnType, SQLError,
};
use std::collections::BTreeSet;

/// `CreateTableAsRelExists` reports the written relation's unqualified name for both CTAS and materialized views.
pub fn existing_create_as_target(
    name: &str,
    if_not_exists: bool,
) -> Result<crate::SQLNotice, SQLError> {
    let (_, relation) =
        uqa_core::RelationIdentity::parse_reference(name).map_err(SQLError::Unsupported)?;
    if if_not_exists {
        Ok(
            crate::SQLNotice::notice(format!("relation \"{relation}\" already exists, skipping"))
                .with_sqlstate("42P07"),
        )
    } else {
        Err(SQLError::Routine {
            sqlstate: "42P07".into(),
            message: format!("relation \"{relation}\" already exists"),
        })
    }
}

/// `CheckAttributeNamesTypes`: no column may take a system column's name, and then no column may have a pseudo-type.
pub fn validate_create_table_columns(table: &CreateTable) -> Result<(), SQLError> {
    validate_relation_column_names_and_types(&table.columns)
}

fn validate_relation_column_names_and_types(columns: &[ColumnDef]) -> Result<(), SQLError> {
    for column in columns {
        super::columns::validate_postgres_column_name(&column.name)?;
    }
    for column in columns {
        super::columns::validate_postgres_relation_column_type(&column.name, &column.ty)?;
    }
    Ok(())
}

/// `DefineRelation` for a table whose columns a query defines: `MergeAttributes` rejects a repeated name, `BuildDescForRelation` requires `USAGE` on every column's type, and `CheckAttributeNamesTypes` rejects system column names and pseudo-types.
pub fn validate_create_table_as_columns(
    types: &dyn FunctionTypeResolver,
    columns: &[ColumnDef],
) -> Result<(), SQLError> {
    let mut seen = BTreeSet::new();
    for column in columns {
        if !seen.insert(&column.name) {
            return Err(SQLError::Routine {
                sqlstate: "42701".into(),
                message: format!("column \"{}\" specified more than once", column.name),
            });
        }
    }
    for column in columns {
        types.require_type_usage(&column.ty)?;
    }
    validate_relation_column_names_and_types(columns)
}

/// The columns of a table that a query defines, named by the column list and then by the query, as `intorel_startup` names them: more names than the query has columns are rejected.
pub fn create_table_as_columns(
    query_schema: &crate::RowSchema,
    column_names: &[String],
) -> Result<Vec<ColumnDef>, SQLError> {
    let names = query_schema
        .columns()
        .iter()
        .enumerate()
        .map(|(position, name)| {
            column_names
                .get(position)
                .cloned()
                .unwrap_or_else(|| name.clone())
        })
        .collect::<Vec<_>>();
    let columns = names
        .into_iter()
        .enumerate()
        .map(|(position, name)| {
            let ty = query_schema
                .column_type(position)
                .cloned()
                .unwrap_or(ColumnType::Text);
            let collation = query_schema.column_collation(position).unwrap_or_else(|| {
                u32::try_from(crate::catalog::type_metadata::pg_type_collation_oid(&ty))
                    .expect("column collation OID")
            });
            if collation == 0 && super::composites::type_is_collatable(&ty) {
                return Err(SQLError::Diagnostic {
                    sqlstate: "42P22".into(),
                    message: format!(
                        "no collation was derived for column \"{name}\" with collatable type {}",
                        ty.display_name()
                    ),
                    detail: None,
                    hint: Some("Use the COLLATE clause to set the collation explicitly.".into()),
                });
            }
            Ok(ColumnDef {
                collation_oid: Some(collation),
                name,
                ty,
                object_id: None,
                attribute_number: None,
                missing_value: None,
                primary_key: false,
                not_null: false,
                not_null_explicit: false,
                not_null_name: None,
                not_null_identity: None,
                not_null_validated: true,
                not_null_no_inherit: false,
                not_null_is_local: true,
                auto_increment: None,
                unique: false,
                default: None,
                generated: None,
                check: None,
                check_name: None,
                check_enforced: true,
                check_validated: true,
                check_no_inherit: false,
                check_is_local: true,
                check_object_id: None,
                check_catalog_oid: None,
                default_catalog_oid: None,
                references: None,
            })
        })
        .collect::<Result<Vec<_>, SQLError>>()?;
    if column_names.len() > query_schema.len() {
        return Err(SQLError::Routine {
            sqlstate: "42601".into(),
            message: "too many column names were specified".into(),
        });
    }
    Ok(columns)
}

pub mod checks;
pub mod column_declarations;
pub mod declaration;
pub mod foreign_keys;
pub mod keys;
pub mod not_nulls;

#[cfg(test)]
mod tests;