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    /// **Whether the task runs on its schedule once it is made.** `false`
32    /// asks for it made and left disabled; an engine that cannot say that
33    /// answers `None` rather than schedule a task its author turned off.
34    pub enabled: bool,
35}
36
37/// A single task dependency for rendering.
38#[derive(Debug)]
39pub struct TaskDependencyConfig<'a> {
40    /// Name of the predecessor task.
41    pub predecessor: &'a str,
42    /// Condition kind: `"on success"`, `"on completion"`, or `"on failure"`.
43    pub condition: &'a str,
44}
45
46/// Render a dependency list as a SQL comment line.
47///
48/// Returns an empty string when `deps` is empty.
49#[must_use]
50pub fn render_dependency_comment(deps: &[TaskDependencyConfig<'_>]) -> String {
51    if deps.is_empty() {
52        return String::new();
53    }
54    let strs: Vec<String> =
55        deps.iter().map(|d| format!("{} ({})", d.predecessor, d.condition)).collect();
56    crate::comment::block_comment(&format!("Dependencies: {}", strs.join(", ")))
57}
58
59/// Task and schedule rendering for database-native orchestration.
60///
61/// Platform-specific task syntax is implemented by dialect plugins.
62pub trait TaskRenderer: SqlRenderer {
63    // ========================================================================
64    // Schedule rendering
65    // ========================================================================
66
67    /// Render a cron-based schedule clause.
68    fn render_schedule_cron(&self, expression: &str, timezone: Option<&str>) -> String {
69        let expression = crate::comment::cron_in_comment(expression);
70        let tz = timezone.unwrap_or("UTC");
71        crate::comment::block_comment(&format!("Schedule: cron {expression} {tz}"))
72    }
73
74    /// Render an interval-based schedule clause.
75    fn render_schedule_interval(&self, minutes: u32) -> String {
76        crate::comment::block_comment(&format!("Schedule: every {minutes} minutes"))
77    }
78
79    /// Render a stream-based schedule trigger.
80    fn render_schedule_stream(&self, stream_name: &str) -> String {
81        crate::comment::block_comment(&format!("Stream trigger: {stream_name}"))
82    }
83
84    /// Render an event-driven schedule trigger.
85    fn render_schedule_event(&self, source: &str, description: &str) -> String {
86        crate::comment::block_comment(&format!("Event-driven: {source} ({description})"))
87    }
88
89    // ========================================================================
90    // Task rendering
91    // ========================================================================
92
93    /// **The statement that makes a task run on its schedule**, or `None`
94    /// where this engine has no scheduler of its own.
95    ///
96    /// `None` is the default, and the honest answer for an engine whose
97    /// statements run when they are sent: Razeri then refuses the task by
98    /// name. A task written as its bare statement under a comment would run
99    /// that statement at once, the moment the script ran, rather than on its
100    /// schedule. Answer `None` too for a task whose
101    /// [`enabled`](TaskRenderConfig::enabled) is `false` where your engine
102    /// cannot make one disabled.
103    fn render_create_task(&self, config: &TaskRenderConfig<'_>) -> Option<String> {
104        let _ = config;
105        None
106    }
107
108    /// Render DROP TASK / unschedule.
109    fn render_drop_task(&self, name: &str) -> String {
110        crate::comment::block_comment(&format!("Drop task: {name} (platform manages externally)"))
111    }
112
113    /// Render ALTER TASK ... RESUME or equivalent enable command.
114    fn render_resume_task(&self, name: &str) -> String {
115        crate::comment::block_comment(&format!("Resume task: {name} (no native suspend/resume)"))
116    }
117
118    /// Render ALTER TASK ... SUSPEND or equivalent disable command.
119    fn render_suspend_task(&self, name: &str) -> String {
120        crate::comment::block_comment(&format!("Suspend task: {name} (no native suspend/resume)"))
121    }
122
123    /// **The statement a task runs to load a file into a table** — `COPY
124    /// INTO`, `BULK INSERT`, `LOAD DATA` and their kin — or `None` where this
125    /// engine has no such statement, and the task is refused by name.
126    fn render_bulk_load(&self, table: &str, source: &str, format: &str) -> Option<String> {
127        let _ = (table, source, format);
128        None
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::DataType;
136
137    struct TestDialect;
138
139    impl SqlRenderer for TestDialect {
140        fn render_data_type(&self, _dt: &DataType) -> String {
141            "TEXT".to_string()
142        }
143
144        fn quote_identifier(&self, name: &str) -> String {
145            format!("\"{name}\"")
146        }
147
148        fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
149            match schema {
150                Some(s) => format!("\"{s}\".\"{name}\""),
151                None => format!("\"{name}\""),
152            }
153        }
154
155        fn render_current_timestamp(&self) -> String {
156            "CURRENT_TIMESTAMP".to_string()
157        }
158    }
159
160    impl TaskRenderer for TestDialect {}
161
162    #[test]
163    fn task_renderer_defaults_return_comments() {
164        let d = TestDialect;
165
166        // Schedule defaults
167        assert_eq!(
168            d.render_schedule_cron("0 * * * *", Some("UTC")),
169            crate::comment::block_comment("Schedule: cron 0 * * * * UTC")
170        );
171        assert_eq!(
172            d.render_schedule_interval(15),
173            crate::comment::block_comment("Schedule: every 15 minutes")
174        );
175        assert_eq!(
176            d.render_schedule_stream("my_stream"),
177            crate::comment::block_comment("Stream trigger: my_stream")
178        );
179        assert_eq!(
180            d.render_schedule_event("sns", "order placed"),
181            crate::comment::block_comment("Event-driven: sns (order placed)")
182        );
183
184        // An engine with no scheduler writes no task: its body would run at
185        // once, rather than on its schedule.
186        let config = TaskRenderConfig {
187            name: "hourly",
188            body: "SELECT 1",
189            schedule: Some(crate::comment::block_comment("Schedule: cron 0 * * * * UTC")),
190            warehouse: None,
191            comment: Some("Hourly check"),
192            error_integration: None,
193            condition: None,
194            dependencies: vec![],
195            enabled: true,
196        };
197        assert_eq!(d.render_create_task(&config), None);
198
199        // Drop/resume/suspend defaults include task name
200        assert_eq!(
201            d.render_drop_task("hourly"),
202            crate::comment::block_comment("Drop task: hourly (platform manages externally)")
203        );
204        assert_eq!(
205            d.render_resume_task("hourly"),
206            crate::comment::block_comment("Resume task: hourly (no native suspend/resume)")
207        );
208        assert_eq!(
209            d.render_suspend_task("hourly"),
210            crate::comment::block_comment("Suspend task: hourly (no native suspend/resume)")
211        );
212
213        // Bulk load default: none.
214        assert_eq!(d.render_bulk_load("raw_orders", "s3://bucket/orders.csv", "csv"), None);
215    }
216
217    #[test]
218    fn render_dependency_comment_formats_correctly() {
219        let deps = vec![
220            TaskDependencyConfig { predecessor: "task_a", condition: "on success" },
221            TaskDependencyConfig { predecessor: "task_b", condition: "on completion" },
222        ];
223        let result = render_dependency_comment(&deps);
224        assert_eq!(
225            result,
226            crate::comment::block_comment(
227                "Dependencies: task_a (on success), task_b (on completion)"
228            )
229        );
230    }
231
232    #[test]
233    fn render_dependency_comment_empty_returns_empty() {
234        assert_eq!(render_dependency_comment(&[]), "");
235    }
236}