waypoint_core/
directive.rs1#[derive(Debug, Clone, Default, PartialEq, Eq)]
12pub struct MigrationDirectives {
13 pub depends: Vec<String>,
15 pub env: Vec<String>,
17 pub require: Vec<String>,
19 pub ensure: Vec<String>,
21 pub safety_override: bool,
23}
24
25fn strip_directive_prefix<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
28 if let Some(rest) = line.strip_prefix(prefix) {
29 if rest.is_empty() || rest.starts_with(char::is_whitespace) {
30 Some(rest.trim())
31 } else {
32 None
33 }
34 } else {
35 None
36 }
37}
38
39pub fn parse_directives(sql: &str) -> MigrationDirectives {
44 let mut directives = MigrationDirectives::default();
45
46 for line in sql.lines() {
47 let trimmed = line.trim();
48
49 if trimmed.is_empty() {
51 continue;
52 }
53
54 if !trimmed.starts_with("--") {
56 break;
57 }
58
59 let comment_body = trimmed.strip_prefix("--").unwrap().trim();
60
61 if let Some(value) = strip_directive_prefix(comment_body, "waypoint:depends") {
62 for item in value.split(',') {
63 let item = item.trim();
64 if !item.is_empty() {
65 let version = item.strip_prefix('V').unwrap_or(item);
67 directives.depends.push(version.to_string());
68 }
69 }
70 } else if let Some(value) = strip_directive_prefix(comment_body, "waypoint:env") {
71 for item in value.split(',') {
72 let item = item.trim();
73 if !item.is_empty() {
74 directives.env.push(item.to_string());
75 }
76 }
77 } else if let Some(value) = strip_directive_prefix(comment_body, "waypoint:require") {
78 if !value.is_empty() {
79 directives.require.push(value.to_string());
80 }
81 } else if let Some(value) = strip_directive_prefix(comment_body, "waypoint:ensure") {
82 if !value.is_empty() {
83 directives.ensure.push(value.to_string());
84 }
85 } else if comment_body.trim() == "waypoint:safety-override" {
86 directives.safety_override = true;
87 }
88 }
89
90 directives
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn test_parse_env_directive() {
99 let sql = "-- waypoint:env dev,staging\nCREATE TABLE foo();";
100 let d = parse_directives(sql);
101 assert_eq!(d.env, vec!["dev", "staging"]);
102 assert!(d.depends.is_empty());
103 }
104
105 #[test]
106 fn test_parse_depends_directive() {
107 let sql = "-- waypoint:depends V3,V5\nCREATE TABLE foo();";
108 let d = parse_directives(sql);
109 assert_eq!(d.depends, vec!["3", "5"]);
110 assert!(d.env.is_empty());
111 }
112
113 #[test]
114 fn test_parse_depends_without_v_prefix() {
115 let sql = "-- waypoint:depends 3,5\nCREATE TABLE foo();";
116 let d = parse_directives(sql);
117 assert_eq!(d.depends, vec!["3", "5"]);
118 }
119
120 #[test]
121 fn test_parse_multiple_directives() {
122 let sql = "-- waypoint:env dev\n-- waypoint:depends V1,V2\nCREATE TABLE foo();";
123 let d = parse_directives(sql);
124 assert_eq!(d.env, vec!["dev"]);
125 assert_eq!(d.depends, vec!["1", "2"]);
126 }
127
128 #[test]
129 fn test_stops_at_non_comment_line() {
130 let sql = "-- waypoint:env dev\nCREATE TABLE foo();\n-- waypoint:env prod\n";
131 let d = parse_directives(sql);
132 assert_eq!(d.env, vec!["dev"]);
133 }
134
135 #[test]
136 fn test_empty_sql() {
137 let d = parse_directives("");
138 assert!(d.env.is_empty());
139 assert!(d.depends.is_empty());
140 }
141
142 #[test]
143 fn test_no_directives() {
144 let sql = "-- Regular comment\nCREATE TABLE foo();";
145 let d = parse_directives(sql);
146 assert!(d.env.is_empty());
147 assert!(d.depends.is_empty());
148 }
149
150 #[test]
151 fn test_skips_leading_blank_lines() {
152 let sql = "\n\n-- waypoint:env prod\nCREATE TABLE foo();";
153 let d = parse_directives(sql);
154 assert_eq!(d.env, vec!["prod"]);
155 }
156
157 #[test]
158 fn test_whitespace_in_values() {
159 let sql = "-- waypoint:env dev , staging , prod \nCREATE TABLE foo();";
160 let d = parse_directives(sql);
161 assert_eq!(d.env, vec!["dev", "staging", "prod"]);
162 }
163
164 #[test]
165 fn test_no_env_runs_everywhere() {
166 let d = MigrationDirectives::default();
167 assert!(d.env.is_empty());
168 }
169
170 #[test]
171 fn test_parse_require_directive() {
172 let sql = "-- waypoint:require table_exists(\"users\")\nCREATE TABLE foo();";
173 let d = parse_directives(sql);
174 assert_eq!(d.require, vec!["table_exists(\"users\")"]);
175 }
176
177 #[test]
178 fn test_parse_ensure_directive() {
179 let sql = "-- waypoint:ensure column_exists(\"users\", \"email\")\nALTER TABLE users ADD COLUMN email TEXT;";
180 let d = parse_directives(sql);
181 assert_eq!(d.ensure, vec!["column_exists(\"users\", \"email\")"]);
182 }
183
184 #[test]
185 fn test_parse_multiple_guards() {
186 let sql = "-- waypoint:require table_exists(\"users\")\n-- waypoint:require NOT column_exists(\"users\", \"email\")\n-- waypoint:ensure column_exists(\"users\", \"email\")\nALTER TABLE users ADD COLUMN email TEXT;";
187 let d = parse_directives(sql);
188 assert_eq!(d.require.len(), 2);
189 assert_eq!(d.ensure.len(), 1);
190 }
191
192 #[test]
193 fn test_parse_safety_override() {
194 let sql = "-- waypoint:safety-override\nALTER TABLE large_table ADD COLUMN foo TEXT;";
195 let d = parse_directives(sql);
196 assert!(d.safety_override);
197 }
198
199 #[test]
200 fn test_safety_override_default_false() {
201 let sql = "CREATE TABLE foo();";
202 let d = parse_directives(sql);
203 assert!(!d.safety_override);
204 }
205
206 #[test]
207 fn test_env_prefix_does_not_match_ensure() {
208 let sql = "-- waypoint:ensure column_exists(\"users\", \"email\")\nALTER TABLE users ADD COLUMN email TEXT;";
209 let d = parse_directives(sql);
210 assert!(d.env.is_empty());
212 assert_eq!(d.ensure.len(), 1);
213 }
214
215 #[test]
216 fn test_directive_prefix_boundary() {
217 let sql = "-- waypoint:environment prod\nCREATE TABLE foo();";
219 let d = parse_directives(sql);
220 assert!(d.env.is_empty());
222 }
223
224 #[test]
225 fn test_parse_empty_depends() {
226 let sql = "-- waypoint:depends\nCREATE TABLE foo();";
227 let d = parse_directives(sql);
228 assert!(d.depends.is_empty());
229 }
230
231 #[test]
232 fn test_parse_empty_env() {
233 let sql = "-- waypoint:env\nCREATE TABLE foo();";
234 let d = parse_directives(sql);
235 assert!(d.env.is_empty());
236 }
237
238 #[test]
239 fn test_parse_require_with_special_chars() {
240 let sql = "-- waypoint:require table_exists(\"my-table\")\nCREATE TABLE foo();";
241 let d = parse_directives(sql);
242 assert_eq!(d.require, vec!["table_exists(\"my-table\")"]);
243 }
244}