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 test_provider_round_trip() -> Result<()> {
251 use unitycatalog_common::models::providers::v1::{Provider, ProviderAuthenticationType};
252
253 let store = test_store();
254 let resource: unitycatalog_common::models::Resource = Provider {
255 name: "acme".into(),
256 authentication_type: ProviderAuthenticationType::Token.into(),
257 comment: Some("inbound share from acme".into()),
258 ..Default::default()
259 }
260 .into();
261
262 let (created, reference) = store.create(resource.clone()).await?;
264 assert_eq!(created.resource_name(), resource.resource_name());
265
266 let ident = ObjectLabel::Provider.to_ident(reference);
269 let (retrieved, _) = store.get(&ident).await?;
270 assert_eq!(retrieved, created);
271 let provider: Provider = retrieved.try_into()?;
272 assert_eq!(provider.name, "acme");
273 assert_eq!(provider.comment.as_deref(), Some("inbound share from acme"));
274
275 let (resources, _) = store.list(&ObjectLabel::Provider, None, None, None).await?;
277 assert_eq!(resources.len(), 1);
278
279 store.delete(&ident).await?;
280 assert!(matches!(
281 store.get(&ident).await.unwrap_err(),
282 Error::NotFound
283 ));
284 Ok(())
285 }
286
287 #[tokio::test]
288 async fn update_checked_cas_succeeds_on_match_conflicts_on_stale() -> Result<()> {
289 use unitycatalog_common::store::Precondition;
290
291 let store = test_store();
292 let (_, reference) = store
293 .create(
294 Catalog {
295 name: "cat".into(),
296 ..Default::default()
297 }
298 .into(),
299 )
300 .await?;
301 let ident = ObjectLabel::Catalog.to_ident(reference);
302
303 let (resource, _, version) = store.get_versioned(&ident).await?;
305 let mut catalog: Catalog = resource.try_into()?;
306 catalog.comment = Some("first".into());
307
308 let (_, _, new_version) = store
310 .update_checked(
311 &ident,
312 catalog.clone().into(),
313 Precondition::Version(version),
314 )
315 .await?;
316 assert!(new_version > version, "version must advance on update");
317
318 catalog.comment = Some("second".into());
320 let res = store
321 .update_checked(&ident, catalog.into(), Precondition::Version(version))
322 .await;
323 assert!(matches!(res, Err(Error::Conflict)), "{res:?}");
324 Ok(())
325 }
326
327 #[tokio::test]
328 async fn rename_preserves_id_and_associations() -> Result<()> {
329 use unitycatalog_common::models::AssociationLabel;
330 use unitycatalog_common::models::ResourceName;
331 use unitycatalog_common::store::Precondition;
332
333 let store = test_store();
334 let (_, from_ref) = store
336 .create(
337 Catalog {
338 name: "old_name".into(),
339 ..Default::default()
340 }
341 .into(),
342 )
343 .await?;
344 let (_, to_ref) = store
345 .create(
346 Catalog {
347 name: "peer".into(),
348 ..Default::default()
349 }
350 .into(),
351 )
352 .await?;
353 let from_ident = ObjectLabel::Catalog.to_ident(from_ref.clone());
354 let to_ident = ObjectLabel::Catalog.to_ident(to_ref);
355 store
356 .add_association(&from_ident, &to_ident, &AssociationLabel::ParentOf, None)
357 .await?;
358
359 let (_, renamed_ref, _) = store
361 .rename(
362 &from_ident,
363 &ResourceName::new(["new_name"]),
364 Precondition::Any,
365 )
366 .await?;
367 assert_eq!(renamed_ref, from_ref, "rename must preserve the id");
368
369 let renamed: Catalog = store.get(&from_ident).await?.0.try_into()?;
370 assert_eq!(renamed.name, "new_name");
371
372 let (edges, _) = store
373 .list_associations(&from_ident, &AssociationLabel::ParentOf, None, None, None)
374 .await?;
375 assert_eq!(edges.len(), 1, "association must survive the rename");
376 Ok(())
377 }
378}