//! Storage accounting: the counts and byte totals the sync panel prices caps from. //! //! Extracted from the former `db.rs`; these are inherent methods on //! [`Database`], so the parent needs no re-export. use super::{Database, DbError}; impl Database { /// Aggregate storage stats: (sample_count, total_file_bytes). /// /// Excludes tombstoned rows (`deleted_at IS NOT NULL`) so the figure matches /// the library the user actually sees, the M019 read-path filter applies here /// like every other sample read site. pub fn storage_stats(&self) -> Result<(u64, u64), DbError> { let (count, total): (u64, u64) = self.conn.query_row( "SELECT COUNT(*), COALESCE(SUM(file_size), 0) FROM live_samples", [], // SQLite integers are i64. COUNT/SUM should be non-negative, but a // single corrupt negative file_size must surface as an error, not // wrap silently to ~1.8e19 (workspace denies unwrap for this class). |row| { let count = row.get::<_, i64>(0)?; let total = row.get::<_, i64>(1)?; Ok(( u64::try_from(count) .map_err(|_| rusqlite::Error::IntegralValueOutOfRange(0, count))?, u64::try_from(total) .map_err(|_| rusqlite::Error::IntegralValueOutOfRange(1, total))?, )) }, )?; Ok((count, total)) } /// Per-VFS storage stats: count and total bytes of *unique* samples /// referenced by `vfs_id`. A sample referenced from multiple nodes in the /// same VFS counts once. Used by the sync panel's per-VFS toggle rows so /// the user can see how much would upload before enabling blob sync. pub fn vfs_storage_stats(&self, vfs_id: i64) -> Result<(u64, u64), DbError> { // Soft-delete keeps vfs placements, so a tombstoned sample would still be // counted/summed here and inflate the "would upload" estimate; read through // live_samples to exclude it. let (count, total): (u64, u64) = self.conn.query_row( "SELECT COUNT(*), COALESCE(SUM(file_size), 0) FROM live_samples \ WHERE hash IN (\ SELECT DISTINCT sample_hash FROM vfs_nodes \ WHERE vfs_id = ? AND sample_hash IS NOT NULL\ )", [vfs_id], // Non-negative in practice; a corrupt negative surfaces as an error // rather than wrapping silently to a nonsense u64. |row| { let count = row.get::<_, i64>(0)?; let total = row.get::<_, i64>(1)?; Ok(( u64::try_from(count) .map_err(|_| rusqlite::Error::IntegralValueOutOfRange(0, count))?, u64::try_from(total) .map_err(|_| rusqlite::Error::IntegralValueOutOfRange(1, total))?, )) }, )?; Ok((count, total)) } /// Count and total bytes of the samples blob sync would actually upload: /// the *union* of every VFS with `sync_files` set. /// /// A union rather than a sum over [`vfs_storage_stats`](Self::vfs_storage_stats), /// because a sample placed in two synced VFSes uploads once. Blobs are /// content-addressed and the server dedups on `(app, user, hash)`, so /// adding the per-VFS figures would overstate the need and buy the user a /// cap they do not require. Reads through `live_samples` for the same /// reason the per-VFS query does: a tombstoned sample is not going to /// upload. /// /// Zero synced VFSes gives `(0, 0)`, which is the honest answer — nothing /// is set to sync, so nothing would upload. pub fn synced_storage_stats(&self) -> Result<(u64, u64), DbError> { let (count, total): (u64, u64) = self.conn.query_row( "SELECT COUNT(*), COALESCE(SUM(file_size), 0) FROM live_samples \ WHERE hash IN (\ SELECT DISTINCT sample_hash FROM vfs_nodes \ WHERE sample_hash IS NOT NULL \ AND vfs_id IN (SELECT id FROM vfs WHERE sync_files != 0)\ )", [], |row| { let count = row.get::<_, i64>(0)?; let total = row.get::<_, i64>(1)?; Ok(( u64::try_from(count) .map_err(|_| rusqlite::Error::IntegralValueOutOfRange(0, count))?, u64::try_from(total) .map_err(|_| rusqlite::Error::IntegralValueOutOfRange(1, total))?, )) }, )?; Ok((count, total)) } }