razeri-plugin-contract 0.10.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>>,
}

/// 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
    // ========================================================================

    /// Render CREATE TASK / schedule DDL for a specific platform.
    fn render_create_task(&self, config: &TaskRenderConfig<'_>) -> String {
        let mut lines = Vec::new();
        if let Some(comment) = config.comment {
            lines.push(format!("-- {comment}"));
        }
        if let Some(ref schedule) = config.schedule {
            lines.push(schedule.clone());
        }
        let dep = render_dependency_comment(&config.dependencies);
        if !dep.is_empty() {
            lines.push(dep);
        }
        lines.push(format!("{};", config.body));
        lines.join("\n")
    }

    /// 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)")
    }

    /// Render COPY INTO / BULK INSERT / LOAD DATA style statement.
    fn render_bulk_load(&self, _table: &str, _source: &str, _format: &str) -> String {
        "-- Bulk load not supported on this platform".to_string()
    }
}

#[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)"
        );

        // Create task default renders comment-style
        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![],
        };
        let sql = d.render_create_task(&config);
        assert!(sql.contains("-- Hourly check"));
        assert!(sql.contains("SELECT 1;"));

        // 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
        assert_eq!(
            d.render_bulk_load("raw_orders", "s3://bucket/orders.csv", "csv"),
            "-- Bulk load not supported on this platform"
        );
    }

    #[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(&[]), "");
    }
}