Skip to main content

razeri_plugin_contract/
task_renderer.rs

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