Skip to main content

mz_repr/
catalog_item_id.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::fmt;
11use std::str::FromStr;
12
13use anyhow::{Error, anyhow};
14use mz_proto::{RustType, TryFromProtoError};
15#[cfg(any(test, feature = "proptest"))]
16use proptest_derive::Arbitrary;
17use serde::{Deserialize, Serialize};
18
19include!(concat!(env!("OUT_DIR"), "/mz_repr.catalog_item_id.rs"));
20
21/// The identifier for an item within the Catalog.
22#[derive(
23    Clone,
24    Copy,
25    Debug,
26    Eq,
27    PartialEq,
28    Ord,
29    PartialOrd,
30    Hash,
31    Serialize,
32    Deserialize
33)]
34#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
35pub enum CatalogItemId {
36    /// System namespace.
37    System(u64),
38    /// Introspection Source Index namespace.
39    IntrospectionSourceIndex(u64),
40    /// User namespace.
41    User(u64),
42    /// Transient item.
43    Transient(u64),
44}
45
46impl CatalogItemId {
47    /// Reports whether this ID is in the system namespace.
48    pub fn is_system(&self) -> bool {
49        matches!(
50            self,
51            CatalogItemId::System(_) | CatalogItemId::IntrospectionSourceIndex(_)
52        )
53    }
54
55    /// Reports whether this ID is in the user namespace.
56    pub fn is_user(&self) -> bool {
57        matches!(self, CatalogItemId::User(_))
58    }
59
60    /// Reports whether this ID is for a transient item.
61    pub fn is_transient(&self) -> bool {
62        matches!(self, CatalogItemId::Transient(_))
63    }
64}
65
66impl FromStr for CatalogItemId {
67    type Err = Error;
68
69    fn from_str(mut s: &str) -> Result<Self, Self::Err> {
70        if s.len() < 2 {
71            return Err(anyhow!("couldn't parse id {}", s));
72        }
73        let tag = s.chars().next().unwrap();
74        s = &s[tag.len_utf8()..];
75        let variant = match tag {
76            's' => {
77                if Some('i') == s.chars().next() {
78                    s = &s[1..];
79                    CatalogItemId::IntrospectionSourceIndex
80                } else {
81                    CatalogItemId::System
82                }
83            }
84            'u' => CatalogItemId::User,
85            't' => CatalogItemId::Transient,
86            _ => return Err(anyhow!("couldn't parse id {}", s)),
87        };
88        let val: u64 = s.parse()?;
89        Ok(variant(val))
90    }
91}
92
93impl fmt::Display for CatalogItemId {
94    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
95        match self {
96            CatalogItemId::System(id) => write!(f, "s{}", id),
97            CatalogItemId::IntrospectionSourceIndex(id) => write!(f, "si{}", id),
98            CatalogItemId::User(id) => write!(f, "u{}", id),
99            CatalogItemId::Transient(id) => write!(f, "t{}", id),
100        }
101    }
102}
103
104impl RustType<ProtoCatalogItemId> for CatalogItemId {
105    fn into_proto(&self) -> ProtoCatalogItemId {
106        use proto_catalog_item_id::Kind::*;
107        ProtoCatalogItemId {
108            kind: Some(match self {
109                CatalogItemId::System(x) => System(*x),
110                CatalogItemId::IntrospectionSourceIndex(x) => IntrospectionSourceIndex(*x),
111                CatalogItemId::User(x) => User(*x),
112                CatalogItemId::Transient(x) => Transient(*x),
113            }),
114        }
115    }
116
117    fn from_proto(proto: ProtoCatalogItemId) -> Result<Self, TryFromProtoError> {
118        use proto_catalog_item_id::Kind::*;
119        match proto.kind {
120            Some(System(x)) => Ok(CatalogItemId::System(x)),
121            Some(IntrospectionSourceIndex(x)) => Ok(CatalogItemId::IntrospectionSourceIndex(x)),
122            Some(User(x)) => Ok(CatalogItemId::User(x)),
123            Some(Transient(x)) => Ok(CatalogItemId::Transient(x)),
124            None => Err(TryFromProtoError::missing_field("ProtoCatalogItemId::kind")),
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use proptest::prelude::*;
132
133    use super::*;
134
135    #[mz_ore::test]
136    fn proptest_catalog_item_id_roundtrips() {
137        fn testcase(og: CatalogItemId) {
138            let s = og.to_string();
139            let rnd: CatalogItemId = s.parse().unwrap();
140            assert_eq!(og, rnd);
141        }
142
143        proptest!(|(id in any::<CatalogItemId>())| {
144            testcase(id);
145        })
146    }
147
148    #[mz_ore::test]
149    fn test_catalog_item_id_from_str_non_ascii() {
150        // Regression test for a panic on multi-byte leading characters, where
151        // slicing off a single byte landed inside a UTF-8 char boundary (SQL-195).
152        for invalid in ["ü1", "ü", "é42", "🦀7", ""] {
153            assert!(
154                invalid.parse::<CatalogItemId>().is_err(),
155                "expected {invalid:?} to fail to parse"
156            );
157        }
158    }
159}