razeri-plugin-contract 0.20.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.

//! **How a comment is written into generated SQL.**
//!
//! Every comment Razeri writes is a block comment, `/* … */`. A line comment
//! (`-- …`) ends at the end of its line, so a tool that joins a script's lines
//! into one, as a minifier or a copy through a single-line field does, turns
//! everything after the first `--` into comment, and the statements that
//! followed it are gone without an error. A block comment closes itself, so
//! the same script still runs with its line endings turned into spaces.
//!
//! [`SqlRenderer::render_comment`](crate::SqlRenderer::render_comment) is the
//! one place a dialect can change that, and its default is [`block_comment`].
//! No dialect Razeri carries overrides it: every engine it writes SQL for
//! reads `/* … */`.

/// **A comment in the block form**, ready to stand on its own lines.
///
/// One line of text is written `/* text */`. Several lines are written with
/// the first after the opening `/* `, each line after it indented three
/// spaces so it lines up under the first, and the closing ` */` at the end of
/// the last. An empty line of text stays empty.
///
/// The text cannot end the comment early or open another. `*/` in it is
/// written `* /`, and `/*` is written `/ *`, because SQL Server and
/// `PostgreSQL` nest block comments and a stray `/*` would hide the rest of the
/// script. Every comment opens with `/* ` and a space, so it is never read
/// as `MySQL`'s `/*!` or an optimizer hint's `/*+`.
///
/// Trailing spaces are dropped from each line, and so are empty lines at the
/// start and the end.
#[must_use]
pub fn block_comment(text: &str) -> String {
    let safe = escaped(text);
    let lines: Vec<&str> = safe.lines().map(str::trim_end).collect();
    let first = lines.iter().position(|line| !line.is_empty());
    let last = lines.iter().rposition(|line| !line.is_empty());
    let (Some(first), Some(last)) = (first, last) else {
        return "/* */".to_owned();
    };
    let lines = &lines[first..=last];
    if let [only] = lines {
        return format!("/* {only} */");
    }
    let mut out = String::with_capacity(safe.len() + lines.len() * 4 + 6);
    for (at, line) in lines.iter().enumerate() {
        if at == 0 {
            out.push_str("/* ");
        } else {
            out.push('\n');
            if !line.is_empty() {
                out.push_str(BLOCK_COMMENT_HANG);
            }
        }
        out.push_str(line);
    }
    out.push_str(" */");
    out
}

/// **A cron expression as a comment can carry it**, with the same schedule.
///
/// A step over a whole field, `*/6`, holds the two characters that end a block
/// comment, and [`block_comment`] breaks them with a space, which would leave a
/// reader a different expression. So each such step is written over the
/// field's own range instead, `0-23/6` in the hours: the schedule is the same,
/// and nothing in it can end the comment. A five-field expression reads minute,
/// hour, day of month, month and day of week; six or seven fields read a
/// second first, as Quartz does, with its days of the week numbered 1 to 7 and
/// a year last. An expression of any other length is returned as it is.
#[must_use]
pub fn cron_in_comment(expression: &str) -> String {
    const FIVE: [(u32, u32); 5] = [(0, 59), (0, 23), (1, 31), (1, 12), (0, 6)];
    const SEVEN: [(u32, u32); 7] =
        [(0, 59), (0, 59), (0, 23), (1, 31), (1, 12), (1, 7), (1970, 2099)];
    let fields: Vec<&str> = expression.split_whitespace().collect();
    let ranges: &[(u32, u32)] = match fields.len() {
        5 => &FIVE,
        6 | 7 => &SEVEN[..fields.len()],
        _ => return expression.to_owned(),
    };
    fields
        .iter()
        .zip(ranges)
        .map(|(field, (low, high))| {
            field
                .split(',')
                .map(|part| match part.strip_prefix("*/") {
                    Some(step) => format!("{low}-{high}/{step}"),
                    None => part.to_owned(),
                })
                .collect::<Vec<_>>()
                .join(",")
        })
        .collect::<Vec<_>>()
        .join(" ")
}

/// What each line of a block comment after its first is indented by, so it
/// lines up under the text that follows the opening `/* `.
pub const BLOCK_COMMENT_HANG: &str = "   ";

/// The comment's text with every `*/` and `/*` broken by a space.
fn escaped(text: &str) -> String {
    let mut out = String::with_capacity(text.len());
    let mut chars = text.chars().peekable();
    while let Some(c) = chars.next() {
        out.push(c);
        let next = chars.peek().copied();
        if (c == '*' && next == Some('/')) || (c == '/' && next == Some('*')) {
            out.push(' ');
        }
    }
    out
}

