mz_deploy/project/resolve/normalize/
overlay_transformer.rs1use std::collections::BTreeSet;
29
30use mz_sql_parser::ast::{Ident, UnresolvedItemName};
31
32use crate::project::SchemaQualifier;
33use crate::project::ir::compiled::FullyQualifiedName;
34use crate::project::resolve::normalize::transformers::{ClusterTransformer, NameTransformer};
35use mz_repr::namespaces::is_system_schema;
36
37pub(crate) struct OverlayTransformer<'a> {
50 pub(crate) fqn: &'a FullyQualifiedName,
51 pub(crate) profile_name: &'a str,
52 pub(crate) in_project_databases: &'a BTreeSet<String>,
53 pub(crate) dirty_schemas: &'a BTreeSet<SchemaQualifier>,
54 pub(crate) target_cluster: &'a str,
55}
56
57impl<'a> NameTransformer for OverlayTransformer<'a> {
58 fn transform_name(&self, name: &UnresolvedItemName) -> UnresolvedItemName {
59 if name.0.len() == 2 && is_system_schema(name.0[0].as_str()) {
62 return name.clone();
63 }
64
65 let (database, schema, object) = match name.0.len() {
67 1 => {
68 let database = Ident::new(self.fqn.database()).expect("valid database identifier");
70 let schema = Ident::new(self.fqn.schema()).expect("valid schema identifier");
71 let object = name.0[0].clone();
72 (database, schema, object)
73 }
74 2 => {
75 let database = Ident::new(self.fqn.database()).expect("valid database identifier");
77 let schema = name.0[0].clone();
78 let object = name.0[1].clone();
79 (database, schema, object)
80 }
81 3 => {
82 let database = name.0[0].clone();
84 let schema = name.0[1].clone();
85 let object = name.0[2].clone();
86 (database, schema, object)
87 }
88 _ => {
89 return name.clone();
91 }
92 };
93
94 let db_str = database.to_string();
95
96 if !self.in_project_databases.contains(&db_str) {
98 return UnresolvedItemName(vec![database, schema, object]);
99 }
100
101 let qualifier = SchemaQualifier::new(db_str.clone(), schema.to_string());
103 let final_db_str = if self.dirty_schemas.contains(&qualifier) {
104 format!("{}__{}", db_str, self.profile_name)
105 } else {
106 db_str
107 };
108
109 let final_db = Ident::new(&final_db_str).expect("valid database identifier");
110 UnresolvedItemName(vec![final_db, schema, object])
111 }
112
113 fn database_name(&self) -> &str {
114 self.fqn.database()
115 }
116}
117
118impl<'a> ClusterTransformer for OverlayTransformer<'a> {
119 fn transform_cluster(&self, _: &Ident) -> Ident {
120 Ident::new(self.target_cluster).expect("valid cluster identifier")
121 }
122
123 fn get_original_cluster_name(&self, name: &str) -> String {
124 name.to_string()
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::project::ir::compiled::FullyQualifiedName;
132 use crate::project::ir::object_id::ObjectId;
133
134 fn make_fqn(database: &str, schema: &str, object: &str) -> FullyQualifiedName {
135 ObjectId::new(database.to_string(), schema.to_string(), object.to_string()).into()
136 }
137
138 fn make_name(parts: &[&str]) -> UnresolvedItemName {
139 UnresolvedItemName(
140 parts
141 .iter()
142 .map(|s| Ident::new(*s).expect("valid identifier"))
143 .collect(),
144 )
145 }
146
147 fn make_transformer<'a>(
149 fqn: &'a FullyQualifiedName,
150 profile_name: &'a str,
151 in_project_databases: &'a BTreeSet<String>,
152 dirty_schemas: &'a BTreeSet<SchemaQualifier>,
153 ) -> OverlayTransformer<'a> {
154 OverlayTransformer {
155 fqn,
156 profile_name,
157 in_project_databases,
158 dirty_schemas,
159 target_cluster: "quickstart_dev",
160 }
161 }
162
163 #[mz_ore::test]
165 fn external_reference_unchanged() {
166 let fqn = make_fqn("mydb", "public", "ctx");
167 let in_project = BTreeSet::from(["mydb".to_string()]);
168 let dirty: BTreeSet<SchemaQualifier> = BTreeSet::new();
169 let t = make_transformer(&fqn, "alice", &in_project, &dirty);
170
171 let input = make_name(&["external_db", "analytics", "events"]);
172 let result = t.transform_name(&input);
173
174 assert_eq!(result.0[0].as_str(), "external_db");
175 assert_eq!(result.0[1].as_str(), "analytics");
176 assert_eq!(result.0[2].as_str(), "events");
177 }
178
179 #[mz_ore::test]
181 fn in_project_clean_schema_routes_to_prod() {
182 let fqn = make_fqn("mydb", "public", "ctx");
183 let in_project = BTreeSet::from(["mydb".to_string()]);
184 let dirty: BTreeSet<SchemaQualifier> = BTreeSet::new();
185 let t = make_transformer(&fqn, "alice", &in_project, &dirty);
186
187 let input = make_name(&["mydb", "public", "orders"]);
188 let result = t.transform_name(&input);
189
190 assert_eq!(result.0[0].as_str(), "mydb");
191 assert_eq!(result.0[1].as_str(), "public");
192 assert_eq!(result.0[2].as_str(), "orders");
193 }
194
195 #[mz_ore::test]
197 fn in_project_dirty_schema_routes_to_overlay() {
198 let fqn = make_fqn("mydb", "public", "ctx");
199 let in_project = BTreeSet::from(["mydb".to_string()]);
200 let dirty = BTreeSet::from([SchemaQualifier::new(
201 "mydb".to_string(),
202 "public".to_string(),
203 )]);
204 let t = make_transformer(&fqn, "alice", &in_project, &dirty);
205
206 let input = make_name(&["mydb", "public", "orders"]);
207 let result = t.transform_name(&input);
208
209 assert_eq!(result.0[0].as_str(), "mydb__alice");
210 assert_eq!(result.0[1].as_str(), "public");
211 assert_eq!(result.0[2].as_str(), "orders");
212 }
213
214 #[mz_ore::test]
217 fn in_project_dirty_db_with_non_dirty_schema() {
218 let fqn = make_fqn("mydb", "public", "ctx");
219 let in_project = BTreeSet::from(["mydb".to_string()]);
220 let dirty = BTreeSet::from([SchemaQualifier::new(
222 "mydb".to_string(),
223 "analytics".to_string(),
224 )]);
225 let t = make_transformer(&fqn, "alice", &in_project, &dirty);
226
227 let input = make_name(&["mydb", "public", "orders"]);
228 let result = t.transform_name(&input);
229
230 assert_eq!(result.0[0].as_str(), "mydb");
232 assert_eq!(result.0[1].as_str(), "public");
233 assert_eq!(result.0[2].as_str(), "orders");
234 }
235
236 #[mz_ore::test]
239 fn unqualified_name_resolved_via_fqn_then_routed_to_overlay() {
240 let fqn = make_fqn("mydb", "public", "ctx");
241 let in_project = BTreeSet::from(["mydb".to_string()]);
242 let dirty = BTreeSet::from([SchemaQualifier::new(
243 "mydb".to_string(),
244 "public".to_string(),
245 )]);
246 let t = make_transformer(&fqn, "alice", &in_project, &dirty);
247
248 let input = make_name(&["orders"]);
250 let result = t.transform_name(&input);
251
252 assert_eq!(result.0[0].as_str(), "mydb__alice");
253 assert_eq!(result.0[1].as_str(), "public");
254 assert_eq!(result.0[2].as_str(), "orders");
255 }
256
257 #[mz_ore::test]
259 fn transform_cluster_rewrites_to_target() {
260 let fqn = make_fqn("mydb", "public", "ctx");
261 let in_project = BTreeSet::from(["mydb".to_string()]);
262 let dirty: BTreeSet<SchemaQualifier> = BTreeSet::new();
263 let t = make_transformer(&fqn, "alice", &in_project, &dirty);
264
265 let input = Ident::new("prod").expect("valid identifier");
266 let out = t.transform_cluster(&input);
267 assert_eq!(out.as_str(), "quickstart_dev");
268
269 let input2 = Ident::new("anything_else").expect("valid identifier");
270 let out2 = t.transform_cluster(&input2);
271 assert_eq!(out2.as_str(), "quickstart_dev");
272 }
273
274 #[mz_ore::test]
277 fn schema_qualified_name_resolved_via_fqn_then_routed_to_overlay() {
278 let fqn = make_fqn("mydb", "public", "ctx");
279 let in_project = BTreeSet::from(["mydb".to_string()]);
280 let dirty = BTreeSet::from([SchemaQualifier::new(
281 "mydb".to_string(),
282 "analytics".to_string(),
283 )]);
284 let t = make_transformer(&fqn, "alice", &in_project, &dirty);
285
286 let input = make_name(&["analytics", "summary"]);
288 let result = t.transform_name(&input);
289
290 assert_eq!(result.0[0].as_str(), "mydb__alice");
291 assert_eq!(result.0[1].as_str(), "analytics");
292 assert_eq!(result.0[2].as_str(), "summary");
293 }
294}