mz_deploy/project/syntax/
profile_files.rs1use crate::project::error::LoadError;
34use std::collections::BTreeMap;
35use std::path::{Path, PathBuf};
36
37pub(crate) fn parse_file_stem(stem: &str) -> (&str, Option<&str>) {
42 if let Some((object_name, profile)) = stem.rsplit_once('#') {
43 if !object_name.is_empty() && !profile.is_empty() {
44 return (object_name, Some(profile));
45 }
46 }
47 (stem, None)
48}
49
50#[derive(Debug, Clone)]
52pub(crate) struct ObjectFiles {
53 pub name: String,
55 pub default: Option<PathBuf>,
57 pub overrides: BTreeMap<String, PathBuf>,
59}
60
61pub(crate) fn collect_all_sql_files(directory: &Path) -> Result<Vec<ObjectFiles>, LoadError> {
66 let entries: Vec<_> = std::fs::read_dir(directory)
67 .map_err(|e| LoadError::DirectoryReadFailed {
68 path: directory.to_path_buf(),
69 source: e,
70 })?
71 .collect::<Result<Vec<_>, _>>()
72 .map_err(|e| LoadError::EntryReadFailed {
73 directory: directory.to_path_buf(),
74 source: e,
75 })?;
76
77 let mut groups: BTreeMap<String, ObjectFiles> = BTreeMap::new();
78
79 for entry in entries {
80 let path = entry.path();
81
82 if path.extension().and_then(|e| e.to_str()) != Some("sql") {
83 continue;
84 }
85
86 let file_stem = path
87 .file_stem()
88 .and_then(|s| s.to_str())
89 .ok_or_else(|| LoadError::InvalidFileName { path: path.clone() })?
90 .to_string();
91
92 let (object_name, file_profile) = parse_file_stem(&file_stem);
93 let group = groups
94 .entry(object_name.to_string())
95 .or_insert_with(|| ObjectFiles {
96 name: object_name.to_string(),
97 default: None,
98 overrides: BTreeMap::new(),
99 });
100
101 match file_profile {
102 None => {
103 if let Some(existing) = &group.default {
104 return Err(LoadError::DuplicateProfileObject {
105 name: object_name.to_string(),
106 profile: "default".to_string(),
107 path1: existing.clone(),
108 path2: path,
109 });
110 }
111 group.default = Some(path);
112 }
113 Some(p) => {
114 if let Some(existing) = group.overrides.get(p) {
115 return Err(LoadError::DuplicateProfileObject {
116 name: object_name.to_string(),
117 profile: p.to_string(),
118 path1: existing.clone(),
119 path2: path,
120 });
121 }
122 group.overrides.insert(p.to_string(), path);
123 }
124 }
125 }
126
127 Ok(groups.into_values().collect())
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[mz_ore::test]
137 fn test_parse_no_delimiter() {
138 assert_eq!(parse_file_stem("pg_conn"), ("pg_conn", None));
139 }
140
141 #[mz_ore::test]
142 fn test_parse_with_profile() {
143 assert_eq!(
144 parse_file_stem("pg_conn#staging"),
145 ("pg_conn", Some("staging"))
146 );
147 }
148
149 #[mz_ore::test]
150 fn test_parse_object_name_with_underscores() {
151 assert_eq!(
154 parse_file_stem("stg_stripe__payments#staging"),
155 ("stg_stripe__payments", Some("staging"))
156 );
157 }
158
159 #[mz_ore::test]
160 fn test_parse_empty_profile() {
161 assert_eq!(parse_file_stem("pg_conn#"), ("pg_conn#", None));
163 }
164
165 #[mz_ore::test]
166 fn test_parse_empty_object_name() {
167 assert_eq!(parse_file_stem("#staging"), ("#staging", None));
169 }
170
171 #[mz_ore::test]
172 fn test_parse_no_underscores() {
173 assert_eq!(parse_file_stem("simple"), ("simple", None));
174 }
175
176 #[mz_ore::test]
177 fn test_parse_single_underscore() {
178 assert_eq!(parse_file_stem("my_table"), ("my_table", None));
179 }
180
181 #[mz_ore::test]
184 fn test_collect_all_sql_files_basic() {
185 let dir = tempfile::TempDir::new().unwrap();
186 std::fs::write(dir.path().join("conn.sql"), "SELECT 1;").unwrap();
187 std::fs::write(dir.path().join("conn#staging.sql"), "SELECT 2;").unwrap();
188 std::fs::write(dir.path().join("conn#prod.sql"), "SELECT 3;").unwrap();
189 std::fs::write(dir.path().join("table.sql"), "SELECT 4;").unwrap();
190
191 let result = collect_all_sql_files(dir.path()).unwrap();
192 assert_eq!(result.len(), 2);
193
194 let conn = result.iter().find(|f| f.name == "conn").unwrap();
195 assert!(conn.default.is_some());
196 assert_eq!(conn.overrides.len(), 2);
197 assert!(conn.overrides.contains_key("staging"));
198 assert!(conn.overrides.contains_key("prod"));
199
200 let table = result.iter().find(|f| f.name == "table").unwrap();
201 assert!(table.default.is_some());
202 assert!(table.overrides.is_empty());
203 }
204
205 #[mz_ore::test]
206 fn test_collect_all_sql_files_override_only() {
207 let dir = tempfile::TempDir::new().unwrap();
208 std::fs::write(dir.path().join("secret#staging.sql"), "SELECT 1;").unwrap();
209
210 let result = collect_all_sql_files(dir.path()).unwrap();
211 assert_eq!(result.len(), 1);
212 assert_eq!(result[0].name, "secret");
213 assert!(result[0].default.is_none());
214 assert_eq!(result[0].overrides.len(), 1);
215 }
216
217 #[mz_ore::test]
218 fn test_collect_all_sql_files_duplicate_override_errors() {
219 let dir = tempfile::TempDir::new().unwrap();
224 std::fs::write(dir.path().join("conn.sql"), "SELECT 1;").unwrap();
225 let result = collect_all_sql_files(dir.path()).unwrap();
226 assert_eq!(result.len(), 1);
227 }
228
229 #[mz_ore::test]
230 fn test_collect_all_sql_files_ignores_non_sql() {
231 let dir = tempfile::TempDir::new().unwrap();
232 std::fs::write(dir.path().join("conn.sql"), "SELECT 1;").unwrap();
233 std::fs::write(dir.path().join("readme.md"), "hello").unwrap();
234
235 let result = collect_all_sql_files(dir.path()).unwrap();
236 assert_eq!(result.len(), 1);
237 }
238}