1use std::fmt;
12use std::path::PathBuf;
13use std::sync::Arc;
14use std::time::SystemTime;
15
16use async_trait::async_trait;
17use corium_log::{LogError, MemLogRegistry, TransactionLog, TxRecord, VersionedLog};
18#[cfg(any(feature = "postgres", feature = "turso", feature = "s3"))]
19use corium_log::{NativeLogStorage, NativeVersionedLog};
20use corium_store::{BlobId, BlobIdStream, BlobStore, FsStore, MemoryStore, RootStore, StoreError};
21
22#[cfg(feature = "postgres")]
23use corium_store::PostgresBlobStore;
24#[cfg(feature = "s3")]
25use corium_store::S3BlobStore;
26#[cfg(feature = "turso")]
27use corium_store::TursoBlobStore;
28
29#[derive(Clone, Default)]
31pub enum StoreSpec {
32 Memory,
36 #[default]
38 Fs,
39 #[cfg(feature = "postgres")]
41 Postgres {
42 connection_string: String,
44 },
45 #[cfg(feature = "turso")]
48 Turso {
49 path: String,
51 },
52 #[cfg(feature = "s3")]
54 S3 {
55 bucket: String,
57 prefix: String,
59 },
60}
61
62impl fmt::Debug for StoreSpec {
63 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
64 match self {
65 Self::Memory => formatter.write_str("Memory"),
66 Self::Fs => formatter.write_str("Fs"),
67 #[cfg(feature = "postgres")]
68 Self::Postgres { .. } => formatter
69 .debug_struct("Postgres")
70 .field("connection_string", &"[REDACTED]")
71 .finish(),
72 #[cfg(feature = "turso")]
73 Self::Turso { path } => formatter.debug_struct("Turso").field("path", path).finish(),
74 #[cfg(feature = "s3")]
75 Self::S3 { bucket, prefix } => formatter
76 .debug_struct("S3")
77 .field("bucket", bucket)
78 .field("prefix", prefix)
79 .finish(),
80 }
81 }
82}
83
84#[derive(Debug, thiserror::Error)]
89pub enum StorageConnectionError {
90 #[error("transactor returned no storage backend")]
92 Missing,
93 #[error("{0}")]
96 Unsupported(String),
97}
98
99impl StoreSpec {
100 pub fn from_connection(
110 connection: corium_protocol::pb::StorageConnection,
111 ) -> Result<(Self, PathBuf), StorageConnectionError> {
112 use corium_protocol::pb::storage_connection::Backend;
113
114 let backend = connection.backend.ok_or(StorageConnectionError::Missing)?;
115 let resolved = match backend {
116 Backend::Memory(_) => {
117 return Err(StorageConnectionError::Unsupported(
118 "memory storage is confined to the transactor process".into(),
119 ));
120 }
121 Backend::Filesystem(storage) => (Self::Fs, PathBuf::from(storage.data_dir)),
122 Backend::Postgres(storage) => {
123 #[cfg(feature = "postgres")]
124 {
125 (
126 Self::Postgres {
127 connection_string: storage.connection_string,
128 },
129 PathBuf::new(),
130 )
131 }
132 #[cfg(not(feature = "postgres"))]
133 {
134 let _ = storage;
135 return Err(StorageConnectionError::Unsupported(
136 "this build lacks PostgreSQL support".into(),
137 ));
138 }
139 }
140 Backend::Turso(storage) => {
141 #[cfg(feature = "turso")]
142 {
143 (Self::Turso { path: storage.path }, PathBuf::new())
144 }
145 #[cfg(not(feature = "turso"))]
146 {
147 let _ = storage;
148 return Err(StorageConnectionError::Unsupported(
149 "this build lacks Turso support".into(),
150 ));
151 }
152 }
153 Backend::S3(storage) => {
154 #[cfg(feature = "s3")]
155 {
156 (
157 Self::S3 {
158 bucket: storage.bucket,
159 prefix: storage.prefix,
160 },
161 PathBuf::new(),
162 )
163 }
164 #[cfg(not(feature = "s3"))]
165 {
166 let _ = storage;
167 return Err(StorageConnectionError::Unsupported(
168 "this build lacks S3 support".into(),
169 ));
170 }
171 }
172 };
173 Ok(resolved)
174 }
175
176 pub fn connection_info(
186 &self,
187 data_dir: &std::path::Path,
188 ) -> Result<corium_protocol::pb::StorageConnection, String> {
189 use corium_protocol::pb;
190 use pb::storage_connection::Backend;
191
192 fn absolute(path: &std::path::Path) -> Result<String, String> {
193 let path = if path.is_absolute() {
194 path.to_path_buf()
195 } else {
196 std::env::current_dir()
197 .map_err(|error| error.to_string())?
198 .join(path)
199 };
200 path.into_os_string()
201 .into_string()
202 .map_err(|_| "storage path is not valid UTF-8".to_owned())
203 }
204
205 let backend = match self {
206 Self::Memory => Backend::Memory(pb::MemoryStorage {}),
207 Self::Fs => Backend::Filesystem(pb::FilesystemStorage {
208 data_dir: absolute(data_dir)?,
209 }),
210 #[cfg(feature = "postgres")]
211 Self::Postgres { connection_string } => Backend::Postgres(pb::PostgreSqlStorage {
212 connection_string: connection_string.clone(),
213 }),
214 #[cfg(feature = "turso")]
215 Self::Turso { path } => Backend::Turso(pb::TursoStorage {
216 path: absolute(std::path::Path::new(path))?,
217 }),
218 #[cfg(feature = "s3")]
219 Self::S3 { bucket, prefix } => Backend::S3(pb::S3Storage {
220 bucket: bucket.clone(),
221 prefix: prefix.clone(),
222 }),
223 };
224 Ok(pb::StorageConnection {
225 backend: Some(backend),
226 })
227 }
228}
229
230pub enum NodeStore {
235 Mem(MemoryStore),
237 Fs(FsStore),
239 #[cfg(feature = "postgres")]
241 Postgres(PostgresBlobStore),
242 #[cfg(feature = "turso")]
244 Turso(TursoBlobStore),
245 #[cfg(feature = "s3")]
247 S3(S3BlobStore),
248}
249
250impl NodeStore {
251 #[allow(clippy::unused_async)]
258 pub async fn open(spec: &StoreSpec, data_dir: &std::path::Path) -> Result<Self, StoreError> {
259 match spec {
260 StoreSpec::Memory => Ok(Self::Mem(MemoryStore::default())),
261 StoreSpec::Fs => Ok(Self::Fs(FsStore::open(data_dir.join("store"))?)),
262 #[cfg(feature = "postgres")]
263 StoreSpec::Postgres { connection_string } => Ok(Self::Postgres(
264 PostgresBlobStore::connect(connection_string).await?,
265 )),
266 #[cfg(feature = "turso")]
267 StoreSpec::Turso { path } => Ok(Self::Turso(TursoBlobStore::open(path).await?)),
268 #[cfg(feature = "s3")]
269 StoreSpec::S3 { bucket, prefix } => {
270 Ok(Self::S3(S3BlobStore::connect(bucket, prefix).await?))
271 }
272 }
273 }
274
275 #[allow(clippy::unused_async)]
281 pub async fn open_existing(
282 spec: &StoreSpec,
283 data_dir: &std::path::Path,
284 ) -> Result<Self, StoreError> {
285 match spec {
286 StoreSpec::Memory => Ok(Self::Mem(MemoryStore::default())),
287 StoreSpec::Fs => Ok(Self::Fs(FsStore::open(data_dir.join("store"))?)),
288 #[cfg(feature = "postgres")]
289 StoreSpec::Postgres { connection_string } => Ok(Self::Postgres(
290 PostgresBlobStore::connect_existing(connection_string).await?,
291 )),
292 #[cfg(feature = "turso")]
293 StoreSpec::Turso { path } => {
294 Ok(Self::Turso(TursoBlobStore::open_existing(path).await?))
295 }
296 #[cfg(feature = "s3")]
297 StoreSpec::S3 { bucket, prefix } => {
298 Ok(Self::S3(S3BlobStore::connect(bucket, prefix).await?))
299 }
300 }
301 }
302}
303
304#[async_trait]
305impl BlobStore for NodeStore {
306 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
307 match self {
308 Self::Mem(store) => store.put(bytes).await,
309 Self::Fs(store) => store.put(bytes).await,
310 #[cfg(feature = "postgres")]
311 Self::Postgres(store) => store.put(bytes).await,
312 #[cfg(feature = "turso")]
313 Self::Turso(store) => store.put(bytes).await,
314 #[cfg(feature = "s3")]
315 Self::S3(store) => store.put(bytes).await,
316 }
317 }
318
319 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
320 match self {
321 Self::Mem(store) => store.get(id).await,
322 Self::Fs(store) => store.get(id).await,
323 #[cfg(feature = "postgres")]
324 Self::Postgres(store) => store.get(id).await,
325 #[cfg(feature = "turso")]
326 Self::Turso(store) => store.get(id).await,
327 #[cfg(feature = "s3")]
328 Self::S3(store) => store.get(id).await,
329 }
330 }
331
332 async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
333 match self {
334 Self::Mem(store) => store.contains(id).await,
335 Self::Fs(store) => store.contains(id).await,
336 #[cfg(feature = "postgres")]
337 Self::Postgres(store) => store.contains(id).await,
338 #[cfg(feature = "turso")]
339 Self::Turso(store) => store.contains(id).await,
340 #[cfg(feature = "s3")]
341 Self::S3(store) => store.contains(id).await,
342 }
343 }
344
345 async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
346 match self {
347 Self::Mem(store) => store.delete(id).await,
348 Self::Fs(store) => store.delete(id).await,
349 #[cfg(feature = "postgres")]
350 Self::Postgres(store) => store.delete(id).await,
351 #[cfg(feature = "turso")]
352 Self::Turso(store) => store.delete(id).await,
353 #[cfg(feature = "s3")]
354 Self::S3(store) => store.delete(id).await,
355 }
356 }
357
358 async fn list(&self) -> Result<BlobIdStream, StoreError> {
359 match self {
360 Self::Mem(store) => store.list().await,
361 Self::Fs(store) => store.list().await,
362 #[cfg(feature = "postgres")]
363 Self::Postgres(store) => store.list().await,
364 #[cfg(feature = "turso")]
365 Self::Turso(store) => store.list().await,
366 #[cfg(feature = "s3")]
367 Self::S3(store) => store.list().await,
368 }
369 }
370
371 async fn modified_at(&self, id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
372 match self {
373 Self::Mem(store) => store.modified_at(id).await,
374 Self::Fs(store) => store.modified_at(id).await,
375 #[cfg(feature = "postgres")]
376 Self::Postgres(store) => store.modified_at(id).await,
377 #[cfg(feature = "turso")]
378 Self::Turso(store) => store.modified_at(id).await,
379 #[cfg(feature = "s3")]
380 Self::S3(store) => store.modified_at(id).await,
381 }
382 }
383}
384
385#[async_trait]
386impl RootStore for NodeStore {
387 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
388 match self {
389 Self::Mem(store) => store.get_root(name).await,
390 Self::Fs(store) => store.get_root(name).await,
391 #[cfg(feature = "postgres")]
392 Self::Postgres(store) => store.get_root(name).await,
393 #[cfg(feature = "turso")]
394 Self::Turso(store) => store.get_root(name).await,
395 #[cfg(feature = "s3")]
396 Self::S3(store) => store.get_root(name).await,
397 }
398 }
399
400 async fn cas_root(
401 &self,
402 name: &str,
403 expected: Option<&[u8]>,
404 new: &[u8],
405 ) -> Result<(), StoreError> {
406 match self {
407 Self::Mem(store) => store.cas_root(name, expected, new).await,
408 Self::Fs(store) => store.cas_root(name, expected, new).await,
409 #[cfg(feature = "postgres")]
410 Self::Postgres(store) => store.cas_root(name, expected, new).await,
411 #[cfg(feature = "turso")]
412 Self::Turso(store) => store.cas_root(name, expected, new).await,
413 #[cfg(feature = "s3")]
414 Self::S3(store) => store.cas_root(name, expected, new).await,
415 }
416 }
417
418 async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
419 match self {
420 Self::Mem(store) => store.delete_root(name).await,
421 Self::Fs(store) => store.delete_root(name).await,
422 #[cfg(feature = "postgres")]
423 Self::Postgres(store) => store.delete_root(name).await,
424 #[cfg(feature = "turso")]
425 Self::Turso(store) => store.delete_root(name).await,
426 #[cfg(feature = "s3")]
427 Self::S3(store) => store.delete_root(name).await,
428 }
429 }
430
431 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
432 match self {
433 Self::Mem(store) => store.list_roots(prefix).await,
434 Self::Fs(store) => store.list_roots(prefix).await,
435 #[cfg(feature = "postgres")]
436 Self::Postgres(store) => store.list_roots(prefix).await,
437 #[cfg(feature = "turso")]
438 Self::Turso(store) => store.list_roots(prefix).await,
439 #[cfg(feature = "s3")]
440 Self::S3(store) => store.list_roots(prefix).await,
441 }
442 }
443}
444
445pub enum LogBackend {
447 Fs(PathBuf),
449 Mem(MemLogRegistry),
451 #[cfg(any(feature = "postgres", feature = "turso", feature = "s3"))]
453 Native(Arc<dyn NativeLogStorage>),
454}
455
456struct BlockingTransactionLog(Arc<dyn TransactionLog>);
459
460#[async_trait]
461impl TransactionLog for BlockingTransactionLog {
462 fn append(&self, record: &TxRecord) -> Result<(), LogError> {
463 self.0.append(record)
464 }
465
466 async fn append_async(&self, record: &TxRecord) -> Result<(), LogError> {
467 let log = Arc::clone(&self.0);
468 let record = record.clone();
469 tokio::task::spawn_blocking(move || log.append(&record))
470 .await
471 .map_err(|error| LogError::Native(format!("log task failed: {error}")))?
472 }
473
474 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
475 self.0.tx_range(start, end)
476 }
477
478 async fn tx_range_async(
479 &self,
480 start: u64,
481 end: Option<u64>,
482 ) -> Result<Vec<TxRecord>, LogError> {
483 let log = Arc::clone(&self.0);
484 tokio::task::spawn_blocking(move || log.tx_range(start, end))
485 .await
486 .map_err(|error| LogError::Native(format!("log task failed: {error}")))?
487 }
488}
489
490struct ReadOnlyTransactionLog(Arc<dyn TransactionLog>);
493
494#[async_trait]
495impl TransactionLog for ReadOnlyTransactionLog {
496 fn append(&self, _record: &TxRecord) -> Result<(), LogError> {
497 Err(LogError::Native("transaction log is read-only".into()))
498 }
499
500 fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, LogError> {
501 self.0.tx_range(start, end)
502 }
503
504 async fn tx_range_async(
505 &self,
506 start: u64,
507 end: Option<u64>,
508 ) -> Result<Vec<TxRecord>, LogError> {
509 self.0.tx_range_async(start, end).await
510 }
511}
512
513impl LogBackend {
514 #[must_use]
516 #[allow(clippy::needless_pass_by_value)]
517 pub fn for_spec(
518 spec: &StoreSpec,
519 data_dir: &std::path::Path,
520 #[cfg_attr(
521 not(any(feature = "postgres", feature = "turso", feature = "s3")),
522 allow(unused_variables)
523 )]
524 store: Arc<NodeStore>,
525 ) -> Self {
526 match spec {
527 StoreSpec::Memory => Self::Mem(MemLogRegistry::new()),
528 StoreSpec::Fs => Self::Fs(data_dir.join("logs")),
529 #[cfg(feature = "postgres")]
530 StoreSpec::Postgres { .. } => Self::Native(Arc::new(NativeRootLogStore::new(store))),
531 #[cfg(feature = "turso")]
532 StoreSpec::Turso { .. } => Self::Native(Arc::new(NativeRootLogStore::new(store))),
533 #[cfg(feature = "s3")]
534 StoreSpec::S3 { .. } => Self::Native(Arc::new(NativeRootLogStore::new(store))),
535 }
536 }
537
538 pub async fn open(
543 &self,
544 name: &str,
545 write_version: u64,
546 ) -> Result<Arc<dyn TransactionLog>, LogError> {
547 match self {
548 Self::Fs(dir) => {
549 let dir = dir.clone();
550 let name = name.to_owned();
551 let log = tokio::task::spawn_blocking(move || {
552 VersionedLog::open(dir, &name, write_version)
553 })
554 .await
555 .map_err(|error| LogError::Native(format!("log task failed: {error}")))??;
556 Ok(Arc::new(BlockingTransactionLog(Arc::new(log))))
557 }
558 Self::Mem(registry) => Ok(Arc::new(registry.open(name, write_version))),
559 #[cfg(any(feature = "postgres", feature = "turso", feature = "s3"))]
560 Self::Native(storage) => Ok(Arc::new(
561 NativeVersionedLog::open(Arc::clone(storage), name, write_version).await?,
562 )),
563 }
564 }
565
566 pub async fn open_read_only(&self, name: &str) -> Result<Arc<dyn TransactionLog>, LogError> {
575 match self {
576 Self::Fs(dir) => {
577 let dir = dir.clone();
578 let name = name.to_owned();
579 let log =
580 tokio::task::spawn_blocking(move || VersionedLog::open_read_only(dir, &name))
581 .await
582 .map_err(|error| LogError::Native(format!("log task failed: {error}")))??;
583 Ok(Arc::new(ReadOnlyTransactionLog(Arc::new(
584 BlockingTransactionLog(Arc::new(log)),
585 ))))
586 }
587 Self::Mem(registry) => Ok(Arc::new(ReadOnlyTransactionLog(Arc::new(
588 registry.open(name, 0),
589 )))),
590 #[cfg(any(feature = "postgres", feature = "turso", feature = "s3"))]
591 Self::Native(storage) => Ok(Arc::new(ReadOnlyTransactionLog(Arc::new(
592 NativeVersionedLog::open_read_only(Arc::clone(storage), name),
593 )))),
594 }
595 }
596
597 #[must_use]
599 pub async fn exists(&self, name: &str) -> bool {
600 match self {
601 Self::Fs(dir) => {
602 let dir = dir.clone();
603 let name = name.to_owned();
604 tokio::task::spawn_blocking(move || VersionedLog::exists(dir, &name))
605 .await
606 .unwrap_or(false)
607 }
608 Self::Mem(registry) => registry.exists(name),
609 #[cfg(any(feature = "postgres", feature = "turso", feature = "s3"))]
610 Self::Native(storage) => {
611 storage
612 .list_records(name)
613 .await
614 .is_ok_and(|records| !records.is_empty())
615 || storage
616 .list_legacy_chunks(name)
617 .await
618 .is_ok_and(|chunks| !chunks.is_empty())
619 }
620 }
621 }
622
623 pub async fn delete_all(&self, name: &str) -> Result<(), LogError> {
628 match self {
629 Self::Fs(dir) => {
630 let dir = dir.clone();
631 let name = name.to_owned();
632 tokio::task::spawn_blocking(move || VersionedLog::delete_all(dir, &name))
633 .await
634 .map_err(|error| LogError::Native(format!("log task failed: {error}")))?
635 }
636 Self::Mem(registry) => {
637 registry.delete_all(name);
638 Ok(())
639 }
640 #[cfg(any(feature = "postgres", feature = "turso", feature = "s3"))]
641 Self::Native(storage) => storage.delete_all(name).await,
642 }
643 }
644}
645
646#[cfg(any(feature = "postgres", feature = "turso", feature = "s3"))]
647struct NativeRootLogStore {
648 store: Arc<NodeStore>,
649}
650
651#[cfg(any(feature = "postgres", feature = "turso", feature = "s3"))]
652impl NativeRootLogStore {
653 fn new(store: Arc<NodeStore>) -> Self {
654 Self { store }
655 }
656}
657
658#[cfg(any(feature = "postgres", feature = "turso", feature = "s3"))]
661enum LogKey {
662 Record(u64, u64),
664 Legacy(u64, u64),
666}
667
668#[cfg(any(feature = "postgres", feature = "turso", feature = "s3"))]
669impl NativeRootLogStore {
670 fn record_key(name: &str, version: u64, t: u64) -> String {
673 format!("log:{name}:v{version:020}:r{t:020}")
674 }
675
676 fn legacy_key(name: &str, version: u64, chunk: u64) -> String {
680 if chunk == 0 {
681 format!("log:{name}:v{version:020}")
682 } else {
683 format!("log:{name}:v{version:020}:c{chunk:020}")
684 }
685 }
686
687 fn prefix(name: &str) -> String {
688 format!("log:{name}:v")
689 }
690
691 fn parse_key(prefix: &str, key: &str) -> Option<LogKey> {
695 let rest = key.strip_prefix(prefix)?;
696 if let Some((version, t)) = rest.split_once(":r") {
697 Some(LogKey::Record(version.parse().ok()?, t.parse().ok()?))
698 } else if let Some((version, chunk)) = rest.split_once(":c") {
699 Some(LogKey::Legacy(version.parse().ok()?, chunk.parse().ok()?))
700 } else {
701 Some(LogKey::Legacy(rest.parse().ok()?, 0))
702 }
703 }
704}
705
706#[cfg(any(feature = "postgres", feature = "turso", feature = "s3"))]
707#[async_trait]
708impl NativeLogStorage for NativeRootLogStore {
709 async fn put_batch(
710 &self,
711 name: &str,
712 version: u64,
713 records: &[(u64, Vec<u8>)],
714 ) -> Result<bool, LogError> {
715 let Some((last_t, _)) = records.last() else {
716 return Ok(true);
717 };
718 let mut bytes = Vec::new();
726 for (_, framed) in records {
727 bytes.extend_from_slice(framed);
728 }
729 match self
730 .store
731 .cas_root(&Self::record_key(name, version, *last_t), None, &bytes)
732 .await
733 {
734 Ok(()) => Ok(true),
735 Err(StoreError::CasFailed { .. }) => Ok(false),
736 Err(error) => Err(LogError::Native(error.to_string())),
737 }
738 }
739
740 async fn read_record(
741 &self,
742 name: &str,
743 version: u64,
744 t: u64,
745 ) -> Result<Option<Vec<u8>>, LogError> {
746 self.store
747 .get_root(&Self::record_key(name, version, t))
748 .await
749 .map_err(|error| LogError::Native(error.to_string()))
750 }
751
752 async fn list_records(&self, name: &str) -> Result<Vec<(u64, u64)>, LogError> {
753 let prefix = Self::prefix(name);
754 let names = self
755 .store
756 .list_roots(&prefix)
757 .await
758 .map_err(|error| LogError::Native(error.to_string()))?;
759 names
760 .into_iter()
761 .filter_map(|key| match Self::parse_key(&prefix, &key) {
762 Some(LogKey::Record(version, t)) => Some(Ok((version, t))),
763 Some(LogKey::Legacy(..)) => None,
764 None => Some(Err(LogError::Corrupt)),
765 })
766 .collect()
767 }
768
769 async fn read_legacy_chunk(
770 &self,
771 name: &str,
772 version: u64,
773 chunk: u64,
774 ) -> Result<Option<Vec<u8>>, LogError> {
775 self.store
776 .get_root(&Self::legacy_key(name, version, chunk))
777 .await
778 .map_err(|error| LogError::Native(error.to_string()))
779 }
780
781 async fn list_legacy_chunks(&self, name: &str) -> Result<Vec<(u64, u64)>, LogError> {
782 let prefix = Self::prefix(name);
783 let names = self
784 .store
785 .list_roots(&prefix)
786 .await
787 .map_err(|error| LogError::Native(error.to_string()))?;
788 names
789 .into_iter()
790 .filter_map(|key| match Self::parse_key(&prefix, &key) {
791 Some(LogKey::Legacy(version, chunk)) => Some(Ok((version, chunk))),
792 Some(LogKey::Record(..)) => None,
793 None => Some(Err(LogError::Corrupt)),
794 })
795 .collect()
796 }
797
798 async fn delete_all(&self, name: &str) -> Result<(), LogError> {
799 let prefix = Self::prefix(name);
800 let names = self
801 .store
802 .list_roots(&prefix)
803 .await
804 .map_err(|error| LogError::Native(error.to_string()))?;
805 for key in names {
806 self.store
807 .delete_root(&key)
808 .await
809 .map_err(|error| LogError::Native(error.to_string()))?;
810 }
811 Ok(())
812 }
813}
814
815#[cfg(all(test, any(feature = "postgres", feature = "turso", feature = "s3")))]
816mod tests {
817 use super::*;
818 use corium_core::{Datom, EntityId, Value};
819
820 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
821 async fn native_root_log_writes_one_object_per_record_on_the_runtime() {
822 let run = async {
823 let dir = tempfile::tempdir().expect("tempdir");
824 let store = Arc::new(
825 NodeStore::open(&StoreSpec::Memory, dir.path())
826 .await
827 .expect("memory store"),
828 );
829 let storage = Arc::new(NativeRootLogStore::new(store));
830 let log = NativeVersionedLog::open(Arc::clone(&storage), "db", 1)
831 .await
832 .expect("open native log");
833 for t in 1..=3 {
836 log.append_async(&TxRecord {
837 t,
838 tx_instant: i64::try_from(t).expect("small t"),
839 datoms: vec![Datom {
840 e: EntityId::from_raw(t),
841 a: EntityId::from_raw(1),
842 v: Value::Bytes(vec![0; 300 * 1024].into()),
843 tx: EntityId::from_raw(t),
844 added: true,
845 }],
846 })
847 .await
848 .expect("append");
849 }
850 let records = storage.list_records("db").await.expect("list records");
851 assert_eq!(records.len(), 3);
852 assert!(
853 storage
854 .list_legacy_chunks("db")
855 .await
856 .expect("list legacy")
857 .is_empty()
858 );
859 assert_eq!(log.replay_async().await.expect("replay").len(), 3);
860 };
861 tokio::time::timeout(std::time::Duration::from_secs(5), run)
862 .await
863 .expect("native log operation stalled on its runtime");
864 }
865}