razeri-plugin-contract 0.8.0

The dialect contract Razeri plugins are written against — rendering traits, data types, platform identity
Documentation
// Copyright (C) 2026 Gabriel Shea
// Licensed under either of the Apache License, Version 2.0 or the MIT
// license, at your option.

//! **The routine Razeri composed, handed to a dialect that spells its own
//! grammar.**
//!
//! Razeri decides *what* a routine is — its name, its parameters, the
//! statements in its body, the comment a reader will find on it — and a
//! [`ProcedureForm`](crate::ProcedureForm),
//! [`ScalarFunctionForm`](crate::ScalarFunctionForm) or
//! [`TableFunctionForm`](crate::TableFunctionForm) decides *how it is spelled*.
//! Where the form is one Razeri carries, Razeri spells it. Where the form is
//! `Custom`, this struct is what Razeri hands the dialect instead, and the
//! dialect returns the whole `CREATE`.
//!
//! # What is in here and what is not
//!
//! Everything a `CREATE` needs that Razeri decided: the name, the schema, the
//! parameters with their types already rendered in **this engine's** spelling,
//! the local declarations, what the routine returns, the body, and the comment.
//!
//! Nothing about how any of it is punctuated. Where the keyword goes, how the
//! body is quoted, whether the statement ends in `;` or `;` and a slash on a
//! line of its own — those are the engine's, which is the whole reason the
//! dialect is being asked.
//!
//! # The types are already rendered, and the type itself is still here
//!
//! Every `data_type` here is a string this dialect produced. A dialect asked to
//! render a routine is never also asked to translate a type it already answered
//! for, and there is no second reading of the type vocabulary for the two
//! answers to drift apart in.
//!
//! **A parameter's type is not always spelled the way a column's is.** Oracle
//! takes `VARCHAR2(100)` on a column and refuses it in a parameter list, where
//! the bare `VARCHAR2` is required. So a parameter's string comes from
//! [`render_parameter_data_type`](crate::SqlRenderer::render_parameter_data_type),
//! which is `render_data_type` for every engine that spells the two the same
//! way and the place to say so for an engine that does not.
//!
//! Where Razeri composed the routine from a typed design, `declared_type`
//! carries the [`DataType`] the string came from, so a renderer that needs to
//! look at the type rather than at its spelling can — a `Some` there is the type
//! Razeri decided, and a `None` says the design supplied this type as text and
//! there is nothing more to read.

use crate::data_type::DataType;

/// Which of the three routine kinds this is.
///
/// A dialect's three render methods each receive one kind and only that kind,
/// so this is here for a renderer that shares one code path between them and
/// for a report that names what it is looking at.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RoutineKind {
    /// A stored procedure — called, returns nothing.
    Procedure,
    /// A function returning one value.
    ScalarFunction,
    /// A function a query selects `FROM`, returning rows.
    TableFunction,
}

/// **Whose privileges the routine runs with.**
///
/// Every engine that has routines has this choice and every engine spells it
/// differently — `SECURITY INVOKER` / `SECURITY DEFINER` on `PostgreSQL`,
/// `EXECUTE AS CALLER` / `EXECUTE AS OWNER` on T-SQL and Snowflake,
/// `AUTHID CURRENT_USER` / `AUTHID DEFINER` on Oracle — so the choice is here
/// and the spelling is the renderer's.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RoutineSecurity {
    /// The privileges of whoever calls it. The default, and the safer one: a
    /// routine that runs as its caller cannot reach past what the caller
    /// already could.
    #[default]
    Caller,
    /// The privileges of whoever owns it.
    Owner,
}

/// Which way a parameter carries its value.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ParameterDirection {
    /// Read by the routine. The only direction a function's parameters take.
    #[default]
    In,
    /// Written by the routine and read by its caller.
    Out,
    /// Both.
    InOut,
    /// Any number of values of the parameter's type, collected into the last
    /// parameter.
    Variadic,
}

/// One parameter of the routine.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoutineParameter {
    /// The parameter's name, as Razeri composed it and **unquoted** — a
    /// dialect that decorates parameter names (T-SQL's `@`, `Redshift`'s
    /// positional `$1`) applies its own decoration.
    ///
    /// Empty where the routine declares a type and no name, which some engines
    /// allow and which a renderer must then address by position.
    pub name: String,
    /// The parameter's type, in the spelling this dialect's
    /// [`render_parameter_data_type`](crate::SqlRenderer::render_parameter_data_type)
    /// gave it.
    pub data_type: String,
    /// The type the string above was rendered from, where Razeri composed this
    /// routine from a typed design; `None` where the design supplied the type as
    /// text.
    pub declared_type: Option<DataType>,
    /// Which way the value travels.
    pub direction: ParameterDirection,
    /// The default expression, where the routine declares one.
    pub default: Option<String>,
}

/// One local variable the routine declares before its body runs.
///
/// Empty for functions; a procedure's declarations are the ones Razeri's
/// composed load needs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoutineDeclaration {
    /// The variable's name, unquoted and undecorated.
    pub name: String,
    /// Its type, in this dialect's column spelling — a declaration is the one
    /// place Oracle keeps the precision a parameter list refuses, so this is
    /// [`render_data_type`](crate::SqlRenderer::render_data_type) and not the
    /// parameter spelling.
    pub data_type: String,
    /// The type the string above was rendered from, where Razeri composed this
    /// routine from a typed design.
    pub declared_type: Option<DataType>,
    /// The initial value, where the routine declares one.
    pub default: Option<String>,
}

