1use mz_repr::namespaces::is_system_schema;
45use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
46use std::path::Path;
47use std::str::FromStr;
48use tower_lsp::lsp_types::Url;
49
50use mz_sql_parser::ast::{Ident, RawItemName, UnresolvedItemName};
51
52#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
58pub struct ObjectId {
59 database: Option<Ident>,
60 schema: Ident,
61 object: Ident,
62}
63
64impl ObjectId {
65 pub fn new(database: String, schema: String, object: String) -> Self {
71 Self {
72 database: Some(Ident::new_unchecked(database)),
73 schema: Ident::new_unchecked(schema),
74 object: Ident::new_unchecked(object),
75 }
76 }
77
78 pub fn new_system(schema: String, object: String) -> Self {
81 Self {
82 database: None,
83 schema: Ident::new_unchecked(schema),
84 object: Ident::new_unchecked(object),
85 }
86 }
87
88 #[inline]
90 pub fn database(&self) -> Option<&str> {
91 self.database.as_ref().map(Ident::as_str)
92 }
93
94 #[inline]
100 pub fn expect_database(&self) -> &str {
101 self.database
102 .as_ref()
103 .map(Ident::as_str)
104 .unwrap_or_else(|| {
105 panic!(
106 "system-schema ObjectId '{}' used in user-object context",
107 self
108 )
109 })
110 }
111
112 #[inline]
114 pub fn schema(&self) -> &str {
115 self.schema.as_str()
116 }
117
118 #[inline]
120 pub fn object(&self) -> &str {
121 self.object.as_str()
122 }
123
124 pub fn from_item_name(
132 name: &UnresolvedItemName,
133 default_database: &str,
134 default_schema: &str,
135 ) -> Self {
136 match name.0.as_slice() {
137 [object] => Self {
138 database: Some(Ident::new_unchecked(default_database)),
139 schema: Ident::new_unchecked(default_schema),
140 object: object.clone(),
141 },
142 [schema, object] => {
143 let database = if is_system_schema(schema.as_str()) {
144 None
145 } else {
146 Some(Ident::new_unchecked(default_database))
147 };
148 Self {
149 database,
150 schema: schema.clone(),
151 object: object.clone(),
152 }
153 }
154 [database, schema, object] => Self {
155 database: Some(database.clone()),
156 schema: schema.clone(),
157 object: object.clone(),
158 },
159 _ => Self {
160 database: Some(Ident::new_unchecked(default_database)),
161 schema: Ident::new_unchecked(default_schema),
162 object: Ident::new_unchecked("unknown"),
163 },
164 }
165 }
166
167 pub fn from_raw_item_name(
172 name: &RawItemName,
173 default_database: &str,
174 default_schema: &str,
175 ) -> Self {
176 Self::from_item_name(name.name(), default_database, default_schema)
178 }
179
180 pub fn to_unresolved_item_name(&self) -> UnresolvedItemName {
185 let mut parts = Vec::with_capacity(3);
186 if let Some(db) = &self.database {
187 parts.push(db.clone());
188 }
189 parts.push(self.schema.clone());
190 parts.push(self.object.clone());
191 UnresolvedItemName(parts)
192 }
193
194 pub fn default_db_schema_from_uri(file_uri: &Url, root: &Path) -> Option<(String, String)> {
209 let file_path = file_uri.to_file_path().ok()?;
210 let models_dir = root.join("models");
211 let relative = file_path.strip_prefix(&models_dir).ok()?;
212
213 let components: Vec<_> = relative
214 .components()
215 .map(|c| c.as_os_str().to_string_lossy().to_string())
216 .collect();
217
218 if components.len() >= 3 {
220 Some((components[0].clone(), components[1].clone()))
221 } else {
222 None
223 }
224 }
225}
226
227impl FromStr for ObjectId {
228 type Err = String;
229
230 fn from_str(s: &str) -> Result<Self, Self::Err> {
231 let invalid = || {
232 format!(
233 "invalid object id '{}': expected format \
234 'database.schema.object' (or 'schema.object' for system catalogs)",
235 s
236 )
237 };
238 let name = mz_sql_parser::parser::parse_item_name(s).map_err(|_| invalid())?;
239 match name.0.as_slice() {
240 [database, schema, object] => Ok(ObjectId {
241 database: Some(database.clone()),
242 schema: schema.clone(),
243 object: object.clone(),
244 }),
245 [schema, object] if is_system_schema(schema.as_str()) => Ok(ObjectId {
246 database: None,
247 schema: schema.clone(),
248 object: object.clone(),
249 }),
250 _ => Err(invalid()),
251 }
252 }
253}
254
255impl std::fmt::Display for ObjectId {
256 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257 match &self.database {
258 Some(db) => write!(f, "{}.{}.{}", db, self.schema, self.object),
259 None => write!(f, "{}.{}", self.schema, self.object),
260 }
261 }
262}
263
264impl Serialize for ObjectId {
265 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
266 where
267 S: Serializer,
268 {
269 serializer.serialize_str(&self.to_string())
270 }
271}
272
273impl<'de> Deserialize<'de> for ObjectId {
274 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
275 where
276 D: Deserializer<'de>,
277 {
278 struct ObjectIdVisitor;
279
280 impl<'de> de::Visitor<'de> for ObjectIdVisitor {
281 type Value = ObjectId;
282 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
283 formatter.write_str("an object ID")
284 }
285
286 fn visit_str<E>(self, value: &str) -> Result<ObjectId, E>
287 where
288 E: de::Error,
289 {
290 ObjectId::from_str(value).map_err(de::Error::custom)
291 }
292 }
293
294 deserializer.deserialize_str(ObjectIdVisitor)
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[mz_ore::test]
303 fn parse_user_object_three_parts() {
304 let id: ObjectId = "materialize.public.foo".parse().unwrap();
305 assert_eq!(id.database(), Some("materialize"));
306 assert_eq!(id.schema(), "public");
307 assert_eq!(id.object(), "foo");
308 }
309
310 #[mz_ore::test]
311 fn parse_system_schema_two_parts() {
312 for input in [
313 "mz_catalog.mz_objects",
314 "pg_catalog.pg_class",
315 "mz_internal.mz_comments",
316 "mz_introspection.mz_compute_dependencies",
317 "information_schema.tables",
318 ] {
319 let id: ObjectId = input.parse().unwrap_or_else(|e| panic!("{}: {}", input, e));
320 assert_eq!(id.database(), None, "{}", input);
321 }
322 }
323
324 #[mz_ore::test]
325 fn parse_user_two_parts_rejected() {
326 let err = "public.foo".parse::<ObjectId>().unwrap_err();
327 assert!(err.contains("invalid object id"), "got: {}", err);
328 }
329
330 #[mz_ore::test]
331 fn parse_one_part_rejected() {
332 assert!("foo".parse::<ObjectId>().is_err());
333 }
334
335 #[mz_ore::test]
336 fn display_round_trip() {
337 for input in [
338 "materialize.public.foo",
339 "mz_catalog.mz_objects",
340 "pg_catalog.pg_class",
341 ] {
342 let id: ObjectId = input.parse().unwrap();
343 assert_eq!(id.to_string(), input);
344 }
345 }
346
347 #[mz_ore::test]
348 fn from_item_name_two_part_system_strips_default_db() {
349 let name = UnresolvedItemName(vec![
350 Ident::new("mz_catalog").unwrap(),
351 Ident::new("mz_objects").unwrap(),
352 ]);
353 let id = ObjectId::from_item_name(&name, "materialize", "public");
354 assert_eq!(id.database(), None);
355 assert_eq!(id.schema(), "mz_catalog");
356 assert_eq!(id.object(), "mz_objects");
357 }
358
359 #[mz_ore::test]
360 fn from_item_name_two_part_user_uses_default_db() {
361 let name = UnresolvedItemName(vec![
362 Ident::new("public").unwrap(),
363 Ident::new("foo").unwrap(),
364 ]);
365 let id = ObjectId::from_item_name(&name, "materialize", "default");
366 assert_eq!(id.database(), Some("materialize"));
367 assert_eq!(id.schema(), "public");
368 assert_eq!(id.object(), "foo");
369 }
370
371 #[mz_ore::test]
372 fn from_item_name_three_part_used_as_is() {
373 let name = UnresolvedItemName(vec![
374 Ident::new("other_db").unwrap(),
375 Ident::new("staging").unwrap(),
376 Ident::new("events").unwrap(),
377 ]);
378 let id = ObjectId::from_item_name(&name, "materialize", "public");
379 assert_eq!(id.database(), Some("other_db"));
380 assert_eq!(id.schema(), "staging");
381 assert_eq!(id.object(), "events");
382 }
383
384 #[mz_ore::test]
386 fn parse_quoted_reserved_word_three_parts() {
387 let id: ObjectId = "materialize.public.\"table\"".parse().unwrap();
388 assert_eq!(id.database(), Some("materialize"));
389 assert_eq!(id.schema(), "public");
390 assert_eq!(id.object(), "table");
391 }
392
393 #[mz_ore::test]
395 fn reserved_word_display_round_trips() {
396 let id: ObjectId = "materialize.public.\"table\"".parse().unwrap();
397 assert_eq!(id.to_string(), "materialize.public.\"table\"");
398 let reparsed: ObjectId = id.to_string().parse().unwrap();
399 assert_eq!(reparsed, id);
400 }
401
402 #[mz_ore::test]
405 fn reserved_word_identity_matches_across_constructors() {
406 let name = UnresolvedItemName(vec![
407 Ident::new("materialize").unwrap(),
408 Ident::new("public").unwrap(),
409 Ident::new("table").unwrap(),
410 ]);
411 let from_ref = ObjectId::from_item_name(&name, "materialize", "public");
412 let from_stem = ObjectId::new(
413 "materialize".to_string(),
414 "public".to_string(),
415 "table".to_string(),
416 );
417 assert_eq!(from_ref, from_stem);
418 assert_eq!(from_ref.object(), "table");
419 }
420}