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