1use std::str::FromStr;
13use std::sync::Arc;
14
15use olai_store::{InMemoryStore, ManagedObjectStore, ResourceRegistry};
16use unitycatalog_common::models::AssociationLabel;
17use unitycatalog_common::models::ObjectLabel;
18use unitycatalog_common::models::labels::RESOURCE_DESCRIPTORS;
19use unitycatalog_common::services::encryption::EnvelopeEncryptor;
20use unitycatalog_common::store::{ObjectStoreAdapter, ProvidesResourceStore, ResourceStore};
21
22fn inverse_resolver(label: &str) -> Option<String> {
25 AssociationLabel::from_str(label)
26 .ok()
27 .and_then(|l| l.inverse())
28 .map(|inv| inv.to_string())
29}
30
31type MemoryAdapter =
34 ObjectStoreAdapter<ManagedObjectStore<ObjectLabel, InMemoryStore<ObjectLabel>>>;
35
36#[derive(Clone)]
42pub struct InMemoryResourceStore {
43 store: Arc<MemoryAdapter>,
44}
45
46impl InMemoryResourceStore {
47 pub fn new(encryptor: EnvelopeEncryptor) -> Self {
48 let inner = InMemoryStore::<ObjectLabel>::with_inverse(inverse_resolver);
49 let registry = ResourceRegistry::from_static(RESOURCE_DESCRIPTORS);
50 let managed = ManagedObjectStore::with_encryptor(inner, encryptor, registry);
51 Self {
52 store: Arc::new(ObjectStoreAdapter::new(managed)),
53 }
54 }
55}
56
57impl ProvidesResourceStore for InMemoryResourceStore {
58 fn store(&self) -> &dyn ResourceStore {
59 self.store.as_ref()
60 }
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use unitycatalog_common::models::{Catalog, ObjectLabel, ResourceExt, ResourceRef};
67 use unitycatalog_common::services::encryption::LocalKeyProvider;
68 use unitycatalog_common::store::{ResourceStore, ResourceStoreReader};
69 use unitycatalog_common::{Error, Result};
70 use uuid::Uuid;
71
72 fn test_store() -> InMemoryResourceStore {
73 let encryptor =
74 EnvelopeEncryptor::local(LocalKeyProvider::single("test", vec![0x42; 32]).unwrap());
75 InMemoryResourceStore::new(encryptor)
76 }
77
78 #[tokio::test]
79 async fn test_create_get_delete() -> Result<()> {
80 let store = test_store();
81 let resource: unitycatalog_common::models::Resource = Catalog {
82 name: "new_catalog".into(),
83 ..Default::default()
84 }
85 .into();
86 let (created, reference) = store.create(resource.clone()).await?;
87 assert_eq!(created.resource_name(), resource.resource_name());
88
89 let ident = ObjectLabel::Catalog.to_ident(reference);
90 let (retrieved, _) = store.get(&ident).await?;
91 assert_eq!(retrieved, created);
92
93 store.delete(&ident).await?;
94 let result = store.get(&ident).await;
95 assert!(matches!(result.unwrap_err(), Error::NotFound));
96 Ok(())
97 }
98
99 #[tokio::test]
100 async fn create_honors_pre_allocated_id() -> Result<()> {
101 use unitycatalog_common::models::volumes::v1::Volume;
102
103 let store = test_store();
104 let id = Uuid::new_v4();
105 let resource: unitycatalog_common::models::Resource = Volume {
106 name: "vol".into(),
107 catalog_name: "cat".into(),
108 schema_name: "sch".into(),
109 volume_id: id.hyphenated().to_string(),
110 ..Default::default()
111 }
112 .into();
113
114 let (_, reference) = store.create(resource).await?;
115 assert_eq!(reference, ResourceRef::Uuid(id));
117 Ok(())
118 }
119
120 #[tokio::test]
121 async fn create_generates_id_when_absent() -> Result<()> {
122 let store = test_store();
124 let resource: unitycatalog_common::models::Resource = Catalog {
125 name: "cat".into(),
126 ..Default::default()
127 }
128 .into();
129 let (_, reference) = store.create(resource).await?;
130 let ResourceRef::Uuid(id) = reference else {
131 panic!("expected a uuid reference, got {reference:?}");
132 };
133 assert!(!id.is_nil(), "store should mint a non-nil id");
134 Ok(())
135 }
136
137 #[tokio::test]
138 async fn create_at_same_uuid_is_rejected() -> Result<()> {
139 use unitycatalog_common::models::staging_tables::v1::StagingTable;
140 use unitycatalog_common::models::tables::v1::Table;
141
142 let store = test_store();
148 let id = Uuid::new_v4();
149 let id_str = id.hyphenated().to_string();
150
151 store
152 .create(
153 StagingTable {
154 name: "t".into(),
155 catalog_name: "cat".into(),
156 schema_name: "sch".into(),
157 id: id_str.clone(),
158 ..Default::default()
159 }
160 .into(),
161 )
162 .await?;
163 let res = store
166 .create(
167 Table {
168 name: "t".into(),
169 catalog_name: "cat".into(),
170 schema_name: "sch".into(),
171 table_id: Some(id_str.clone()),
172 ..Default::default()
173 }
174 .into(),
175 )
176 .await;
177 assert!(matches!(res, Err(Error::AlreadyExists)), "{res:?}");
178
179 let staging_ident = ObjectLabel::StagingTable.to_ident(ResourceRef::Uuid(id));
181 let staging: StagingTable = store.get(&staging_ident).await?.0.try_into()?;
182 assert_eq!(staging.id, id_str);
183 Ok(())
184 }
185
186 #[tokio::test]
187 async fn create_rejects_duplicate_pre_allocated_id() -> Result<()> {
188 use unitycatalog_common::models::volumes::v1::Volume;
189
190 let store = test_store();
191 let id = Uuid::new_v4();
192 let volume = |name: &str| -> unitycatalog_common::models::Resource {
193 Volume {
194 name: name.into(),
195 catalog_name: "cat".into(),
196 schema_name: "sch".into(),
197 volume_id: id.hyphenated().to_string(),
198 ..Default::default()
199 }
200 .into()
201 };
202 store.create(volume("a")).await?;
203 let res = store.create(volume("b")).await;
206 assert!(matches!(res, Err(Error::AlreadyExists)), "{res:?}");
207 Ok(())
208 }
209
210 #[tokio::test]
211 async fn test_list() -> Result<()> {
212 let store = test_store();
213 let resource: unitycatalog_common::models::Resource = Catalog {
214 name: "new_catalog".into(),
215 ..Default::default()
216 }
217 .into();
218 let (created, _) = store.create(resource.clone()).await?;
219
220 let (resources, next) = store.list(&ObjectLabel::Catalog, None, None, None).await?;
221 assert_eq!(resources.len(), 1);
222 assert_eq!(resources[0], created);
223 assert!(next.is_none());
224
225 for name in ["new_catalog2", "new_catalog3"] {
227 let resource: unitycatalog_common::models::Resource = Catalog {
228 name: name.into(),
229 ..Default::default()
230 }
231 .into();
232 store.create(resource).await?;
233 }
234
235 let (resources, next) = store
236 .list(&ObjectLabel::Catalog, None, Some(2), None)
237 .await?;
238 assert_eq!(resources.len(), 2);
239 assert!(next.is_some());
240
241 let (resources, next) = store
242 .list(&ObjectLabel::Catalog, None, Some(2), next)
243 .await?;
244 assert_eq!(resources.len(), 1);
245 assert!(next.is_none());
246 Ok(())
247 }
248
249 #[tokio::test]
250 async fn update_checked_cas_succeeds_on_match_conflicts_on_stale() -> Result<()> {
251 use unitycatalog_common::store::Precondition;
252
253 let store = test_store();
254 let (_, reference) = store
255 .create(
256 Catalog {
257 name: "cat".into(),
258 ..Default::default()
259 }
260 .into(),
261 )
262 .await?;
263 let ident = ObjectLabel::Catalog.to_ident(reference);
264
265 let (resource, _, version) = store.get_versioned(&ident).await?;
267 let mut catalog: Catalog = resource.try_into()?;
268 catalog.comment = Some("first".into());
269
270 let (_, _, new_version) = store
272 .update_checked(
273 &ident,
274 catalog.clone().into(),
275 Precondition::Version(version),
276 )
277 .await?;
278 assert!(new_version > version, "version must advance on update");
279
280 catalog.comment = Some("second".into());
282 let res = store
283 .update_checked(&ident, catalog.into(), Precondition::Version(version))
284 .await;
285 assert!(matches!(res, Err(Error::Conflict)), "{res:?}");
286 Ok(())
287 }
288
289 #[tokio::test]
290 async fn rename_preserves_id_and_associations() -> Result<()> {
291 use unitycatalog_common::models::AssociationLabel;
292 use unitycatalog_common::models::ResourceName;
293 use unitycatalog_common::store::Precondition;
294
295 let store = test_store();
296 let (_, from_ref) = store
298 .create(
299 Catalog {
300 name: "old_name".into(),
301 ..Default::default()
302 }
303 .into(),
304 )
305 .await?;
306 let (_, to_ref) = store
307 .create(
308 Catalog {
309 name: "peer".into(),
310 ..Default::default()
311 }
312 .into(),
313 )
314 .await?;
315 let from_ident = ObjectLabel::Catalog.to_ident(from_ref.clone());
316 let to_ident = ObjectLabel::Catalog.to_ident(to_ref);
317 store
318 .add_association(&from_ident, &to_ident, &AssociationLabel::ParentOf, None)
319 .await?;
320
321 let (_, renamed_ref, _) = store
323 .rename(
324 &from_ident,
325 &ResourceName::new(["new_name"]),
326 Precondition::Any,
327 )
328 .await?;
329 assert_eq!(renamed_ref, from_ref, "rename must preserve the id");
330
331 let renamed: Catalog = store.get(&from_ident).await?.0.try_into()?;
332 assert_eq!(renamed.name, "new_name");
333
334 let (edges, _) = store
335 .list_associations(&from_ident, &AssociationLabel::ParentOf, None, None, None)
336 .await?;
337 assert_eq!(edges.len(), 1, "association must survive the rename");
338 Ok(())
339 }
340}