Skip to main content

mz_repr/
global_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 columnar::Columnar;
15use mz_ore::id_gen::AtomicIdGen;
16#[cfg(any(test, feature = "proptest"))]
17use proptest_derive::Arbitrary;
18use serde::{Deserialize, Serialize};
19
20use crate::CatalogItemId;
21
22/// The identifier for an item/object.
23///
24/// WARNING: `GlobalId`'s `Ord` implementation does not express a dependency order.
25/// One should explicitly topologically sort objects by their dependencies, rather
26/// than rely on the order of identifiers.
27#[derive(
28    Clone,
29    Copy,
30    Debug,
31    Eq,
32    PartialEq,
33    Ord,
34    PartialOrd,
35    Hash,
36    Serialize,
37    Deserialize,
38    Columnar
39)]
40#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
41pub enum GlobalId {
42    /// System namespace.
43    System(u64),
44    /// Introspection Source Index namespace.
45    IntrospectionSourceIndex(u64),
46    /// User namespace.
47    User(u64),
48    /// Transient namespace.
49    Transient(u64),
50    /// Dummy id for query being explained
51    Explain,
52}
53
54// `GlobalId`s are serialized often, so it would be nice to try and keep them small. If this assert
55// fails, then there isn't any correctness issues just potential performance issues.
56static_assertions::assert_eq_size!(GlobalId, [u8; 16]);
57
58impl GlobalId {
59    /// Reports whether this ID is in the system namespace.
60    pub fn is_system(&self) -> bool {
61        matches!(
62            self,
63            GlobalId::System(_) | GlobalId::IntrospectionSourceIndex(_)
64        )
65    }
66
67    /// Reports whether this ID is in the user namespace.
68    pub fn is_user(&self) -> bool {
69        matches!(self, GlobalId::User(_))
70    }
71
72    /// Reports whether this ID is in the transient namespace.
73    pub fn is_transient(&self) -> bool {
74        matches!(self, GlobalId::Transient(_))
75    }
76}
77
78impl FromStr for GlobalId {
79    type Err = Error;
80
81    fn from_str(mut s: &str) -> Result<Self, Self::Err> {
82        if s.len() < 2 {
83            return Err(anyhow!("couldn't parse id {}", s));
84        }
85        if s == "Explained Query" {
86            return Ok(GlobalId::Explain);
87        }
88        let tag = s.chars().next().unwrap();
89        s = &s[tag.len_utf8()..];
90        let variant = match tag {
91            's' => {
92                if Some('i') == s.chars().next() {
93                    s = &s[1..];
94                    GlobalId::IntrospectionSourceIndex
95                } else {
96                    GlobalId::System
97                }
98            }
99            'u' => GlobalId::User,
100            't' => GlobalId::Transient,
101            _ => return Err(anyhow!("couldn't parse id {}", s)),
102        };
103        let val: u64 = s.parse()?;
104        Ok(variant(val))
105    }
106}
107
108impl fmt::Display for GlobalId {
109    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
110        match self {
111            GlobalId::System(id) => write!(f, "s{}", id),
112            GlobalId::IntrospectionSourceIndex(id) => write!(f, "si{}", id),
113            GlobalId::User(id) => write!(f, "u{}", id),
114            GlobalId::Transient(id) => write!(f, "t{}", id),
115            GlobalId::Explain => write!(f, "Explained Query"),
116        }
117    }
118}
119
120#[derive(Debug)]
121pub struct TransientIdGen(AtomicIdGen);
122
123impl TransientIdGen {
124    pub fn new() -> Self {
125        let inner = AtomicIdGen::default();
126        // Transient IDs start at 1, so throw away the 0 value.
127        let _ = inner.allocate_id();
128        Self(inner)
129    }
130
131    pub fn allocate_id(&self) -> (CatalogItemId, GlobalId) {
132        let inner = self.0.allocate_id();
133        (CatalogItemId::Transient(inner), GlobalId::Transient(inner))
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use proptest::prelude::*;
140
141    use super::*;
142
143    #[mz_ore::test]
144    fn proptest_global_id_roundtrips() {
145        fn testcase(og: GlobalId) {
146            let s = og.to_string();
147            let rnd: GlobalId = s.parse().unwrap();
148            assert_eq!(og, rnd);
149        }
150
151        proptest!(|(id in any::<GlobalId>())| {
152            testcase(id);
153        })
154    }
155
156    #[mz_ore::test]
157    fn test_global_id_from_str_non_ascii() {
158        // Regression test for a panic on multi-byte leading characters, where
159        // slicing off a single byte landed inside a UTF-8 char boundary (SQL-195).
160        for invalid in ["ü1", "ü", "é42", "🦀7", ""] {
161            assert!(
162                invalid.parse::<GlobalId>().is_err(),
163                "expected {invalid:?} to fail to parse"
164            );
165        }
166    }
167}