Skip to main content

veilid_core/table_store/
mod.rs

1use super::*;
2
3mod table_db;
4mod tasks;
5
6pub use table_db::*;
7
8#[cfg(any(test, feature = "test-util"))]
9#[doc(hidden)]
10pub mod tests_table_store;
11
12#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
13mod wasm;
14#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
15use wasm::*;
16#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
17mod native;
18#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
19use native::*;
20
21use keyvaluedb::*;
22use weak_table::WeakValueHashMap;
23
24impl_veilid_log_facility!("tstore");
25
26const ALL_TABLE_NAMES: &[u8] = b"all_table_names";
27const FLUSH_TABLES_INTERVAL_SECS: u32 = 60;
28const CLEANUP_TABLES_INTERVAL_SECS: u32 = 600;
29
30/// Description of column
31#[apply(api_data_struct!)]
32#[api(eq, default, ts)]
33pub struct ColumnInfo {
34    pub key_count: AlignedU64,
35}
36
37/// IO Stats for table
38#[apply(api_data_struct!)]
39#[api(eq, default, ts)]
40pub struct IOStatsInfo {
41    /// Number of transaction.
42    pub transactions: AlignedU64,
43    /// Number of read operations.
44    pub reads: AlignedU64,
45    /// Number of reads resulted in a read from cache.
46    pub cache_reads: AlignedU64,
47    /// Number of write operations.
48    pub writes: AlignedU64,
49    /// Number of bytes read
50    pub bytes_read: ByteCount,
51    /// Number of bytes read from cache
52    pub cache_read_bytes: ByteCount,
53    /// Number of bytes write
54    pub bytes_written: ByteCount,
55    /// Number of delete operations.
56    pub deletes: AlignedU64,
57    /// Number of prefix (batch) delete operations.
58    pub prefix_deletes: AlignedU64,
59    /// Write size buckets. Keys are write sizes, slightly rounded upwards.
60    /// Values are the number of times a value of a particular size was written.
61    pub write_size_buckets: BTreeMap<usize, u64>,
62    /// Similar to `write_size_buckets` but tracks total sizes of transactions
63    /// and the average duration for a transaction of that size. The duration is
64    /// in **microseconds**.
65    pub tx_write_size_buckets: BTreeMap<usize, (u64, TimestampDuration)>,
66    /// Start of the statistic period.
67    pub started: Timestamp,
68    /// Total duration of the statistic period.
69    pub span: TimestampDuration,
70}
71
72/// Description of table
73#[apply(api_data_struct!)]
74#[api(eq, default, ts)]
75pub struct TableInfo {
76    /// Internal table name
77    pub table_name: String,
78    /// IO statistics since previous query
79    pub io_stats_since_previous: IOStatsInfo,
80    /// IO statistics since database open
81    pub io_stats_overall: IOStatsInfo,
82    /// Total number of columns in the table
83    pub column_count: u32,
84    /// Column descriptions
85    pub columns: Vec<ColumnInfo>,
86}
87
88#[must_use]
89struct TableStoreInner {
90    opened: WeakValueHashMap<String, Weak<TableDBUnlockedInner>>,
91    encryption_key: Option<SharedSecret>,
92    all_table_names: HashMap<String, String>,
93    all_tables_db: Option<Database>,
94    /// Tick subscription
95    tick_subscription: Option<EventBusSubscription>,
96}
97
98impl fmt::Debug for TableStoreInner {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        f.debug_struct("TableStoreInner")
101            .field("opened", &self.opened)
102            .field("encryption_key", &self.encryption_key)
103            .field("all_table_names", &self.all_table_names)
104            .finish()
105    }
106}
107
108/// Veilid Table Storage.
109/// Database for storing key value pairs persistently and securely across runs.
110#[must_use]
111pub struct TableStore {
112    registry: VeilidComponentRegistry,
113    startup_lock: StartupLock,
114    inner: Mutex<TableStoreInner>, // Sync mutex here because TableDB drops can happen at any time
115    table_store_driver: TableStoreDriver,
116    async_lock: Arc<AsyncMutex<()>>,
117    flush_tables_task: TickTask<EyreReport>,
118    cleanup_tables_task: TickTask<EyreReport>,
119}
120
121impl fmt::Debug for TableStore {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        f.debug_struct("TableStore")
124            .field("registry", &self.registry)
125            .field("startup_lock", &self.startup_lock)
126            .field("inner", &self.inner)
127            .field("async_lock", &self.async_lock)
128            .finish()
129    }
130}
131
132impl_veilid_component!(TableStore);
133
134impl TableStore {
135    fn new_inner() -> TableStoreInner {
136        TableStoreInner {
137            opened: WeakValueHashMap::new(),
138            encryption_key: None,
139            all_table_names: HashMap::new(),
140            all_tables_db: None,
141            tick_subscription: None,
142        }
143    }
144    pub(crate) fn new(registry: VeilidComponentRegistry) -> Self {
145        let inner = Self::new_inner();
146        let table_store_driver = TableStoreDriver::new(registry.clone());
147
148        let this = Self {
149            registry,
150            startup_lock: StartupLock::new(),
151            inner: Mutex::new(inner),
152            table_store_driver,
153            async_lock: Arc::new(AsyncMutex::new(())),
154            flush_tables_task: TickTask::new("flush_tables_task", FLUSH_TABLES_INTERVAL_SECS),
155            cleanup_tables_task: TickTask::new("cleanup_tables_task", CLEANUP_TABLES_INTERVAL_SECS),
156        };
157
158        this.setup_tasks();
159
160        this
161    }
162
163    // Flush internal control state
164    async fn flush(&self) {
165        let (all_table_names_value, all_tables_db) = {
166            let inner = self.inner.lock();
167            let all_table_names_value = serialize_json_bytes(&inner.all_table_names);
168            (
169                all_table_names_value,
170                inner.all_tables_db.clone().unwrap_or_log(),
171            )
172        };
173        let mut dbt = DBTransaction::new();
174        dbt.put(0, ALL_TABLE_NAMES, &all_table_names_value);
175        if let Err(e) = all_tables_db.write(dbt).await {
176            veilid_log!(self error "failed to write all tables db: {}", e);
177        }
178    }
179
180    // Cleanup/vacuum database
181    async fn cleanup(&self) {
182        // Vacuum the open databases while we're at it
183        let all_open_db: Vec<_> = {
184            let inner = self.inner.lock();
185            inner.opened.values().collect()
186        };
187        for db in all_open_db {
188            let tdb = TableDB::new_from_unlocked_inner(db, 0);
189            if let Err(e) = tdb.cleanup().await {
190                veilid_log!(self error "Error cleaning up database '{}': {}", tdb.table_name(), e);
191            }
192        }
193    }
194
195    // Internal naming support
196    // Adds rename capability and ensures names of tables are totally unique and valid
197
198    fn namespaced_name(&self, table: &str) -> VeilidAPIResult<String> {
199        if !table
200            .chars()
201            .all(|c| char::is_alphanumeric(c) || c == '_' || c == '-')
202        {
203            apibail_invalid_argument!("table name is invalid", "table", table);
204        }
205        let namespace = self.config().namespace.clone();
206        Ok(if namespace.is_empty() {
207            table.to_string()
208        } else {
209            format!("_ns_{}_{}", namespace, table)
210        })
211    }
212
213    fn name_get_or_create(&self, table: &str) -> VeilidAPIResult<String> {
214        let name = self.namespaced_name(table)?;
215
216        let mut inner = self.inner.lock();
217        // Do we have this name yet?
218        if let Some(real_name) = inner.all_table_names.get(&name) {
219            return Ok(real_name.clone());
220        }
221
222        // If not, make a new low level name mapping
223        let mut real_name_bytes = [0u8; 32];
224        random_bytes(&mut real_name_bytes);
225        let real_name = data_encoding::BASE64URL_NOPAD.encode(&real_name_bytes);
226
227        if inner
228            .all_table_names
229            .insert(name.to_owned(), real_name.clone())
230            .is_some()
231        {
232            veilid_log!(self error "should not have already had this table name: {}", name);
233        };
234
235        Ok(real_name)
236    }
237
238    #[cfg_attr(
239        feature = "instrument",
240        instrument(level = "trace", target = "tstore", skip_all)
241    )]
242    fn name_delete(&self, table: &str) -> VeilidAPIResult<Option<String>> {
243        let name = self.namespaced_name(table)?;
244        let mut inner = self.inner.lock();
245        let real_name = inner.all_table_names.remove(&name);
246        Ok(real_name)
247    }
248
249    #[cfg_attr(
250        feature = "instrument",
251        instrument(level = "trace", target = "tstore", skip_all)
252    )]
253    fn name_get(&self, table: &str) -> VeilidAPIResult<Option<String>> {
254        let name = self.namespaced_name(table)?;
255        let inner = self.inner.lock();
256        let real_name = inner.all_table_names.get(&name).cloned();
257        Ok(real_name)
258    }
259
260    #[cfg_attr(
261        feature = "instrument",
262        instrument(level = "trace", target = "tstore", skip_all)
263    )]
264    fn name_rename(&self, old_table: &str, new_table: &str) -> VeilidAPIResult<()> {
265        let old_name = self.namespaced_name(old_table)?;
266        let new_name = self.namespaced_name(new_table)?;
267
268        let mut inner = self.inner.lock();
269        // Ensure new name doesn't exist
270        if inner.all_table_names.contains_key(&new_name) {
271            return Err(VeilidAPIError::generic("new table already exists"));
272        }
273        // Do we have this name yet?
274        let Some(real_name) = inner.all_table_names.remove(&old_name) else {
275            return Err(VeilidAPIError::generic("table does not exist"));
276        };
277        // Insert with new name
278        inner.all_table_names.insert(new_name.to_owned(), real_name);
279
280        Ok(())
281    }
282
283    /// List all known tables
284    ///
285    /// Reads cached names locally without blocking on disk; returns empty before init or after shutdown.
286    #[cfg_attr(
287        feature = "instrument",
288        instrument(level = "trace", target = "tstore", skip_all)
289    )]
290    pub fn list_all(&self) -> Vec<(String, String)> {
291        let Ok(_startup_guard) = self.startup_lock.enter() else {
292            return vec![];
293        };
294
295        let inner = self.inner.lock();
296        inner
297            .all_table_names
298            .iter()
299            .map(|(k, v)| (k.clone(), v.clone()))
300            .collect::<Vec<(String, String)>>()
301    }
302
303    /// Delete all known tables
304    ///
305    /// No-op before init or after shutdown. Blocks on deleting each table on disk and flushing.
306    #[cfg_attr(
307        feature = "instrument",
308        instrument(level = "trace", target = "tstore", skip_all)
309    )]
310    pub async fn delete_all(&self) {
311        let Ok(_startup_guard) = self.startup_lock.enter() else {
312            return;
313        };
314
315        // Get all tables
316        let table_names = {
317            let mut inner = self.inner.lock();
318            let real_names = inner
319                .all_table_names
320                .iter()
321                .map(|(k, v)| (k.clone(), v.clone()))
322                .collect::<Vec<(String, String)>>();
323            inner.all_table_names.clear();
324            real_names
325        };
326
327        // Delete all tables
328        for (table_name, table_real_name) in table_names {
329            veilid_log!(self debug "deleting table: {} ({})", table_real_name, table_name);
330            if let Err(e) = self.table_store_driver.delete(&table_real_name).await {
331                veilid_log!(self error "error deleting table: {}", e);
332            }
333        }
334        self.flush().await;
335    }
336
337    #[cfg_attr(
338        feature = "instrument",
339        instrument(level = "trace", target = "tstore", skip_all)
340    )]
341    pub(crate) async fn maybe_unprotect_device_encryption_key(
342        &self,
343        dek_bytes: &[u8],
344        device_encryption_key_password: &str,
345    ) -> EyreResult<SharedSecret> {
346        // Ensure the key is at least as long as necessary
347        // to check for crypto kind
348        if dek_bytes.len() < 4 {
349            bail!("device encryption key is not valid");
350        }
351
352        // Get cryptosystem
353        let kind = CryptoKind::try_from(&dek_bytes[0..4]).unwrap_or_log();
354        let crypto = self.crypto();
355        let Some(vcrypto) = crypto.get_async(kind) else {
356            bail!("unsupported cryptosystem '{kind}'");
357        };
358
359        if !device_encryption_key_password.is_empty() {
360            if dek_bytes.len()
361                != (4
362                    + vcrypto.shared_secret_length()
363                    + vcrypto.aead_overhead()
364                    + vcrypto.nonce_length())
365            {
366                bail!("password protected device encryption key is not valid");
367            }
368            let protected_key =
369                &dek_bytes[4..(4 + vcrypto.shared_secret_length() + vcrypto.aead_overhead())];
370            let nonce = Nonce::new(
371                &dek_bytes[(4 + vcrypto.shared_secret_length() + vcrypto.aead_overhead())..],
372            );
373            let shared_secret = vcrypto
374                .derive_shared_secret(
375                    Bytes::copy_from_slice(device_encryption_key_password.as_bytes()),
376                    nonce.bytes(),
377                )
378                .await
379                .wrap_err("failed to derive shared secret")?;
380
381            let unprotected_key = vcrypto
382                .decrypt_aead(
383                    Bytes::copy_from_slice(protected_key),
384                    &nonce,
385                    &shared_secret,
386                    None,
387                )
388                .await
389                .wrap_err("failed to decrypt device encryption key")?;
390
391            return Ok(SharedSecret::new(
392                kind,
393                BareSharedSecret::new(unprotected_key.as_ref()),
394            ));
395        }
396
397        if dek_bytes.len() != (4 + vcrypto.shared_secret_length()) {
398            bail!("unprotected device encryption key is not valid");
399        }
400
401        Ok(SharedSecret::new(
402            kind,
403            BareSharedSecret::new(&dek_bytes[4..]),
404        ))
405    }
406
407    #[cfg_attr(
408        feature = "instrument",
409        instrument(level = "trace", target = "tstore", skip_all)
410    )]
411    pub(crate) async fn maybe_protect_device_encryption_key(
412        &self,
413        dek: SharedSecret,
414        device_encryption_key_password: &str,
415    ) -> EyreResult<Vec<u8>> {
416        // Check if we are to protect the key
417        if device_encryption_key_password.is_empty() {
418            veilid_log!(self debug "no dek password");
419            // Return the unprotected key bytes
420            return Ok(Vec::from(dek));
421        }
422
423        // Get cryptosystem
424        let crypto = self.crypto();
425        let Some(vcrypto) = crypto.get_async(dek.kind()) else {
426            bail!("unsupported cryptosystem '{}'", dek.kind());
427        };
428
429        let nonce = vcrypto.random_nonce().await;
430        let shared_secret = vcrypto
431            .derive_shared_secret(
432                Bytes::copy_from_slice(device_encryption_key_password.as_bytes()),
433                Bytes::copy_from_slice(&nonce),
434            )
435            .await
436            .wrap_err("failed to derive shared secret")?;
437        let protected_key = vcrypto
438            .encrypt_aead(
439                Bytes::copy_from_slice(dek.ref_value()),
440                &nonce,
441                &shared_secret,
442                None,
443            )
444            .await
445            .wrap_err("failed to decrypt device encryption key")?;
446        let mut out = Vec::with_capacity(
447            4 + vcrypto.shared_secret_length() + vcrypto.aead_overhead() + vcrypto.nonce_length(),
448        );
449        out.extend_from_slice(dek.kind().bytes());
450        out.extend_from_slice(&protected_key);
451        out.extend_from_slice(&nonce);
452        debug_assert_eq!(
453            out.len(),
454            4 + vcrypto.shared_secret_length() + vcrypto.aead_overhead() + vcrypto.nonce_length()
455        );
456        Ok(out)
457    }
458
459    #[cfg_attr(
460        feature = "instrument",
461        instrument(level = "trace", target = "tstore", skip_all)
462    )]
463    async fn load_device_encryption_key(&self) -> EyreResult<Option<SharedSecret>> {
464        let dek_bytes: Option<Vec<u8>> = self
465            .protected_store()
466            .load_user_secret("device_encryption_key")?;
467        let Some(dek_bytes) = dek_bytes else {
468            veilid_log!(self debug "no device encryption key");
469            return Ok(None);
470        };
471
472        // Get device encryption key protection password if we have it
473        let device_encryption_key_password = self
474            .config()
475            .protected_store
476            .device_encryption_key_password
477            .clone();
478
479        Ok(Some(
480            self.maybe_unprotect_device_encryption_key(&dek_bytes, &device_encryption_key_password)
481                .await?,
482        ))
483    }
484
485    #[cfg_attr(
486        feature = "instrument",
487        instrument(level = "trace", target = "tstore", skip_all)
488    )]
489    async fn save_device_encryption_key(
490        &self,
491        device_encryption_key: Option<SharedSecret>,
492    ) -> EyreResult<()> {
493        let Some(device_encryption_key) = device_encryption_key else {
494            // Remove the device encryption key
495            let existed = self
496                .protected_store()
497                .remove_user_secret("device_encryption_key")?;
498            veilid_log!(self debug "removed device encryption key. existed: {}", existed);
499            return Ok(());
500        };
501
502        // Get new device encryption key protection password if we are changing it
503        let new_device_encryption_key_password = self
504            .config()
505            .protected_store
506            .new_device_encryption_key_password
507            .clone();
508        let device_encryption_key_password =
509            if let Some(new_device_encryption_key_password) = new_device_encryption_key_password {
510                // Change password
511                veilid_log!(self debug "changing dek password");
512                new_device_encryption_key_password
513            } else {
514                // Get device encryption key protection password if we have it
515                veilid_log!(self debug "saving with existing dek password");
516                self.config()
517                    .protected_store
518                    .device_encryption_key_password
519                    .clone()
520            };
521
522        let dek_bytes = self
523            .maybe_protect_device_encryption_key(
524                device_encryption_key,
525                &device_encryption_key_password,
526            )
527            .await?;
528
529        // Save the new device encryption key
530        let existed = self
531            .protected_store()
532            .save_user_secret("device_encryption_key", &dek_bytes)?;
533        veilid_log!(self debug "saving device encryption key. existed: {}", existed);
534        Ok(())
535    }
536
537    fn log_facilities_impl(&self) -> VeilidComponentLogFacilities {
538        VeilidComponentLogFacilities::new().with_facility(
539            VeilidComponentLogFacility::try_new_with_tags("tstore", ["#common"]).unwrap(),
540        )
541    }
542
543    #[cfg_attr(
544        feature = "instrument",
545        instrument(level = "trace", target = "tstore", skip_all)
546    )]
547    async fn init_async(&self) -> EyreResult<()> {
548        let startup_guard = self.startup_lock.startup()?;
549        {
550            let _async_guard = self.async_lock.lock().await;
551
552            // Get device encryption key from protected store
553            let mut device_encryption_key = self.load_device_encryption_key().await?;
554            let mut device_encryption_key_changed = false;
555            if let Some(device_encryption_key) = &device_encryption_key {
556                // If encryption in current use is not the best encryption, then run table migration
557                let best_kind = best_crypto_kind();
558                if device_encryption_key.kind() != best_kind {
559                    // XXX: Run migration. See issue #209
560                    veilid_log!(self error "Need to write migration support");
561                }
562            } else {
563                // If we don't have an encryption key yet, then make one with the best cryptography and save it
564                let crypto = self.crypto();
565                let vcrypto = crypto.best_async();
566                let shared_secret = vcrypto.random_shared_secret().await;
567
568                device_encryption_key = Some(shared_secret);
569                device_encryption_key_changed = true;
570            }
571
572            // Check for password change
573            let changing_password = self
574                .config()
575                .protected_store
576                .new_device_encryption_key_password
577                .is_some();
578
579            // Save encryption key if it has changed or if the protecting password wants to change
580            if device_encryption_key_changed || changing_password {
581                self.save_device_encryption_key(device_encryption_key.clone())
582                    .await?;
583            }
584
585            // Deserialize all table names
586            let all_tables_db = match self
587                .table_store_driver
588                .open("__veilid_all_tables", 1, 1)
589                .await
590            {
591                Ok(db) => db,
592                Err(e) => {
593                    veilid_log!(self error "failed to create all tables table: {}", e);
594                    return Err(e.into());
595                }
596            };
597            match all_tables_db.get(0, ALL_TABLE_NAMES).await {
598                Ok(Some(v)) => match deserialize_json_bytes::<HashMap<String, String>>(&v) {
599                    Ok(all_table_names) => {
600                        let mut inner = self.inner.lock();
601                        inner.all_table_names = all_table_names;
602                    }
603                    Err(e) => {
604                        veilid_log!(self error "could not deserialize __veilid_all_tables: {}", e);
605                    }
606                },
607                Ok(None) => {
608                    // No table names yet, that's okay
609                    veilid_log!(self trace "__veilid_all_tables is empty");
610                }
611                Err(e) => {
612                    veilid_log!(self error "could not get __veilid_all_tables: {}", e);
613                }
614            };
615
616            {
617                let mut inner = self.inner.lock();
618                inner.encryption_key = device_encryption_key;
619                inner.all_tables_db = Some(all_tables_db);
620            }
621        }
622        startup_guard.success();
623
624        // Delete all tables if config has 'delete' enabled
625        // Must happen after startup guard is marked as successful
626        let do_delete = self.config().table_store.delete;
627        if do_delete {
628            veilid_log!(self debug "TableStore config 'delete' enabled: deleting all tables");
629            self.delete_all().await;
630        }
631
632        Ok(())
633    }
634
635    #[cfg_attr(
636        feature = "instrument",
637        instrument(level = "trace", target = "tstore", skip_all)
638    )]
639    #[allow(clippy::unused_async)]
640    async fn post_init_async(&self) -> EyreResult<()> {
641        // Register event handlers
642        let tick_subscription = impl_subscribe_event_bus_async!(self, Self, tick_event_handler);
643
644        let mut inner = self.inner.lock();
645
646        // Schedule tick
647        inner.tick_subscription = Some(tick_subscription);
648
649        Ok(())
650    }
651
652    #[cfg_attr(
653        feature = "instrument",
654        instrument(level = "trace", target = "tstore", skip_all)
655    )]
656    #[allow(clippy::unused_async)]
657    async fn pre_terminate_async(&self) {
658        // Unsubscribe from ticker
659        let mut inner = self.inner.lock();
660        if let Some(sub) = inner.tick_subscription.take() {
661            self.event_bus().unsubscribe(sub);
662        }
663    }
664
665    #[cfg_attr(
666        feature = "instrument",
667        instrument(level = "trace", target = "tstore", skip_all)
668    )]
669    async fn terminate_async(&self) {
670        let Ok(_startup_guard) = self.startup_lock.shutdown().await else {
671            veilid_log!(self error "table store is already shut down");
672            return;
673        };
674
675        // Cancel tasks
676        self.cancel_tasks().await;
677
678        self.flush().await;
679
680        let mut inner = self.inner.lock();
681        inner.opened.shrink_to_fit();
682        if !inner.opened.is_empty() {
683            veilid_log!(self warn
684                "all open databases should have been closed: {:?}",
685                inner.opened
686            );
687            inner.opened.clear();
688        }
689        inner.all_tables_db = None;
690        inner.all_table_names.clear();
691        inner.encryption_key = None;
692    }
693
694    /// Get or create a TableDB database table. If the column count is greater than an
695    /// existing TableDB's column count, the database will be upgraded to add the missing columns.
696    ///
697    /// Returns a `TableDB` handle the caller must drop before the table can be deleted; while any clone is held the table counts as opened. Opening the same name again returns a handle to the same shared database. Blocks on opening the on-disk database.
698    ///
699    /// Same errors as `open_pooled`.
700    #[cfg_attr(
701        feature = "instrument",
702        instrument(level = "trace", target = "tstore", skip_all)
703    )]
704    pub async fn open(&self, name: &str, column_count: u32) -> VeilidAPIResult<TableDB> {
705        self.open_pooled(name, column_count, 1).await
706    }
707
708    #[cfg_attr(
709        feature = "instrument",
710        instrument(level = "trace", target = "tstore", skip_all)
711    )]
712    /// Get or create a TableDB table, opening a pool of `concurrency` database connections for it.
713    /// Like `open`, upgrades the table if `column_count` exceeds the existing column count.
714    ///
715    /// Returns a `TableDB` handle the caller must drop before the table can be deleted; while any clone is held the table counts as opened. Opening the same name again returns a handle to the same shared database. Blocks on opening the on-disk database.
716    ///
717    /// Errors with:
718    /// - `VeilidAPIError::InvalidArgument` if `column_count` is zero or `name` contains characters other than alphanumeric, `_`, or `-`.
719    /// - `VeilidAPIError::NotInitialized` before init or after shutdown, or when the existing data fails to decrypt (wrong device encryption key) and `wipe_on_invalid_device_encryption_key` is disabled.
720    /// - `VeilidAPIError::Generic` if the table is already open with a smaller column count than requested (close it first), or if the backing-store open fails (`::Internal` for filesystem-permission failures).
721    pub async fn open_pooled(
722        &self,
723        name: &str,
724        column_count: u32,
725        concurrency: usize,
726    ) -> VeilidAPIResult<TableDB> {
727        if column_count == 0 {
728            apibail_invalid_argument!(
729                "column count must be greater than zero",
730                "column_count",
731                column_count
732            );
733        }
734
735        let Ok(_startup_guard) = self.startup_lock.enter() else {
736            apibail_not_initialized!();
737        };
738
739        let _async_guard = self.async_lock.lock().await;
740
741        // If we aren't initialized yet, bail
742        {
743            let inner = self.inner.lock();
744            if inner.all_tables_db.is_none() {
745                apibail_not_initialized!();
746            }
747        }
748
749        let mut table_name = self.name_get_or_create(name)?;
750
751        // See if this table is already opened, if so the column count must be the same
752        {
753            let inner = self.inner.lock();
754            if let Some(table_db_unlocked_inner) = inner.opened.get(&table_name) {
755                let tdb = TableDB::new_from_unlocked_inner(table_db_unlocked_inner, column_count);
756
757                // Ensure column count isnt bigger
758                let existing_col_count = tdb.get_column_count()?;
759                if column_count > existing_col_count {
760                    return Err(VeilidAPIError::generic(format!(
761                        "database must be closed before increasing column count {} -> {}",
762                        existing_col_count, column_count,
763                    )));
764                }
765
766                return Ok(tdb);
767            }
768        }
769
770        // Open table db using platform-specific driver.
771        let mut db = match self
772            .table_store_driver
773            .open(&table_name, column_count, concurrency)
774            .await
775        {
776            Ok(db) => db,
777            Err(e) => {
778                self.name_delete(name).expect_or_log("removing name failed");
779                self.flush().await;
780                return Err(e);
781            }
782        };
783
784        // Flush table names to disk
785        self.flush().await;
786
787        // If more columns are available, open the low level db with the max column count but restrict the tabledb object to the number requested
788        let existing_col_count = db.num_columns().map_err(VeilidAPIError::from)?;
789        if existing_col_count > column_count {
790            drop(db);
791            db = match self
792                .table_store_driver
793                .open(&table_name, existing_col_count, concurrency)
794                .await
795            {
796                Ok(db) => db,
797                Err(e) => {
798                    self.name_delete(name).expect_or_log("removing name failed");
799                    self.flush().await;
800                    return Err(e);
801                }
802            };
803        }
804
805        // Wrap low-level Database in TableDB object
806        let encryption_key = self.inner.lock().encryption_key.clone();
807        let table_db = TableDB::new(
808            table_name.clone(),
809            self.registry(),
810            db,
811            encryption_key.clone(),
812            encryption_key.clone(),
813            column_count,
814        );
815
816        // Validate keys are readable if they exist, otherwise wipe this database
817        // because there is an invalid encryption key and no way to read the data
818        let mut first_failure: Option<(u32, String)> = None;
819        for col in 0..existing_col_count {
820            match table_db.get_keys(col).await {
821                Ok(_keys) => {
822                    #[cfg(feature = "verbose-tracing")]
823                    veilid_log!(self debug "table {}({}) col {}: {} keys read ok", name, table_name, col, _keys.len());
824                }
825                Err(e) => {
826                    veilid_log!(self warn "table {}({}) col {}: get_keys failed: {}", name, table_name, col, e);
827                    if first_failure.is_none() {
828                        first_failure = Some((col, e.to_string()));
829                    }
830                }
831            }
832        }
833
834        let table_db = if let Some((failed_col, failure_msg)) = first_failure {
835            let namespace = self.config().namespace.clone();
836
837            // If the wipe policy is disabled, return NotInitialized so the caller
838            // can decide what to do (e.g., prompt for the right key).
839            if !self
840                .config()
841                .table_store
842                .wipe_on_invalid_device_encryption_key
843            {
844                veilid_log!(self error
845                    "table {}({}) has invalid encryption key (col {} failed: {}); wipe_on_invalid_device_encryption_key=false, refusing to wipe namespace '{}'",
846                    name, table_name, failed_col, failure_msg, namespace
847                );
848                return Err(VeilidAPIError::not_initialized());
849            }
850
851            veilid_log!(self warn
852                "table {}({}) has invalid encryption key (col {} failed: {}); wiping all tables in namespace '{}' and starting fresh",
853                name, table_name, failed_col, failure_msg, namespace
854            );
855
856            // Drop our open handle to the bad table before deleting files.
857            drop(table_db);
858
859            // Drop the open all_tables_db handle and clear cached state so
860            // we can delete and re-create everything in the namespace.
861            {
862                let mut inner = self.inner.lock();
863                inner.all_tables_db = None;
864                inner.all_table_names.clear();
865                inner.opened.clear();
866            }
867
868            let deleted = self.table_store_driver.delete_all_in_namespace().await?;
869            veilid_log!(self warn "wiped {} table file(s) from namespace '{}'", deleted, namespace);
870
871            // Reopen __veilid_all_tables fresh.
872            let all_tables_db = self
873                .table_store_driver
874                .open("__veilid_all_tables", 1, 1)
875                .await?;
876            {
877                let mut inner = self.inner.lock();
878                inner.all_tables_db = Some(all_tables_db);
879            }
880
881            // Generate a new random real_name for the requested friendly name
882            // and open an empty database for it.
883            table_name = self.name_get_or_create(name)?;
884            self.flush().await;
885
886            let db = self
887                .table_store_driver
888                .open(&table_name, column_count, concurrency)
889                .await?;
890
891            TableDB::new(
892                table_name.clone(),
893                self.registry(),
894                db,
895                encryption_key.clone(),
896                encryption_key.clone(),
897                column_count,
898            )
899        } else {
900            table_db
901        };
902
903        // Keep track of opened DBs
904        let mut inner = self.inner.lock();
905        inner
906            .opened
907            .insert(table_name.clone(), table_db.unlocked_inner());
908
909        Ok(table_db)
910    }
911
912    /// Delete a TableDB table by name
913    ///
914    /// Errors if the table is still opened; the caller must drop all `TableDB` handles for it first. Blocks on the on-disk delete and flush.
915    ///
916    /// Returns `Ok(false)` if no table by that name exists. Errors with:
917    /// - `VeilidAPIError::NotInitialized` before init or after shutdown.
918    /// - `VeilidAPIError::InvalidArgument` if `name` contains characters other than alphanumeric, `_`, or `-`.
919    /// - `VeilidAPIError::Generic` if the table is still opened (drop all handles first) or the backing-store delete fails.
920    #[cfg_attr(
921        feature = "instrument",
922        instrument(level = "trace", target = "tstore", skip_all)
923    )]
924    pub async fn delete(&self, name: &str) -> VeilidAPIResult<bool> {
925        let Ok(_startup_guard) = self.startup_lock.enter() else {
926            apibail_not_initialized!();
927        };
928
929        let _async_guard = self.async_lock.lock().await;
930        // If we aren't initialized yet, bail
931        {
932            let inner = self.inner.lock();
933            if inner.all_tables_db.is_none() {
934                apibail_not_initialized!();
935            }
936        }
937
938        let Some(table_name) = self.name_get(name)? else {
939            // Did not exist in name table
940            return Ok(false);
941        };
942
943        // See if this table is opened
944        {
945            let inner = self.inner.lock();
946            if inner.opened.contains_key(&table_name) {
947                apibail_generic!("Not deleting table that is still opened");
948            }
949        }
950
951        // Delete table db using platform-specific driver
952        let deleted = self.table_store_driver.delete(&table_name).await?;
953        if !deleted {
954            // Table missing? Just remove name
955            veilid_log!(self warn
956                "table existed in name table but not in storage: {} : {}",
957                name, table_name
958            );
959        }
960        if let Err(e) = self.name_delete(name) {
961            veilid_log!(self error "failed to delete name: {}", e);
962            return Err(e);
963        }
964        self.flush().await;
965        Ok(true)
966    }
967    /// Get column and key-count information for a table, or `None` if it does not exist.
968    ///
969    /// Opens the table to read it; blocks on the on-disk open and key-count reads.
970    ///
971    /// Errors with `VeilidAPIError::NotInitialized` before init or after shutdown, the same errors as `open` (the table is opened to read it), or `VeilidAPIError::Generic` if a column-count or key-count read fails.
972    pub async fn info(&self, name: &str) -> VeilidAPIResult<Option<TableInfo>> {
973        let Ok(_startup_guard) = self.startup_lock.enter() else {
974            apibail_not_initialized!();
975        };
976
977        // Open with at least one column, then reopen to match all available columns.
978        let mut tdb = self.open(name, 1).await?;
979        let column_count = tdb.get_column_count()?;
980        if column_count > 1 {
981            tdb = self.open(name, column_count).await?;
982        }
983
984        let internal_name = tdb.table_name();
985        let io_stats_since_previous = tdb.io_stats(IoStatsKind::SincePrevious);
986        let io_stats_overall = tdb.io_stats(IoStatsKind::Overall);
987        let mut columns = Vec::<ColumnInfo>::with_capacity(column_count as usize);
988        for col in 0..column_count {
989            let key_count = tdb.get_key_count(col).await?;
990            columns.push(ColumnInfo {
991                key_count: AlignedU64::new(key_count),
992            })
993        }
994        Ok(Some(TableInfo {
995            table_name: internal_name,
996            io_stats_since_previous: IOStatsInfo {
997                transactions: AlignedU64::new(io_stats_since_previous.transactions),
998                reads: AlignedU64::new(io_stats_since_previous.reads),
999                cache_reads: AlignedU64::new(io_stats_since_previous.cache_reads),
1000                writes: AlignedU64::new(io_stats_since_previous.writes),
1001                bytes_read: ByteCount::new(io_stats_since_previous.bytes_read),
1002                cache_read_bytes: ByteCount::new(io_stats_since_previous.cache_read_bytes),
1003                bytes_written: ByteCount::new(io_stats_since_previous.bytes_written),
1004                deletes: AlignedU64::new(io_stats_since_previous.deletes),
1005                prefix_deletes: AlignedU64::new(io_stats_since_previous.prefix_deletes),
1006                write_size_buckets: io_stats_since_previous
1007                    .write_size_buckets
1008                    .into_iter()
1009                    .collect(),
1010                tx_write_size_buckets: io_stats_since_previous
1011                    .tx_write_size_buckets
1012                    .into_iter()
1013                    .map(|(k, (count, avg_duration))| {
1014                        (k, (count, TimestampDuration::new(avg_duration as u64)))
1015                    })
1016                    .collect(),
1017                started: Timestamp::new(io_stats_since_previous.started),
1018                span: TimestampDuration::new(io_stats_since_previous.span.as_micros() as u64),
1019            },
1020            io_stats_overall: IOStatsInfo {
1021                transactions: AlignedU64::new(io_stats_overall.transactions),
1022                reads: AlignedU64::new(io_stats_overall.reads),
1023                cache_reads: AlignedU64::new(io_stats_overall.cache_reads),
1024                writes: AlignedU64::new(io_stats_overall.writes),
1025                bytes_read: ByteCount::new(io_stats_overall.bytes_read),
1026                cache_read_bytes: ByteCount::new(io_stats_overall.cache_read_bytes),
1027                bytes_written: ByteCount::new(io_stats_overall.bytes_written),
1028                deletes: AlignedU64::new(io_stats_overall.deletes),
1029                prefix_deletes: AlignedU64::new(io_stats_overall.prefix_deletes),
1030                write_size_buckets: io_stats_overall.write_size_buckets.into_iter().collect(),
1031                tx_write_size_buckets: io_stats_overall
1032                    .tx_write_size_buckets
1033                    .into_iter()
1034                    .map(|(k, (count, avg_duration))| {
1035                        (k, (count, TimestampDuration::new(avg_duration as u64)))
1036                    })
1037                    .collect(),
1038                started: Timestamp::new(io_stats_overall.started),
1039                span: TimestampDuration::new(io_stats_overall.span.as_micros() as u64),
1040            },
1041            column_count,
1042            columns,
1043        }))
1044    }
1045
1046    /// Rename a TableDB table
1047    ///
1048    /// Blocks on flushing the renamed name table to disk.
1049    ///
1050    /// Errors with:
1051    /// - `VeilidAPIError::NotInitialized` before init or after shutdown.
1052    /// - `VeilidAPIError::InvalidArgument` if `old_name` or `new_name` contains characters other than alphanumeric, `_`, or `-`.
1053    /// - `VeilidAPIError::Generic` if `new_name` already exists or `old_name` does not exist.
1054    #[cfg_attr(
1055        feature = "instrument",
1056        instrument(level = "trace", target = "tstore", skip_all)
1057    )]
1058    pub async fn rename(&self, old_name: &str, new_name: &str) -> VeilidAPIResult<()> {
1059        let Ok(_startup_guard) = self.startup_lock.enter() else {
1060            apibail_not_initialized!();
1061        };
1062
1063        let _async_guard = self.async_lock.lock().await;
1064        // If we aren't initialized yet, bail
1065        {
1066            let inner = self.inner.lock();
1067            if inner.all_tables_db.is_none() {
1068                apibail_not_initialized!();
1069            }
1070        }
1071        veilid_log!(self debug "TableStore::rename {} -> {}", old_name, new_name);
1072        self.name_rename(old_name, new_name)?;
1073        self.flush().await;
1074        Ok(())
1075    }
1076
1077    async fn tick_event_handler(&self, evt: Arc<TickEvent>) {
1078        let lag = evt.last_tick_ts.map(|x| evt.cur_tick_ts.duration_since(x));
1079        if let Err(e) = self.tick(lag).await {
1080            error!("Error in table store tick: {}", e);
1081        }
1082    }
1083}