Skip to main content

razeri_plugin_contract/
task_renderer.rs

1// Copyright (C) 2026 Gabriel Shea
2// Licensed under either of the Apache License, Version 2.0 or the MIT
3// license, at your option.
4
5//! Task and schedule rendering extensions.
6
7use crate::SqlRenderer;
8
9/// Configuration for rendering a CREATE TASK statement.
10///
11/// Uses primitive types so the contract does not depend on Razeri's internal
12/// task and schedule model.
13#[derive(Debug)]
14pub struct TaskRenderConfig<'a> {
15    /// Task name.
16    pub name: &'a str,
17    /// SQL body to execute.
18    pub body: &'a str,
19    /// Pre-rendered schedule clause (from `render_schedule_*` methods).
20    pub schedule: Option<String>,
21    /// Warehouse / compute resource.
22    pub warehouse: Option<&'a str>,
23    /// Human-readable comment.
24    pub comment: Option<&'a str>,
25    /// Error integration name.
26    pub error_integration: Option<&'a str>,
27    /// WHEN condition expression.
28    pub condition: Option<&'a str>,
29    /// Task dependencies.
30    pub dependencies: Vec<TaskDependencyConfig<'a>>,
31}
32
33/// A single task dependency for rendering.
34#[derive(Debug)]
35pub struct TaskDependencyConfig<'a> {
36    /// Name of the predecessor task.
37    pub predecessor: &'a str,
38    /// Condition kind: `"on success"`, `"on completion"`, or `"on failure"`.
39    pub condition: &'a str,
40}
41
42/// Render a dependency list as a SQL comment line.
43///
44/// Returns an empty string when `deps` is empty.
45#[must_use]
46pub fn render_dependency_comment(deps: &[TaskDependencyConfig<'_>]) -> String {
47    if deps.is_empty() {
48        return String::new();
49    }
50    let strs: Vec<String> =
51        deps.iter().map(|d| format!("{} ({})", d.predecessor, d.condition)).collect();
52    format!("-- Dependencies: {}", strs.join(", "))
53}
54
55/// Task and schedule rendering for database-native orchestration.
56///
57/// Platform-specific task syntax is implemented by dialect plugins.
58pub trait TaskRenderer: SqlRenderer {
59    // ========================================================================
60    // Schedule rendering
61    // ========================================================================
62
63    /// Render a cron-based schedule clause.
64    fn render_schedule_cron(&self, expression: &str, timezone: Option<&str>) -> String {
65        let tz = timezone.unwrap_or("UTC");
66        format!("-- Schedule: cron {expression} {tz}")
67    }
68
69    /// Render an interval-based schedule clause.
70    fn render_schedule_interval(&self, minutes: u32) -> String {
71        format!("-- Schedule: every {minutes} minutes")
72    }
73
74    /// Render a stream-based schedule trigger.
75    fn render_schedule_stream(&self, stream_name: &str) -> String {
76        format!("-- Stream trigger: {stream_name}")
77    }
78
79    /// Render an event-driven schedule trigger.
80    fn render_schedule_event(&self, source: &str, description: &str) -> String {
81        format!("-- Event-driven: {source} ({description})")
82    }
83
84    // ========================================================================
85    // Task rendering
86    // ========================================================================
87
88    /// Render CREATE TASK / schedule DDL for a specific platform.
89    fn render_create_task(&self, config: &TaskRenderConfig<'_>) -> String {
90        let mut lines = Vec::new();
91        if let Some(comment) = config.comment {
92            lines.push(format!("-- {comment}"));
93        }
94        if let Some(ref schedule) = config.schedule {
95            lines.push(schedule.clone());
96        }
97        let dep = render_dependency_comment(&config.dependencies);
98        if !dep.is_empty() {
99            lines.push(dep);
100        }
101        lines.push(format!("{};", config.body));
102        lines.join("\n")
103    }
104
105    /// Render DROP TASK / unschedule.
106    fn render_drop_task(&self, name: &str) -> String {
107        format!("-- Drop task: {name} (platform manages externally)")
108    }
109
110    /// Render ALTER TASK ... RESUME or equivalent enable command.
111    fn render_resume_task(&self, name: &str) -> String {
112        format!("-- Resume task: {name} (no native suspend/resume)")
113    }
114
115    /// Render ALTER TASK ... SUSPEND or equivalent disable command.
116    fn render_suspend_task(&self, name: &str) -> String {
117        format!("-- Suspend task: {name} (no native suspend/resume)")
118    }
119
120    /// Render COPY INTO / BULK INSERT / LOAD DATA style statement.
121    fn render_bulk_load(&self, _table: &str, _source: &str, _format: &str) -> String {
122        "-- Bulk load not supported on this platform".to_string()
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::DataType;
130
131    struct TestDialect;
132
133    impl SqlRenderer for TestDialect {
134        fn render_data_type(&self, _dt: &DataType) -> String {
135            "TEXT".to_string()
136        }
137
138        fn quote_identifier(&self, name: &str) -> String {
139            format!("\"{name}\"")
140        }
141
142        fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
143            match schema {
144                Some(s) => format!("\"{s}\".\"{name}\""),
145                None => format!("\"{name}\""),
146            }
147        }
148
149        fn render_current_timestamp(&self) -> String {
150            "CURRENT_TIMESTAMP".to_string()
151        }
152    }
153
154    impl TaskRenderer for TestDialect {}
155
156    #[test]
157    fn task_renderer_defaults_return_comments() {
158        let d = TestDialect;
159
160        // Schedule defaults
161        assert_eq!(
162            d.render_schedule_cron("0 * * * *", Some("UTC")),
163            "-- Schedule: cron 0 * * * * UTC"
164        );
165        assert_eq!(d.render_schedule_interval(15), "-- Schedule: every 15 minutes");
166        assert_eq!(d.render_schedule_stream("my_stream"), "-- Stream trigger: my_stream");
167        assert_eq!(
168            d.render_schedule_event("sns", "order placed"),
169            "-- Event-driven: sns (order placed)"
170        );
171
172        // Create task default renders comment-style
173        let config = TaskRenderConfig {
174            name: "hourly",
175            body: "SELECT 1",
176            schedule: Some("-- Schedule: cron 0 * * * * UTC".to_string()),
177            warehouse: None,
178            comment: Some("Hourly check"),
179            error_integration: None,
180            condition: None,
181            dependencies: vec![],
182        };
183        let sql = d.render_create_task(&config);
184        assert!(sql.contains("-- Hourly check"));
185        assert!(sql.contains("SELECT 1;"));
186
187        // Drop/resume/suspend defaults include task name
188        assert_eq!(
189            d.render_drop_task("hourly"),
190            "-- Drop task: hourly (platform manages externally)"
191        );
192        assert_eq!(
193            d.render_resume_task("hourly"),
194            "-- Resume task: hourly (no native suspend/resume)"
195        );
196        assert_eq!(
197            d.render_suspend_task("hourly"),
198            "-- Suspend task: hourly (no native suspend/resume)"
199        );
200
201        // Bulk load default
202        assert_eq!(
203            d.render_bulk_load("raw_orders", "s3://bucket/orders.csv", "csv"),
204            "-- Bulk load not supported on this platform"
205        );
206    }
207
208    #[test]
209    fn render_dependency_comment_formats_correctly() {
210        let deps = vec![
211            TaskDependencyConfig { predecessor: "task_a", condition: "on success" },
212            TaskDependencyConfig { predecessor: "task_b", condition: "on completion" },
213        ];
214        let result = render_dependency_comment(&deps);
215        assert_eq!(result, "-- Dependencies: task_a (on success), task_b (on completion)");
216    }
217
218    #[test]
219    fn render_dependency_comment_empty_returns_empty() {
220        assert_eq!(render_dependency_comment(&[]), "");
221    }
222}