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