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

//! Task and schedule rendering extensions.

use crate::SqlRenderer;

/// Configuration for rendering a CREATE TASK statement.
///
/// Uses primitive types so the contract does not depend on Razeri's internal
/// task and schedule model.
#[derive(Debug)]
pub struct TaskRenderConfig<'a> {
    /// Task name.
    pub name: &'a str,
    /// SQL body to execute.
    pub body: &'a str,
    /// Pre-rendered schedule clause (from `render_schedule_*` methods).
    pub schedule: Option<String>,
    /// Warehouse / compute resource.
    pub warehouse: Option<&'a str>,
    /// Human-readable comment.
    pub comment: Option<&'a str>,
    /// Error integration name.
    pub error_integration: Option<&'a str>,
    /// WHEN condition expression.
    pub condition: Option<&'a str>,
    /// Task dependencies.
    pub dependencies: Vec<TaskDependencyConfig<'a>>,
    /// **Whether the task runs on its schedule once it is made.** `false`
    /// asks for it made and left disabled; an engine that cannot say that
    /// answers `None` rather than schedule a task its author turned off.
    pub enabled: bool,
}

/// A single task dependency for rendering.
#[derive(Debug)]
pub struct TaskDependencyConfig<'a> {
    /// Name of the predecessor task.
    pub predecessor: &'a str,
    /// Condition kind: `"on success"`, `"on completion"`, or `"on failure"`.
    pub condition: &'a str,
}

/// Render a dependency list as a SQL comment line.
///
/// Returns an empty string when `deps` is empty.
#[must_use]
pub fn render_dependency_comment(deps: &[TaskDependencyConfig<'_>]) -> String {
    if deps.is_empty() {
        return String::new();
    }
    let strs: Vec<String> =
        deps.iter().map(|d| format!("{} ({})", d.predecessor, d.condition)).collect();
    format!("-- Dependencies: {}", strs.join(", "))
}

/// Task and schedule rendering for database-native orchestration.
///
/// Platform-specific task syntax is implemented by dialect plugins.
pub trait TaskRenderer: SqlRenderer {
    // ========================================================================
    // Schedule rendering
    // ========================================================================

    /// Render a cron-based schedule clause.
    fn render_schedule_cron(&self, expression: &str, timezone: Option<&str>) -> String {
        let tz = timezone.unwrap_or("UTC");
        format!("-- Schedule: cron {expression} {tz}")
    }

    /// Render an interval-based schedule clause.
    fn render_schedule_interval(&self, minutes: u32) -> String {
        format!("-- Schedule: every {minutes} minutes")
    }

    /// Render a stream-based schedule trigger.
    fn render_schedule_stream(&self, stream_name: &str) -> String {
        format!("-- Stream trigger: {stream_name}")
    }

    /// Render an event-driven schedule trigger.
    fn render_schedule_event(&self, source: &str, description: &str) -> String {
        format!("-- Event-driven: {source} ({description})")
    }

    // ========================================================================
    // Task rendering
    // ========================================================================

    /// **The statement that makes a task run on its schedule**, or `None`
    /// where this engine has no scheduler of its own.
    ///
    /// `None` is the default, and the honest answer for an engine whose
    /// statements run when they are sent: Razeri then refuses the task by
    /// name. A task written as its bare statement under a comment would run
    /// that statement at once, the moment the script ran, rather than on its
    /// schedule. Answer `None` too for a task whose
    /// [`enabled`](TaskRenderConfig::enabled) is `false` where your engine
    /// cannot make one disabled.
    fn render_create_task(&self, config: &TaskRenderConfig<'_>) -> Option<String> {
        let _ = config;
        None
    }

    /// Render DROP TASK / unschedule.
    fn render_drop_task(&self, name: &str) -> String {
        format!("-- Drop task: {name} (platform manages externally)")
    }

    /// Render ALTER TASK ... RESUME or equivalent enable command.
    fn render_resume_task(&self, name: &str) -> String {
        format!("-- Resume task: {name} (no native suspend/resume)")
    }

    /// Render ALTER TASK ... SUSPEND or equivalent disable command.
    fn render_suspend_task(&self, name: &str) -> String {
        format!("-- Suspend task: {name} (no native suspend/resume)")
    }

    /// **The statement a task runs to load a file into a table** — `COPY
    /// INTO`, `BULK INSERT`, `LOAD DATA` and their kin — or `None` where this
    /// engine has no such statement, and the task is refused by name.
    fn render_bulk_load(&self, table: &str, source: &str, format: &str) -> Option<String> {
        let _ = (table, source, format);
        None
    }
}

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

    struct TestDialect;

    impl SqlRenderer for TestDialect {
        fn render_data_type(&self, _dt: &DataType) -> String {
            "TEXT".to_string()
        }

        fn quote_identifier(&self, name: &str) -> String {
            format!("\"{name}\"")
        }

        fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
            match schema {
                Some(s) => format!("\"{s}\".\"{name}\""),
                None => format!("\"{name}\""),
            }
        }

        fn render_current_timestamp(&self) -> String {
            "CURRENT_TIMESTAMP".to_string()
        }
    }

    impl TaskRenderer for TestDialect {}

    #[test]
    fn task_renderer_defaults_return_comments() {
        let d = TestDialect;

        // Schedule defaults
        assert_eq!(
            d.render_schedule_cron("0 * * * *", Some("UTC")),
            "-- Schedule: cron 0 * * * * UTC"
        );
        assert_eq!(d.render_schedule_interval(15), "-- Schedule: every 15 minutes");
        assert_eq!(d.render_schedule_stream("my_stream"), "-- Stream trigger: my_stream");
        assert_eq!(
            d.render_schedule_event("sns", "order placed"),
            "-- Event-driven: sns (order placed)"
        );

        // An engine with no scheduler writes no task: its body would run at
        // once, rather than on its schedule.
        let config = TaskRenderConfig {
            name: "hourly",
            body: "SELECT 1",
            schedule: Some("-- Schedule: cron 0 * * * * UTC".to_string()),
            warehouse: None,
            comment: Some("Hourly check"),
            error_integration: None,
            condition: None,
            dependencies: vec![],
            enabled: true,
        };
        assert_eq!(d.render_create_task(&config), None);

        // Drop/resume/suspend defaults include task name
        assert_eq!(
            d.render_drop_task("hourly"),
            "-- Drop task: hourly (platform manages externally)"
        );
        assert_eq!(
            d.render_resume_task("hourly"),
            "-- Resume task: hourly (no native suspend/resume)"
        );
        assert_eq!(
            d.render_suspend_task("hourly"),
            "-- Suspend task: hourly (no native suspend/resume)"
        );

        // Bulk load default: none.
        assert_eq!(d.render_bulk_load("raw_orders", "s3://bucket/orders.csv", "csv"), None);
    }

    #[test]
    fn render_dependency_comment_formats_correctly() {
        let deps = vec![
            TaskDependencyConfig { predecessor: "task_a", condition: "on success" },
            TaskDependencyConfig { predecessor: "task_b", condition: "on completion" },
        ];
        let result = render_dependency_comment(&deps);
        assert_eq!(result, "-- Dependencies: task_a (on success), task_b (on completion)");
    }

    #[test]
    fn render_dependency_comment_empty_returns_empty() {
        assert_eq!(render_dependency_comment(&[]), "");
    }
}