/// One column of a table-valued function's row.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoutineColumn {
    /// The column's name, unquoted.
    pub name: String,
    /// Its type, already rendered by this dialect.
    pub data_type: String,
    /// The type the string above was rendered from, where Razeri composed this
    /// routine from a typed design.
    pub declared_type: Option<DataType>,
}

/// What the routine gives back.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RoutineReturns {
    /// Nothing — a procedure.
    Nothing,
    /// One value of this type.
    Value(String),
    /// Rows of these columns.
    Columns(Vec<RoutineColumn>),
    /// Rows shaped like this existing table or type.
    RowsOf(String),
    /// Rows whose shape the engine reads off the query.
    Inferred,
}

/// The routine's body, in the shape Razeri composed it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RoutineBody {
    /// A single expression the function's value is.
    Expression(String),
    /// A query whose rows the function returns.
    Query(String),
    /// Statements, in order, each as Razeri composed it — which means a
    /// statement that needs a `;` already carries one, and a block statement
    /// carries the `;` its own grammar puts inside it.
    ///
    /// How they are separated, and how the block around them is opened and
    /// closed, is still the engine's business. An engine that ends a statement
    /// some other way reads the last character rather than assuming: a `;`
    /// stripped off `END IF;` takes a keyword's punctuation, not a separator.
    Statements(Vec<String>),
    /// A procedural block the design supplied already framed. Emit it as it
    /// stands, inside whatever this engine wraps a body in.
    Block(String),
}

/// **A routine Razeri composed, for a dialect that spells its own grammar.**
///
/// Handed to [`SqlRenderer::render_procedure`](crate::SqlRenderer::render_procedure)
/// and its two siblings when the dialect answered `Custom`.
///
/// It is `#[non_exhaustive]`: Razeri may carry one more fact about a routine
/// later, and a dialect built against today's contract must keep compiling.
/// Read the fields; do not construct one outside Razeri except in a test, where
/// [`RoutineSpec::new`] gives you an empty routine of a kind to fill in.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct RoutineSpec {
    /// Which of the three kinds this is.
    pub kind: RoutineKind,
    /// The schema the routine is created in, where the design names one.
    pub schema: Option<String>,
    /// The routine's name, unquoted.
    pub name: String,
    /// Whether the design asked for a replace rather than a plain create. An
    /// engine with no `OR REPLACE` says so its own way — a `DROP` in front, or
    /// nothing at all.
    pub or_replace: bool,
    /// The routine's parameters, in order.
    pub parameters: Vec<RoutineParameter>,
    /// Local variables the body reads, in order. Empty for a function.
    pub declarations: Vec<RoutineDeclaration>,
    /// What the routine gives back.
    pub returns: RoutineReturns,
    /// Whose privileges the routine runs with.
    pub security: RoutineSecurity,
    /// The body.
    pub body: RoutineBody,
    /// The description a reader of the DDL should find on the routine. Where
    /// the engine has no comment clause, it belongs in a comment.
    pub comment: Option<String>,
}

impl RoutineSpec {
    /// **An empty routine of one kind**, to fill in.
    ///
    /// The constructor exists so that a dialect's own tests can build the spec
    /// their render method is going to receive without waiting for Razeri to
    /// compose one, and so the struct can stay `#[non_exhaustive]` — a field
    /// added later reaches those tests as a default rather than as a broken
    /// build.
    ///
    /// Parameters:
    /// - `kind`: which of the three routine kinds this is;
    /// - `name`: the routine's name, unquoted.
    #[must_use]
    pub fn new(kind: RoutineKind, name: impl Into<String>) -> Self {
        Self {
            kind,
            schema: None,
            name: name.into(),
            or_replace: false,
            parameters: Vec::new(),
            declarations: Vec::new(),
            returns: match kind {
                RoutineKind::Procedure => RoutineReturns::Nothing,
                RoutineKind::ScalarFunction => RoutineReturns::Value(String::new()),
                RoutineKind::TableFunction => RoutineReturns::Columns(Vec::new()),
            },
            security: RoutineSecurity::Caller,
            body: RoutineBody::Statements(Vec::new()),
            comment: None,
        }
    }

    /// **The name a `CREATE` writes**, schema included where there is one.
    ///
    /// Parameters:
    /// - `quote`: how this engine quotes one identifier.
    #[must_use]
    pub fn qualified_name(&self, quote: impl Fn(&str) -> String) -> String {
        match &self.schema {
            Some(schema) => format!("{}.{}", quote(schema), quote(&self.name)),
            None => quote(&self.name),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A new spec of each kind returns what that kind returns, so a dialect
    /// filling one in does not have to remember which.
    #[test]
    fn a_new_spec_returns_what_its_kind_returns() {
        assert_eq!(
            RoutineSpec::new(RoutineKind::Procedure, "load_customer").returns,
            RoutineReturns::Nothing
        );
        assert_eq!(
            RoutineSpec::new(RoutineKind::ScalarFunction, "generate_sid").returns,
            RoutineReturns::Value(String::new())
        );
        assert_eq!(
            RoutineSpec::new(RoutineKind::TableFunction, "customer_at").returns,
            RoutineReturns::Columns(Vec::new())
        );
    }

    /// The qualified name is the engine's quoting over Razeri's names, and a
    /// routine with no schema is one name and not a leading dot.
    #[test]
    fn the_qualified_name_quotes_with_the_engine_and_omits_an_absent_schema() {
        let quote = |name: &str| format!("\"{name}\"");
        let mut spec = RoutineSpec::new(RoutineKind::Procedure, "load_customer");
        assert_eq!(spec.qualified_name(quote), "\"load_customer\"");
        spec.schema = Some("retail_core".to_owned());
        assert_eq!(spec.qualified_name(quote), "\"retail_core\".\"load_customer\"");
    }
}