Skip to main content

iceberg/catalog/
session.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Session catalog API for Apache Iceberg.
19
20use 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/// Context for a session.
34///
35/// # Example
36/// ```rust
37/// use iceberg::SessionContext;
38///
39/// let session = SessionContext::builder()
40///     .identity("user123".to_string())
41///     .build();
42///
43/// assert_eq!(session.identity(), Some("user123"));
44/// assert!(!session.session_id().is_empty());
45/// ```
46#[derive(Debug, Clone, TypedBuilder)]
47pub struct SessionContext {
48    /// The unique identifier for this session.
49    ///
50    /// Note that the session_id may be used for caching session-scoped state
51    /// and re-use of a session_id with different session context may result in
52    /// unexpected behavior.
53    #[builder(default=Uuid::new_v4().to_string())]
54    session_id: String,
55
56    /// An optional user or principal associated with the session.
57    #[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    /// Creates a new unique but empty session.
69    pub fn empty() -> Self {
70        Self::builder().build()
71    }
72
73    /// Returns the identifier for this session.
74    ///
75    /// The identifier may be used for caching state within a session.
76    pub fn session_id(&self) -> &str {
77        &self.session_id
78    }
79
80    /// Returns a string that identifies the current user or principal.
81    pub fn identity(&self) -> Option<&str> {
82        self.identity.as_deref()
83    }
84
85    /// Returns a map of properties currently set for the session.
86    pub fn properties(&self) -> &HashMap<String, String> {
87        &self.properties
88    }
89
90    /// Returns the session's credential map.
91    pub fn credentials(&self) -> &HashMap<String, Credential> {
92        &self.credentials
93    }
94}
95
96/// A string-like type containing sensitive information such as passwords or tokens.
97///
98/// It is redacted from logs and automatically zeroized.
99///
100/// # Example
101/// ```rust
102/// use iceberg::Credential;
103///
104/// let sensitive_value = "my-pw-12345";
105/// let credential = Credential::from(sensitive_value.to_string());
106///
107/// // Not contained in debug logs.
108/// assert!(!format!("{:?}", credential).contains(sensitive_value));
109/// ```
110#[derive(Clone)]
111pub struct Credential(Zeroizing<String>);
112
113impl Credential {
114    /// Returns the raw value of the credential.
115    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/// The catalog API for Iceberg Rust that includes session handling.
133#[async_trait]
134#[cfg_attr(test, automock)]
135pub trait SessionCatalog: Debug + Send + Sync {
136    /// List namespaces inside the catalog.
137    async fn list_namespaces(
138        &self,
139        context: &SessionContext,
140        parent: Option<&NamespaceIdent>,
141    ) -> Result<Vec<NamespaceIdent>>;
142
143    /// Create a new namespace inside the catalog.
144    async fn create_namespace(
145        &self,
146        context: &SessionContext,
147        namespace: &NamespaceIdent,
148        properties: HashMap<String, String>,
149    ) -> Result<Namespace>;
150
151    /// Get a namespace information from the catalog.
152    async fn get_namespace(
153        &self,
154        context: &SessionContext,
155        namespace: &NamespaceIdent,
156    ) -> Result<Namespace>;
157
158    /// Check if namespace exists in catalog.
159    async fn namespace_exists(
160        &self,
161        context: &SessionContext,
162        namespace: &NamespaceIdent,
163    ) -> Result<bool>;
164
165    /// Update a namespace inside the catalog.
166    ///
167    /// # Behavior
168    ///
169    /// The properties must be the full set of namespace.
170    async fn update_namespace(
171        &self,
172        context: &SessionContext,
173        namespace: &NamespaceIdent,
174        properties: HashMap<String, String>,
175    ) -> Result<()>;
176
177    /// Drop a namespace from the catalog, or returns error if it doesn't exist.
178    async fn drop_namespace(
179        &self,
180        context: &SessionContext,
181        namespace: &NamespaceIdent,
182    ) -> Result<()>;
183
184    /// List tables from namespace.
185    async fn list_tables(
186        &self,
187        context: &SessionContext,
188        namespace: &NamespaceIdent,
189    ) -> Result<Vec<TableIdent>>;
190
191    /// Create a new table inside the namespace.
192    async fn create_table(
193        &self,
194        context: &SessionContext,
195        namespace: &NamespaceIdent,
196        creation: TableCreation,
197    ) -> Result<Table>;
198
199    /// Load table from the catalog.
200    async fn load_table(&self, context: &SessionContext, table: &TableIdent) -> Result<Table>;
201
202    /// Drop a table from the catalog, or returns error if it doesn't exist.
203    async fn drop_table(&self, context: &SessionContext, table: &TableIdent) -> Result<()>;
204
205    /// Drop a table from the catalog and delete the underlying table data.
206    ///
207    /// Implementations should load the table metadata, drop the table
208    /// from the catalog, then delete all associated data and metadata files.
209    /// The [`drop_table_data`](super::utils::drop_table_data) utility function can
210    /// be used for the file cleanup step.
211    async fn purge_table(&self, context: &SessionContext, table: &TableIdent) -> Result<()>;
212
213    /// Check if a table exists in the catalog.
214    async fn table_exists(&self, context: &SessionContext, table: &TableIdent) -> Result<bool>;
215
216    /// Rename a table in the catalog.
217    async fn rename_table(
218        &self,
219        context: &SessionContext,
220        src: &TableIdent,
221        dest: &TableIdent,
222    ) -> Result<()>;
223
224    /// Register an existing table to the catalog.
225    async fn register_table(
226        &self,
227        context: &SessionContext,
228        table: &TableIdent,
229        metadata_location: String,
230    ) -> Result<Table>;
231
232    /// Update a table to the catalog.
233    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}