uqa-sql 0.5.4

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

//! Reconstruct SQL from typed catalog nodes using the caller's catalog names.

use super::{invalid, values, Field, Node};
use crate::SQLError;

mod bodies;
mod coercions;
mod control;
mod fields;
mod operators;
mod partition;
mod relation;
mod rows;
mod subscripts;
mod types;

pub use bodies::return_body;
pub use relation::validate_relation;

#[cfg(test)]
mod tests;

pub trait ExpressionNames {
    fn column(&self, attribute: i64) -> Result<String, SQLError>;
    /// The current local relation name used by a whole-row Var.
    fn relation_name(&self) -> Result<String, SQLError> {
        Err(invalid("whole-row reference requires its relation name"))
    }
    fn routine(&self, oid: i64) -> Result<Vec<String>, SQLError>;
    fn type_name(&self, oid: i64, modifier: i64) -> Result<String, SQLError>;

    /// Current SQL-visible name of a selected collation identity.
    fn collation(&self, oid: i64) -> Result<Vec<String>, SQLError> {
        crate::catalog::collations::builtin_collation_name(oid)
            .map(|name| vec![name.into()])
            .ok_or_else(|| invalid("unknown collation identity"))
    }

    /// Type identities and current output metadata for admitted physical constants.
    fn value_catalog(&self) -> Option<&dyn crate::expr::SQLValueCatalog> {
        None
    }

    /// The invoking session's string-literal rule for reconstructed SQL.
    fn standard_conforming_strings(&self) -> Result<bool, SQLError> {
        Ok(true)
    }

    /// SQL spelling of an external parameter after the renderer validates its positive number.
    fn parameter(&self, number: i64) -> Result<String, SQLError> {
        Ok(format!("${number}"))
    }
}

pub fn expression(
    value: &Field,
    names: &dyn ExpressionNames,
    pretty: bool,
) -> Result<String, SQLError> {
    let renderer = Renderer {
        names,
        pretty,
        indent: 0,
    };
    match value {
        Field::List(values) => values
            .iter()
            .map(|value| renderer.field(coercions::implicit_cast_input(value)?, pretty))
            .collect::<Result<Vec<_>, _>>()
            .map(|values| values.join(", ")),
        _ => renderer.field(coercions::implicit_cast_input(value)?, pretty),
    }
}

struct Renderer<'a> {
    names: &'a dyn ExpressionNames,
    pretty: bool,
    indent: usize,
}

