razeri-plugin-contract 0.17.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 named routine grammars, rendered where a plugin can reach them.**
//!
//! A dialect states which grammar its engine speaks through
//! [`ProcedureForm`](crate::ProcedureForm),
//! [`ScalarFunctionForm`](crate::ScalarFunctionForm) and
//! [`TableFunctionForm`](crate::TableFunctionForm). Where the answer is one of
//! the named grammars, Razeri spells it. Where the answer is `Custom`, the
//! dialect spells the whole `CREATE` itself — and a dialect whose grammar is
//! *"PL/pgSQL with one tweak"* should not have to write the whole thing from
//! nothing, with no way to see, reuse or diff against the grammar it is almost
//! the same as.
//!
//! These three functions are that grammar, addressable:
//!
//! ```rust
//! use razeri_plugin_contract::routine_render::render_procedure_as;
//! use razeri_plugin_contract::{ProcedureForm, RoutineSpec};
//!
//! # fn spell(routine: &RoutineSpec, describes_procedures: bool) -> Option<String> {
//! // Start from the neighbouring grammar, then say what is different. The
//! // last argument is the dialect's own `supports_procedure_comment`.
//! let near = render_procedure_as(ProcedureForm::PlPgSql, routine, describes_procedures);
//! Some(near.replace("LANGUAGE plpgsql", "LANGUAGE plpgsql_with_one_tweak"))
//! # }
//! ```
//!
//! # What these render from, and what that means
//!
//! They render from a [`RoutineSpec`] — the routine as a `Custom` dialect
//! receives it: names, parameters with their types already spelled, local
//! declarations, what it returns, the body's statements, the comment. That is
//! everything a `CREATE` needs, and it is deliberately not the design Razeri
//! composed it from.
//!
//! So the output is **the form's grammar around the statements it is given**. A
//! statement's own spelling was decided before the spec was built: where Razeri
//! composes a routine from its own design it spells each statement for the
//! engine first, and where a caller builds a spec by hand the statements are
//! whatever the caller wrote. Neither is this module's business, and that is
//! why one function can answer for every form.
//!
//! # Which forms answer with SQL
//!
//! Every form that names a SQL grammar. Two [`ProcedureForm`](crate::ProcedureForm) variants do not
//! name one: `ClickHouse` deploys a procedure as an executable program with an
//! XML declaration beside it, and `Databricks` as a Python function — neither is
//! a `CREATE` any engine parses, and neither can be spelled from a spec. Both
//! answer here with the agnostic form under a comment saying so, because a
//! wrong `CREATE` is worse than a plain sentence.
//!
//! `Custom` answers with the agnostic form too, under a comment naming the
//! grammar. It is not an error to ask: a dialect that wants a neutral starting
//! point, or a fallback branch that reaches this with its own form, gets
//! something to read rather than a panic.
//!
//! `Unsupported` answers with the dialect's own words in a comment, and never a
//! statement — the same sentence, in the same place, that Razeri puts where the
//! routine would have been.

mod procedure;
mod scalar;
mod table;

pub use procedure::render_procedure_as;
pub use scalar::render_scalar_function_as;
pub use table::render_table_function_as;

use crate::routine_spec::{RoutineParameter, RoutineSpec};

/// The name a `CREATE` writes, unquoted: `schema.name`, or `name` alone.
pub(crate) fn plain_name(routine: &RoutineSpec) -> String {
    routine.qualified_name(std::borrow::ToOwned::to_owned)
}

/// The name under one engine's quoting.
pub(crate) fn quoted_name(routine: &RoutineSpec, open: char, close: char) -> String {
    routine.qualified_name(|part| format!("{open}{part}{close}"))
}

/// A single-quoted SQL string, with the quotes inside it doubled.
pub(crate) fn quoted_text(text: &str) -> String {
    text.replace('\'', "''")
}

