iceberg/catalog/
session.rs1use std::collections::HashMap;
21use std::fmt::Debug;
22
23use async_trait::async_trait;
24#[cfg(test)]
25use mockall::automock;
26use typed_builder::TypedBuilder;
27use uuid::Uuid;
28use zeroize::Zeroizing;
29
30use crate::table::Table;
31use crate::{Namespace, NamespaceIdent, Result, TableCommit, TableCreation, TableIdent};
32
33#[derive(Debug, Clone, TypedBuilder)]
47pub struct SessionContext {
48 #[builder(default=Uuid::new_v4().to_string())]
54 session_id: String,
55
56 #[builder(default, setter(strip_option))]
58 identity: Option<String>,
59
60 #[builder(default)]
61 properties: HashMap<String, String>,
62
63 #[builder(default)]
64 credentials: HashMap<String, Credential>,
65}
66
67impl SessionContext {
68 pub fn empty() -> Self {
70 Self::builder().build()
71 }
72
73 pub fn session_id(&self) -> &str {
77 &self.session_id
78 }
79
80 pub fn identity(&self) -> Option<&str> {
82 self.identity.as_deref()
83 }
84
85 pub fn properties(&self) -> &HashMap<String, String> {
87 &self.properties
88 }
89
90 pub fn credentials(&self) -> &HashMap<String, Credential> {
92 &self.credentials
93 }
94}
95
96#[derive(Clone)]
111pub struct Credential(Zeroizing<String>);
112
113impl Credential {
114 pub fn expose(&self) -> &str {
116 &self.0
117 }
118}
119
120impl Debug for Credential {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 f.write_str("Credential([REDACTED])")
123 }
124}
125
126impl From<String> for Credential {
127 fn from(value: String) -> Self {
128 Self(Zeroizing::new(value))
129 }
130}
131
132#[async_trait]
134#[cfg_attr(test, automock)]
135pub trait SessionCatalog: Debug + Send + Sync {
136 async fn list_namespaces(
138 &self,
139 context: &SessionContext,
140 parent: Option<&NamespaceIdent>,
141 ) -> Result<Vec<NamespaceIdent>>;
142
143 async fn create_namespace(
145 &self,
146 context: &SessionContext,
147 namespace: &NamespaceIdent,
148 properties: HashMap<String, String>,
149 ) -> Result<Namespace>;
150
151 async fn get_namespace(
153 &self,
154 context: &SessionContext,
155 namespace: &NamespaceIdent,
156 ) -> Result<Namespace>;
157
158 async fn namespace_exists(
160 &self,
161 context: &SessionContext,
162 namespace: &NamespaceIdent,
163 ) -> Result<bool>;
164
165 async fn update_namespace(
171 &self,
172 context: &SessionContext,
173 namespace: &NamespaceIdent,
174 properties: HashMap<String, String>,
175 ) -> Result<()>;
176
177 async fn drop_namespace(
179 &self,
180 context: &SessionContext,
181 namespace: &NamespaceIdent,
182 ) -> Result<()>;
183
184 async fn list_tables(
186 &self,
187 context: &SessionContext,
188 namespace: &NamespaceIdent,
189 ) -> Result<Vec<TableIdent>>;
190
191 async fn create_table(
193 &self,
194 context: &SessionContext,
195 namespace: &NamespaceIdent,
196 creation: TableCreation,
197 ) -> Result<Table>;
198
199 async fn load_table(&self, context: &SessionContext, table: &TableIdent) -> Result<Table>;
201
202 async fn drop_table(&self, context: &SessionContext, table: &TableIdent) -> Result<()>;
204
205 async fn purge_table(&self, context: &SessionContext, table: &TableIdent) -> Result<()>;
212
213 async fn table_exists(&self, context: &SessionContext, table: &TableIdent) -> Result<bool>;
215
216 async fn rename_table(
218 &self,
219 context: &SessionContext,
220 src: &TableIdent,
221 dest: &TableIdent,
222 ) -> Result<()>;
223
224 async fn register_table(
226 &self,
227 context: &SessionContext,
228 table: &TableIdent,
229 metadata_location: String,
230 ) -> Result<Table>;
231
232 async fn update_table(&self, context: &SessionContext, commit: TableCommit) -> Result<Table>;
234}
235
236#[cfg(test)]
237mod tests {
238 use std::collections::HashMap;
239
240 use uuid::Uuid;
241
242 use crate::{Credential, SessionCatalog, SessionContext};
243
244 #[test]
245 fn test_empty_session_context_has_uuid_session_id() {
246 let session = SessionContext::empty();
247 let session_id = session.session_id();
248
249 assert!(Uuid::parse_str(session_id).is_ok());
250 }
251
252 #[test]
253 fn test_empty_sessions_get_unique_id() {
254 let session1 = SessionContext::empty();
255 let session2 = SessionContext::empty();
256
257 assert_ne!(session1.session_id(), session2.session_id())
258 }
259
260 #[test]
261 fn test_empty_sessions_get_unique_id_via_builder() {
262 let session1 = SessionContext::builder().build();
263 let session2 = SessionContext::builder().build();
264
265 assert_ne!(session1.session_id(), session2.session_id());
266 }
267
268 #[test]
269 fn test_session_with_credentials_does_not_display_them() {
270 let sensitive_value = "my-pw-123456";
271 let session = SessionContext::builder()
272 .credentials(HashMap::from([(
273 "key".to_string(),
274 Credential::from(sensitive_value.to_string()),
275 )]))
276 .build();
277
278 let logged = format!("{:?}", session);
279 assert!(!logged.contains(sensitive_value))
280 }
281
282 #[test]
283 fn test_credential_redacts_value() {
284 let sensitive_value = "my-pw-12346";
285
286 let logged = format!("{:?}", Credential::from(sensitive_value.to_string()));
287 assert!(!logged.contains(sensitive_value));
288 }
289
290 #[test]
291 fn test_types_are_send_sync() {
292 assert_send_sync::<Credential>();
293 assert_send_sync::<SessionContext>();
294 assert_send_sync::<dyn SessionCatalog>();
295
296 fn _dyn_compatible(_: &dyn SessionCatalog) {}
297 }
298
299 fn assert_send_sync<T: Send + Sync + ?Sized>() {}
300}