impl Renderer<'_> {
    fn field(&self, value: &Field, outer: bool) -> Result<String, SQLError> {
        match value {
            Field::Node(node) => self.node(node, outer),
            Field::List(values) => values
                .iter()
                .map(|value| self.field(value, outer))
                .collect::<Result<Vec<_>, _>>()
                .map(|values| values.join(", ")),
            _ => Err(invalid("expected an expression node")),
        }
    }

    fn node(&self, node: &Node, outer: bool) -> Result<String, SQLError> {
        match node.kind.as_str() {
            "COERCETODOMAINVALUE" => Ok("VALUE".into()),
            "VAR" => self.variable(node),
            "PARAM" => self.parameter(node),
            "SUBSCRIPTINGREF" => self.array_access(node),
            "FIELDSELECT" => self.field_select(node),
            "CONST" => self.constant(node),
            "PARTITIONBOUNDSPEC" => self.partition_bound(node),
            "COLLATEEXPR" => self.collate(node, outer),
            "OPEXPR" | "DISTINCTEXPR" => self.operator(node, outer),
            "ROWEXPR" => self.row(node),
            "ROWCOMPAREEXPR" => self.row_comparison(node),
            "NULLIFEXPR" => {
                let [left, right] = list(node, "args")? else {
                    return Err(invalid("invalid NULLIF operands"));
                };
                Ok(format!(
                    "NULLIF({}, {})",
                    self.field(left, self.pretty)?,
                    self.field(right, self.pretty)?
                ))
            }
            "FUNCEXPR" => self.function(node),
            "NAMEDARGEXPR" => Ok(format!(
                "{} => {}",
                crate::expr::quote_ident(atom(node, "name")?),
                self.field(node.field("arg")?, self.pretty)?
            )),
            "RELABELTYPE" | "COERCEVIAIO" | "COERCETODOMAIN" | "ARRAYCOERCEEXPR" => {
                self.coercion(node, outer)
            }
            "BOOLEXPR" => self.boolean(node, outer),
            "BOOLEANTEST" => self.boolean_test(node, outer),
            "NULLTEST" => self.null_test(node, outer),
            "COALESCEEXPR" | "MINMAXEXPR" => {
                let name = match node.kind.as_str() {
                    "COALESCEEXPR" => "COALESCE",
                    _ => match node.integer("op")? {
                        0 => "GREATEST",
                        1 => "LEAST",
                        _ => return Err(invalid("invalid minimum/maximum expression")),
                    },
                };
                let arguments = list(node, "args")?
                    .iter()
                    .map(|arg| self.field(arg, self.pretty))
                    .collect::<Result<Vec<_>, _>>()?;
                Ok(format!("{name}({})", arguments.join(", ")))
            }
            "ARRAYEXPR" => {
                let elements = list(node, "elements")?
                    .iter()
                    .map(|arg| self.field(arg, self.pretty))
                    .collect::<Result<Vec<_>, _>>()?;
                if elements.is_empty() {
                    return Ok(format!(
                        "ARRAY[]::{}",
                        self.names.type_name(node.integer("array_typeid")?, -1)?
                    ));
                }
                Ok(format!("ARRAY[{}]", elements.join(", ")))
            }
            "CASEEXPR" => self.case(node),
            "SCALARARRAYOPEXPR" => {
                let operator =
                    crate::type_resolution::binary_operator_by_oid(node.integer("opno")?)
                        .ok_or_else(|| invalid("unknown scalar-array operator"))?;
                let [left, right] = list(node, "args")? else {
                    return Err(invalid("invalid scalar-array operands"));
                };
                let quantifier = if node.boolean("useOr")? { "ANY" } else { "ALL" };
                Ok(parentheses(
                    format!(
                        "{} {} {quantifier} ({})",
                        self.field(left, false)?,
                        operator.name,
                        self.field(right, false)?
                    ),
                    !outer,
                ))
            }
            _ => Err(SQLError::Unsupported(format!(
                "catalog expression deparser for {}",
                node.kind
            ))),
        }
    }

    fn variable(&self, node: &Node) -> Result<String, SQLError> {
        if node.integer("varno")? != 1 || node.integer("varlevelsup")? != 0 {
            return Err(invalid("invalid varno in stored expression"));
        }
        match node.integer("varattno")? {
            0 => self
                .names
                .relation_name()
                .map(|name| format!("{}.*", crate::expr::quote_ident(&name))),
            -6 => Ok("tableoid".into()),
            attribute => self
                .names
                .column(attribute)
                .map(|name| crate::expr::quote_ident(&name)),
        }
    }

    fn parameter(&self, node: &Node) -> Result<String, SQLError> {
        let number = node.integer("paramid")?;
        if node.integer("paramkind")? != 0 || number <= 0 {
            return Err(invalid("invalid external parameter in stored expression"));
        }
        self.names.parameter(number)
    }

    fn collate(&self, node: &Node, outer: bool) -> Result<String, SQLError> {
        let name = self
            .names
            .collation(node.integer("collOid")?)?
            .iter()
            .map(|name| crate::expr::quote_ident(name))
            .collect::<Vec<_>>()
            .join(".");
        Ok(parentheses(
            format!("{} COLLATE {name}", self.field(node.field("arg")?, false)?),
            !outer,
        ))
    }

    fn constant(&self, node: &Node) -> Result<String, SQLError> {
        self.constant_labeled(node, true)
    }

    fn constant_labeled(&self, node: &Node, show_type: bool) -> Result<String, SQLError> {
        let oid = node.integer("consttype")?;
        let modifier = node.integer("consttypmod")?;
        let type_name = || self.names.type_name(oid, modifier);
        if node.boolean("constisnull")? {
            return if show_type {
                Ok(format!("NULL::{}", type_name()?))
            } else {
                Ok("NULL".into())
            };
        }
        let Field::Datum { length, bytes } = node.field("constvalue")? else {
            return Err(invalid("constant has no Datum"));
        };
        if node.boolean("constbyval")? && (bytes.len() != 8 || !matches!(*length, 1 | 2 | 4 | 8)) {
            return Err(invalid("invalid by-value Datum length"));
        }
        let quoted = |value: &str| self.constant_literal(value, node, show_type);
        match oid {
            705 => self.constant_literal(values::unknown_text(node)?, node, false),
            16 => match bytes.first() {
                Some(0) => Ok("false".into()),
                Some(1) => Ok("true".into()),
                _ => Err(invalid("invalid boolean Datum")),
            },
            20 | 21 | 23 | 26 | 28 => {
                let value = match oid {
                    21 => i64::from(i16::from_le_bytes(prefix(bytes)?)),
                    23 => i64::from(i32::from_le_bytes(prefix(bytes)?)),
                    26 | 28 => i64::from(u32::from_le_bytes(prefix(bytes)?)),
                    _ => i64::from_le_bytes(prefix(bytes)?),
                };
                if oid == 23 && value >= 0 {
                    Ok(value.to_string())
                } else {
                    quoted(&value.to_string())
                }
            }
            700 => quoted(&crate::expr::format_real(f32::from_le_bytes(prefix(
                bytes,
            )?))),
            701 => quoted(&uqa_core::format_float_pg(f64::from_le_bytes(prefix(
                bytes,
            )?))),
            18 => {
                let byte = *bytes
                    .first()
                    .ok_or_else(|| invalid("empty character Datum"))?;
                quoted(&crate::expr::internal_char::output_with_control(
                    byte,
                    &uqa_core::memory::ProductionControl::uncontrolled(),
                )?)
            }
            19 => {
                let end = bytes
                    .iter()
                    .position(|byte| *byte == 0)
                    .unwrap_or(bytes.len());
                quoted(
                    std::str::from_utf8(&bytes[..end])
                        .map_err(|_| invalid("invalid name Datum"))?,
                )
            }
            25 | 1042 | 1043 | 1790 => quoted(
                std::str::from_utf8(values::varlena_payload(*length, bytes)?)
                    .map_err(|_| invalid("invalid string Datum"))?,
            ),
            17 | 114 | 2950 | 3802 => quoted(&values::scalar_text(node)?),
            1700 => {
                let text = values::numeric::decode(values::varlena_payload(*length, bytes)?)?;
                if text.as_bytes().first().is_some_and(u8::is_ascii_digit) && text.contains('.') {
                    if modifier < 0 || !show_type {
                        Ok(text)
                    } else {
                        Ok(format!("{text}::{}", type_name()?))
                    }
                } else {
                    quoted(&text)
                }
            }
            1082 | 1083 | 1114 | 1184 | 1186 | 1266 => quoted(&crate::expr::value_to_string(
                &uqa_core::Value::Temporal(values::temporal::decode(bytes, oid)?),
            )?),
            _ => match values::catalog_text(node, self.names.value_catalog())? {
                Some(text) => quoted(&text),
                None => Err(SQLError::Unsupported(format!(
                    "catalog expression Datum deparser for type {oid}"
                ))),
            },
        }
    }
    fn constant_literal(
        &self,
        value: &str,
        node: &Node,
        show_type: bool,
    ) -> Result<String, SQLError> {
        let mut output = String::new();
        crate::expr::append_sql_literal(
            &mut output,
            value,
            self.names.standard_conforming_strings()?,
        );
        if show_type {
            output.push_str("::");
            output.push_str(
                &self
                    .names
                    .type_name(node.integer("consttype")?, node.integer("consttypmod")?)?,
            );
        }
        Ok(output)
    }
}

fn list<'a>(node: &'a Node, name: &str) -> Result<&'a [Field], SQLError> {
    match node.field(name)? {
        Field::List(values) => Ok(values),
        Field::Null => Ok(&[]),
        _ => Err(invalid(format!("expected node list in {name}"))),
    }
}

fn atom<'a>(node: &'a Node, name: &str) -> Result<&'a str, SQLError> {
    match node.field(name)? {
        Field::Atom(value) | Field::String(value) => Ok(value),
        _ => Err(invalid(format!("expected token in {name}"))),
    }
}

fn prefix<const N: usize>(bytes: &[u8]) -> Result<[u8; N], SQLError> {
    bytes
        .get(..N)
        .and_then(|prefix| prefix.try_into().ok())
        .ok_or_else(|| invalid("truncated constant Datum"))
}

fn parentheses(text: String, required: bool) -> String {
    if required {
        format!("({text})")
    } else {
        text
    }
}