use std::path::Path;
use std::time::Duration;
use sqlx::sqlite::{SqliteAutoVacuum, SqliteConnectOptions, SqliteJournalMode};
use sqlx::{SqlitePool, query_scalar};
use crate::errors::MbtResult;
use crate::{CacheEntryMeta, CachedTile, MbtError, Mbtiles, Metadata, UnixSeconds};
#[derive(Clone, Debug)]
pub struct MbtilesCache {
mbtiles: Mbtiles,
pool: SqlitePool,
}
impl MbtilesCache {
#[hotpath::measure]
pub async fn open<P: AsRef<Path>>(filepath: P) -> MbtResult<Self> {
let mbtiles = Mbtiles::new(filepath)?;
let opt = SqliteConnectOptions::new()
.filename(mbtiles.filepath())
.create_if_missing(true)
.auto_vacuum(SqliteAutoVacuum::Incremental)
.journal_mode(SqliteJournalMode::Wal)
.busy_timeout(Duration::from_secs(5));
let pool = SqlitePool::connect_with(opt).await?;
let mut conn = pool.acquire().await?;
let objects: i64 = query_scalar("SELECT COUNT(*) FROM sqlite_master")
.fetch_one(&mut *conn)
.await?;
if objects != 0 && !mbtiles.is_cache(&mut *conn).await? {
return Err(MbtError::NotACacheFile(mbtiles.filepath().to_string()));
}
mbtiles.create_cache_schema(&mut *conn, false).await?;
drop(conn);
Ok(Self { mbtiles, pool })
}
#[hotpath::measure]
pub async fn get_cached(&self, z: u8, x: u32, y: u32) -> MbtResult<Option<CachedTile>> {
let mut conn = self.pool.acquire().await?;
self.mbtiles.get_cached(&mut *conn, z, x, y).await
}
#[hotpath::measure]
pub async fn set_cached(
&self,
z: u8,
x: u32,
y: u32,
data: &[u8],
meta: CacheEntryMeta<'_>,
) -> MbtResult<()> {
let mut conn = self.pool.acquire().await?;
self.mbtiles
.set_cached(&mut conn, z, x, y, data, meta)
.await
}
#[hotpath::measure]
pub async fn update_cached_meta(
&self,
z: u8,
x: u32,
y: u32,
meta: CacheEntryMeta<'_>,
) -> MbtResult<bool> {
let mut conn = self.pool.acquire().await?;
self.mbtiles
.update_cached_meta(&mut *conn, z, x, y, meta)
.await
}
#[hotpath::measure]
pub async fn purge_expired(&self, now: UnixSeconds) -> MbtResult<u64> {
let mut conn = self.pool.acquire().await?;
self.mbtiles.purge_expired(&mut conn, now).await
}
#[hotpath::measure]
pub async fn purge_cache_to_size(&self, max_bytes: u64) -> MbtResult<u64> {
let mut conn = self.pool.acquire().await?;
self.mbtiles.purge_cache_to_size(&mut conn, max_bytes).await
}
#[hotpath::measure]
pub async fn get_tile(&self, z: u8, x: u32, y: u32) -> MbtResult<Option<Vec<u8>>> {
let mut conn = self.pool.acquire().await?;
self.mbtiles.get_tile(&mut *conn, z, x, y).await
}
#[hotpath::measure]
pub async fn get_metadata(&self) -> MbtResult<Metadata> {
let mut conn = self.pool.acquire().await?;
self.mbtiles.get_metadata(&mut *conn).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CacheEntryMeta;
#[tokio::test]
async fn cache_roundtrip_and_persist() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("cache.mbtiles");
{
let cache = MbtilesCache::open(&path).await.unwrap();
cache
.set_cached(
2,
1,
1,
b"tile-a",
CacheEntryMeta::new(UnixSeconds(40), UnixSeconds(50), "v1"),
)
.await
.unwrap();
cache
.set_cached(2, 1, 2, b"tile-b", CacheEntryMeta::default())
.await
.unwrap();
}
let cache = MbtilesCache::open(&path).await.unwrap();
let a = cache.get_cached(2, 1, 1).await.unwrap().unwrap();
assert_eq!(a.data, b"tile-a");
assert_eq!(a.fetched, Some(UnixSeconds(40)));
assert_eq!(a.expires, Some(UnixSeconds(50)));
assert_eq!(a.etag.as_deref(), Some("v1"));
assert_eq!(cache.get_tile(2, 1, 2).await.unwrap().unwrap(), b"tile-b");
assert!(
cache
.update_cached_meta(
2,
1,
1,
CacheEntryMeta::new(UnixSeconds(60), UnixSeconds(75), "v1")
)
.await
.unwrap()
);
let a = cache.get_cached(2, 1, 1).await.unwrap().unwrap();
assert_eq!(a.fetched, Some(UnixSeconds(60)));
assert_eq!(a.expires, Some(UnixSeconds(75)));
assert_eq!(cache.purge_expired(UnixSeconds(100)).await.unwrap(), 1);
assert!(cache.get_cached(2, 1, 1).await.unwrap().is_none());
assert!(cache.get_cached(2, 1, 2).await.unwrap().is_some());
}
#[tokio::test]
async fn new_file_gets_incremental_auto_vacuum() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("av.mbtiles");
let cache = MbtilesCache::open(&path).await.unwrap();
let mut conn = cache.pool.acquire().await.unwrap();
let mode: i64 = query_scalar("PRAGMA auto_vacuum")
.fetch_one(&mut *conn)
.await
.unwrap();
assert_eq!(mode, 2, "expected auto_vacuum = INCREMENTAL");
}
#[tokio::test]
async fn refuses_non_cache_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("flat.mbtiles");
let mbt = Mbtiles::new(&path).unwrap();
let mut conn = mbt.open_or_new().await.unwrap();
sqlx::query("CREATE TABLE metadata (name text NOT NULL PRIMARY KEY, value text)")
.execute(&mut conn)
.await
.unwrap();
sqlx::query(
"CREATE TABLE tiles (zoom_level integer NOT NULL, tile_column integer NOT NULL,
tile_row integer NOT NULL, tile_data blob,
PRIMARY KEY(zoom_level, tile_column, tile_row))",
)
.execute(&mut conn)
.await
.unwrap();
drop(conn);
let err = MbtilesCache::open(&path).await.unwrap_err();
assert!(matches!(err, MbtError::NotACacheFile(_)), "got {err:?}");
}
}