Skip to main content

kalamdb_commons/models/ids/
table_id.rs

1// File: backend/crates/kalamdb-commons/src/models/table_id.rs
2// Composite key for system.tables entries
3
4use std::fmt;
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8use super::namespace_id::NamespaceId;
9use crate::{
10    models::schemas::TableName,
11    storage_key::{decode_key, encode_key, encode_prefix},
12    StorageKey,
13};
14
15/// Composite key for system.tables entries: (namespace_id, table_name)
16///
17/// This composite key provides type-safe access to table metadata,
18/// ensuring namespace and table name are always paired correctly.
19///
20/// # Serialization
21///
22/// Serializes as "namespace.table" string format for JSON compatibility.
23/// For example: `"flush_test_ns_mkav1q2g_3.metrics"`
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25pub struct TableId {
26    namespace_id: NamespaceId,
27    table_name: TableName,
28}
29
30impl TableId {
31    /// Create a new TableId from namespace ID and table name
32    #[inline]
33    pub fn new(namespace_id: NamespaceId, table_name: TableName) -> Self {
34        Self {
35            namespace_id,
36            table_name,
37        }
38    }
39
40    /// Get the namespace ID component
41    #[inline]
42    pub fn namespace_id(&self) -> &NamespaceId {
43        &self.namespace_id
44    }
45
46    /// Get the table name component
47    #[inline]
48    pub fn table_name(&self) -> &TableName {
49        &self.table_name
50    }
51
52    /// Create from string components
53    #[inline]
54    pub fn from_strings(namespace_id: &str, table_name: &str) -> Self {
55        Self {
56            namespace_id: NamespaceId::new(namespace_id),
57            table_name: TableName::new(table_name),
58        }
59    }
60
61    /// Create from string components with validation errors instead of panics.
62    #[inline]
63    pub fn try_from_strings(namespace_id: &str, table_name: &str) -> Result<Self, String> {
64        let namespace_id = NamespaceId::try_parse_reference(namespace_id)
65            .map_err(|e| format!("invalid namespace_id '{}': {}", namespace_id, e))?;
66        let table_name = TableName::try_new(table_name)
67            .map_err(|e| format!("invalid table_name '{}': {}", table_name, e))?;
68
69        Ok(Self {
70            namespace_id,
71            table_name,
72        })
73    }
74
75    /// Create a prefix for scanning all tables in a namespace.
76    #[inline]
77    pub fn namespace_prefix(namespace_id: &NamespaceId) -> Vec<u8> {
78        encode_prefix(&(namespace_id.as_str(),))
79    }
80
81    /// Format as bytes for storage using storekey tuple encoding
82    #[inline]
83    pub fn as_storage_key(&self) -> Vec<u8> {
84        encode_key(&(self.namespace_id.as_str(), self.table_name.as_str()))
85    }
86
87    /// Parse from storage key bytes
88    pub fn from_storage_key(key: &[u8]) -> Option<Self> {
89        if let Ok((namespace_id, table_name)) = decode_key::<(String, String)>(key) {
90            return Some(Self {
91                namespace_id: NamespaceId::new(namespace_id),
92                table_name: TableName::new(table_name),
93            });
94        }
95
96        None
97    }
98
99    /// Consume and return inner components
100    pub fn into_parts(self) -> (NamespaceId, TableName) {
101        (self.namespace_id, self.table_name)
102    }
103
104    /// Returns the fully qualified table name in SQL format: "namespace.table"
105    ///
106    /// This is the format used in SQL queries (e.g., `SELECT * FROM app.users`).
107    /// For storage key format (storekey tuple), use `as_storage_key()` instead.
108    pub fn full_name(&self) -> String {
109        format!("{}.{}", self.namespace_id.as_str(), self.table_name.as_str())
110    }
111}
112
113// Custom Serialize implementation: serialize as "namespace.table" string
114impl Serialize for TableId {
115    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
116    where
117        S: Serializer,
118    {
119        serializer.serialize_str(&self.full_name())
120    }
121}
122
123// Custom Deserialize implementation: deserialize from "namespace.table" string
124// Uses a Visitor pattern to avoid deserialize_any for codec compatibility.
125impl<'de> Deserialize<'de> for TableId {
126    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
127    where
128        D: Deserializer<'de>,
129    {
130        use std::fmt;
131
132        use serde::de::{Error, Visitor};
133
134        struct TableIdVisitor;
135
136        impl<'de> Visitor<'de> for TableIdVisitor {
137            type Value = TableId;
138
139            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
140                formatter.write_str("a string in the format 'namespace.table'")
141            }
142
143            fn visit_str<E>(self, value: &str) -> Result<TableId, E>
144            where
145                E: Error,
146            {
147                // Parse "namespace.table" format
148                let mut parts = value.splitn(2, '.');
149                let namespace = parts.next();
150                let table = parts.next();
151                match (namespace, table) {
152                    (Some(namespace), Some(table)) => Ok(TableId {
153                        namespace_id: NamespaceId::new(namespace),
154                        table_name: TableName::new(table),
155                    }),
156                    _ => Err(E::custom(format!("Invalid table_id format: {}", value))),
157                }
158            }
159
160            fn visit_string<E>(self, value: String) -> Result<TableId, E>
161            where
162                E: Error,
163            {
164                self.visit_str(&value)
165            }
166        }
167
168        deserializer.deserialize_str(TableIdVisitor)
169    }
170}
171
172impl AsRef<str> for TableId {
173    fn as_ref(&self) -> &str {
174        // This creates a temporary allocation. For zero-copy access,
175        // use as_storage_key() directly.
176        // This implementation is primarily for trait compatibility.
177        // In performance-critical paths, prefer as_storage_key().
178        self.namespace_id.as_str()
179    }
180}
181
182/// Implement AsRef<[u8]> for EntityStore compatibility
183///
184/// This allocates a new Vec on each call. For performance-critical paths,
185/// consider using as_storage_key() directly instead.
186impl AsRef<[u8]> for TableId {
187    fn as_ref(&self) -> &[u8] {
188        // We need to return a reference, but as_storage_key() creates a new Vec
189        // The best we can do here is to use the namespace_id bytes as a prefix
190        // In practice, the EntityStore will use as_storage_key() internally
191        // This implementation satisfies the trait bound requirement
192        self.namespace_id.as_str().as_bytes()
193    }
194}
195
196impl fmt::Display for TableId {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        write!(f, "{}:{}", self.namespace_id, self.table_name)
199    }
200}
201
202impl StorageKey for TableId {
203    fn storage_key(&self) -> Vec<u8> {
204        self.as_storage_key()
205    }
206
207    fn from_storage_key(bytes: &[u8]) -> Result<Self, String> {
208        Self::from_storage_key(bytes).ok_or_else(|| "Invalid TableId format".to_string())
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_table_id_new() {
218        let namespace_id = NamespaceId::new("ns1");
219        let table_name = TableName::new("users");
220        let table_id = TableId::new(namespace_id.clone(), table_name.clone());
221
222        assert_eq!(table_id.namespace_id(), &namespace_id);
223        assert_eq!(table_id.table_name(), &table_name);
224    }
225
226    #[test]
227    fn test_table_id_from_strings() {
228        let table_id = TableId::from_strings("ns1", "users");
229        assert_eq!(table_id.namespace_id().as_str(), "ns1");
230        assert_eq!(table_id.table_name().as_str(), "users");
231    }
232
233    #[test]
234    fn test_table_id_try_from_strings() {
235        let table_id = TableId::try_from_strings("ns1", "users").unwrap();
236        assert_eq!(table_id.namespace_id().as_str(), "ns1");
237        assert_eq!(table_id.table_name().as_str(), "users");
238    }
239
240    #[test]
241    fn test_table_id_try_from_strings_invalid() {
242        let err = TableId::try_from_strings("../ns1", "users").unwrap_err();
243        assert!(err.contains("invalid namespace_id"));
244    }
245
246    #[test]
247    fn test_table_id_as_storage_key() {
248        let table_id = TableId::from_strings("ns1", "users");
249        let key = table_id.as_storage_key();
250        assert!(!key.is_empty());
251        let parsed = TableId::from_storage_key(&key).unwrap();
252        assert_eq!(parsed, table_id);
253    }
254
255    #[test]
256    fn test_table_id_from_storage_key() {
257        let key = TableId::from_strings("ns1", "users").as_storage_key();
258        let table_id = TableId::from_storage_key(&key).unwrap();
259
260        assert_eq!(table_id.namespace_id().as_str(), "ns1");
261        assert_eq!(table_id.table_name().as_str(), "users");
262    }
263
264    #[test]
265    fn test_table_id_roundtrip() {
266        let original = TableId::from_strings("ns1", "users");
267        let key = original.as_storage_key();
268        let parsed = TableId::from_storage_key(&key).unwrap();
269
270        assert_eq!(original, parsed);
271    }
272
273    #[test]
274    fn test_table_id_display() {
275        let table_id = TableId::from_strings("ns1", "users");
276        assert_eq!(format!("{}", table_id), "ns1:users");
277    }
278
279    #[test]
280    fn test_table_id_full_name() {
281        let table_id = TableId::from_strings("app", "messages");
282        assert_eq!(table_id.full_name(), "app.messages");
283
284        let table_id2 = TableId::from_strings("my_namespace", "user_table");
285        assert_eq!(table_id2.full_name(), "my_namespace.user_table");
286    }
287
288    #[test]
289    fn test_table_id_serialization() {
290        let table_id = TableId::from_strings("ns1", "users");
291        let json = serde_json::to_string(&table_id).unwrap();
292        let deserialized: TableId = serde_json::from_str(&json).unwrap();
293        assert_eq!(table_id, deserialized);
294    }
295
296    #[test]
297    fn test_table_id_into_parts() {
298        let table_id = TableId::from_strings("ns1", "users");
299        let (namespace_id, table_name) = table_id.into_parts();
300
301        assert_eq!(namespace_id.as_str(), "ns1");
302        assert_eq!(table_name.as_str(), "users");
303    }
304
305    #[test]
306    fn test_table_id_with_underscore_namespace() {
307        let table_id = TableId::try_from_strings("my_namespace", "table_name").unwrap();
308        let key = table_id.as_storage_key();
309        let parsed = TableId::from_storage_key(&key).unwrap();
310
311        assert_eq!(table_id, parsed);
312    }
313}