/// **A statement's comment lines, apart from its other lines**: each line
/// that is wholly comment (inside a block comment, or opening one, or a line
/// comment), and the rest, each in its order.
///
/// For a reader that has to move a statement's notes somewhere else, as one
/// engine's `EXEC '…'` cannot carry them inside its string.
#[must_use]
pub fn comment_lines_apart(statement: &str) -> (Vec<&str>, Vec<&str>) {
    let mut notes = Vec::new();
    let mut body = Vec::new();
    for (line, is_note) in statement.lines().zip(comment_line_flags(statement)) {
        if is_note {
            notes.push(line);
        } else {
            body.push(line);
        }
    }
    (notes, body)
}

/// **Whether each line of `text` is wholly comment**: inside a block comment,
/// opening one, or a line comment. One flag per line, in order.
#[must_use]
pub fn comment_line_flags(text: &str) -> Vec<bool> {
    let mut flags = Vec::new();
    let mut open = false;
    for line in text.lines() {
        let trimmed = line.trim_start();
        let line_comment = !open && trimmed.starts_with("--");
        let is_note = open || line_comment || trimmed.starts_with("/*");
        if is_note && !line_comment {
            // Whether the block is still open at the end of this line.
            let mut depth = usize::from(open);
            let mut chars = trimmed.chars().peekable();
            while let Some(c) = chars.next() {
                if c == '/' && chars.peek() == Some(&'*') {
                    chars.next();
                    depth += 1;
                } else if c == '*' && chars.peek() == Some(&'/') {
                    chars.next();
                    depth = depth.saturating_sub(1);
                }
            }
            open = depth > 0;
        }
        flags.push(is_note);
    }
    flags
}

#[cfg(test)]
mod tests {

    #[test]
    fn a_cron_step_over_a_whole_field_keeps_its_schedule_and_closes_nothing() {
        assert_eq!(cron_in_comment("0 */6 * * *"), "0 0-23/6 * * *");
        assert_eq!(cron_in_comment("*/15 * * * 1-5"), "0-59/15 * * * 1-5");
        assert_eq!(cron_in_comment("0 0 */2 * * ?"), "0 0 0-23/2 * * ?");
        assert_eq!(cron_in_comment("0 3 * * *"), "0 3 * * *");
        assert_eq!(cron_in_comment("@daily"), "@daily");
        let said = block_comment(&format!("Schedule: cron {}", cron_in_comment("0 */6 * * *")));
        assert_eq!(said, "/* Schedule: cron 0 0-23/6 * * * */");
    }
    use super::*;

    #[test]
    fn one_line_is_one_block() {
        assert_eq!(
            block_comment("Merge in new and updated rows"),
            "/* Merge in new and updated rows */"
        );
    }

    #[test]
    fn several_lines_hang_under_the_first() {
        assert_eq!(
            block_comment("Stop if a row has no key.\nNothing is written.\n\nThe end."),
            "/* Stop if a row has no key.\n   Nothing is written.\n\n   The end. */"
        );
    }

    #[test]
    fn a_closing_mark_in_the_text_cannot_end_the_comment() {
        let rendered = block_comment("a pattern such as '*/' or '/*' or */*/");
        let inner = &rendered[3..rendered.len() - 3];
        assert!(!inner.contains("*/") && !inner.contains("/*"), "{rendered}");
        assert_eq!(rendered, "/* a pattern such as '* /' or '/ *' or * / * / */");
    }

    #[test]
    fn a_comment_is_never_a_hint_or_a_version_comment() {
        assert!(block_comment("!50001 not a version comment").starts_with("/* !"));
        assert!(block_comment("+ INDEX(t) not a hint").starts_with("/* +"));
    }

    #[test]
    fn empty_text_is_an_empty_comment() {
        assert_eq!(block_comment(""), "/* */");
        assert_eq!(block_comment("\n  \n"), "/* */");
    }

    #[test]
    fn a_rule_of_dashes_inside_a_block_is_the_blocks_own_line() {
        let text = "/* ----\n   Entity: t\n   ---- */\nCREATE TABLE t (a INT);";
        assert_eq!(comment_line_flags(text), [true, true, true, false]);
    }

    #[test]
    fn a_statements_notes_come_apart_from_its_lines() {
        let statement = "/* one\n   two */\nCREATE TABLE t (a INT);\n-- three\nSELECT 1";
        let (notes, body) = comment_lines_apart(statement);
        assert_eq!(notes, ["/* one", "   two */", "-- three"]);
        assert_eq!(body, ["CREATE TABLE t (a INT);", "SELECT 1"]);
    }

    #[test]
    fn trailing_spaces_and_outer_blank_lines_go() {
        assert_eq!(block_comment("\nfirst   \nsecond\n\n"), "/* first\n   second */");
    }
}