/// `OR REPLACE `, or nothing.
pub(crate) fn or_replace(routine: &RoutineSpec) -> &'static str {
    if routine.or_replace { "OR REPLACE " } else { "" }
}

/// One parameter as `name type`, the shape most grammars take.
pub(crate) fn name_and_type(parameter: &RoutineParameter) -> String {
    if parameter.name.is_empty() {
        parameter.data_type.clone()
    } else {
        format!("{} {}", parameter.name, parameter.data_type)
    }
}

/// Every parameter on one line, comma separated.
pub(crate) fn inline_parameters(routine: &RoutineSpec) -> String {
    routine.parameters.iter().map(name_and_type).collect::<Vec<_>>().join(", ")
}

/// Just the types, which is how several engines identify a routine they are
/// commenting on or dropping.
pub(crate) fn parameter_types(routine: &RoutineSpec) -> String {
    routine.parameters.iter().map(|p| p.data_type.clone()).collect::<Vec<_>>().join(", ")
}

/// A statement with its first line indented and the rest left as composed — a
/// block the design supplied already framed keeps the shape it was framed in.
pub(crate) fn indented(statement: &str, pad: &str) -> String {
    format!("{pad}{statement}")
}

/// The body's statements, each terminated once, at one level of indentation.
///
/// A statement Razeri composed already carries its terminator and a statement a
/// caller wrote by hand often does not, so this adds one only where there is
/// none — two `;` in a row is a syntax error on several engines and a silent
/// empty statement on the rest.
pub(crate) fn terminated_statements(
    statements: &[String],
    pad: &str,
    terminator: &str,
) -> Vec<String> {
    statements
        .iter()
        .map(|statement| {
            let statement = indented(statement, pad);
            if statement.trim_end().ends_with(terminator) {
                statement
            } else {
                format!("{statement}{terminator}")
            }
        })
        .collect()
}

/// **A refusal, as a comment.** Never a statement: it lands where DDL was
/// expected, and text that parses would define something over the real name.
pub(crate) fn refusal_comment(kind: &str, name: &str, orientation: &str) -> String {
    format!("-- No {kind} named `{name}` is written here.\n-- {orientation}")
}

/// **A note above an agnostic rendering**, for the forms that have no `CREATE`
/// to give.
pub(crate) fn note_above(note: &str, rendered: &str) -> String {
    format!("-- {note}\n{rendered}")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::routine_spec::{RoutineKind, RoutineSpec};

    fn a_spec() -> RoutineSpec {
        let mut spec = RoutineSpec::new(RoutineKind::Procedure, "load_customer");
        spec.schema = Some("retail_core".to_owned());
        spec
    }

    /// The name a grammar writes is the name the spec carries, under that
    /// grammar's quoting and no other.
    #[test]
    fn a_name_is_quoted_the_way_the_grammar_asked() {
        assert_eq!(plain_name(&a_spec()), "retail_core.load_customer");
        assert_eq!(quoted_name(&a_spec(), '[', ']'), "[retail_core].[load_customer]");
        assert_eq!(quoted_name(&a_spec(), '`', '`'), "`retail_core`.`load_customer`");
    }

    /// A quote inside a description does not end the description.
    #[test]
    fn a_quote_inside_a_description_is_doubled() {
        assert_eq!(quoted_text("the day's rows"), "the day''s rows");
    }

    /// **A statement is terminated once.** A statement that arrived with its
    /// terminator keeps the one it has, because two in a row is a syntax error
    /// on several engines and a silent empty statement on the rest.
    #[test]
    fn a_statement_is_terminated_once_and_not_twice() {
        let statements = vec!["DELETE FROM stage".to_owned(), "IF x THEN NULL; END IF;".to_owned()];
        assert_eq!(
            terminated_statements(&statements, "    ", ";"),
            vec!["    DELETE FROM stage;".to_owned(), "    IF x THEN NULL; END IF;".to_owned()]
        );
    }
}