use std::path::Path;
use martin_tile_utils::TileInfo;
use sqlx::sqlite::SqliteConnectOptions;
use sqlx::{Pool, Sqlite, SqlitePool};
use tilejson::TileJSON;
#[cfg(test)]
use crate::NormalizedSchema;
use crate::errors::MbtResult;
use crate::{MbtType, Mbtiles, Metadata};
#[derive(Clone, Debug)]
pub struct MbtilesPool {
mbtiles: Mbtiles,
pool: Pool<Sqlite>,
}
impl MbtilesPool {
#[hotpath::measure]
pub async fn open_readonly<P: AsRef<Path>>(filepath: P) -> MbtResult<Self> {
let mbtiles = Mbtiles::new(filepath)?;
let opt = SqliteConnectOptions::new()
.filename(mbtiles.filepath())
.read_only(true);
let pool = SqlitePool::connect_with(opt).await?;
Ok(Self { mbtiles, pool })
}
#[hotpath::measure]
pub async fn get_metadata(&self) -> MbtResult<Metadata> {
let mut conn = self.pool.acquire().await?;
self.mbtiles.get_metadata(&mut *conn).await
}
#[hotpath::measure]
pub async fn detect_type(&self) -> MbtResult<MbtType> {
let mut conn = self.pool.acquire().await?;
self.mbtiles.detect_type(&mut *conn).await
}
#[hotpath::measure]
pub async fn detect_format(&self, tilejson: &TileJSON) -> MbtResult<Option<TileInfo>> {
let mut conn = self.pool.acquire().await?;
self.mbtiles.detect_format(tilejson, &mut *conn).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_tile_and_hash(
&self,
mbt_type: MbtType,
z: u8,
x: u32,
y: u32,
) -> MbtResult<Option<(Vec<u8>, Option<String>)>> {
let mut conn = self.pool.acquire().await?;
self.mbtiles
.get_tile_and_hash(&mut conn, mbt_type, z, x, y)
.await
}
#[hotpath::measure]
pub async fn contains(&self, mbt_type: MbtType, z: u8, x: u32, y: u32) -> MbtResult<bool> {
let mut conn = self.pool.acquire().await?;
self.mbtiles.contains(&mut conn, mbt_type, z, x, y).await
}
}
#[cfg(test)]
mod tests {
use martin_tile_utils::{Encoding, Format};
use super::*;
use crate::metadata::temp_named_mbtiles;
#[tokio::test]
async fn test_metadata_invalid() {
let script = include_str!("../../tests/fixtures/mbtiles/webp-no-primary.sql");
let (_mbt, _conn, file) = temp_named_mbtiles("test_metadata_invalid", script).await;
let pool = MbtilesPool::open_readonly(file).await.unwrap();
pool.detect_type().await.unwrap_err();
let metadata = pool.get_metadata().await.unwrap();
insta::assert_yaml_snapshot!(metadata, @r#"
id: "file:test_metadata_invalid?mode=memory&cache=shared"
layer_type: baselayer
tilejson:
tilejson: 3.0.0
tiles: []
bounds:
- -180
- -85.05113
- 180
- 85.05113
center:
- 0
- 0
- 0
maxzoom: 0
minzoom: 0
name: ne2sr
format: webp
"#);
assert_eq!(
pool.detect_format(&metadata.tilejson).await.unwrap(),
Some(TileInfo::new(Format::Webp, Encoding::Internal))
);
}
#[tokio::test]
async fn test_contains_invalid() {
let script = include_str!("../../tests/fixtures/mbtiles/webp-no-primary.sql");
let (_mbt, _conn, file) = temp_named_mbtiles("test_contains_invalid", script).await;
let pool = MbtilesPool::open_readonly(file).await.unwrap();
pool.detect_type().await.unwrap_err();
assert!(pool.contains(MbtType::Flat, 0, 0, 0).await.unwrap());
for error_mbt_type in [
MbtType::Normalized {
hash_view: false,
schema: NormalizedSchema::Hash,
},
MbtType::Normalized {
hash_view: true,
schema: NormalizedSchema::Hash,
},
MbtType::FlatWithHash,
] {
pool.contains(error_mbt_type, 0, 0, 0).await.unwrap_err();
}
}
#[tokio::test]
async fn test_invalid_type() {
let script = include_str!("../../tests/fixtures/mbtiles/webp-no-primary.sql");
let (_mbt, _conn, file) = temp_named_mbtiles("test_invalid_type", script).await;
let pool = MbtilesPool::open_readonly(file).await.unwrap();
pool.detect_type().await.unwrap_err();
let t1 = pool.get_tile(0, 0, 0).await.unwrap().unwrap();
assert!(!t1.is_empty());
let (t2, h2) = pool
.get_tile_and_hash(MbtType::Flat, 0, 0, 0)
.await
.unwrap()
.unwrap();
assert_eq!(t2, t1);
assert_eq!(h2, None);
for error_types in [
MbtType::FlatWithHash,
MbtType::Normalized {
hash_view: false,
schema: NormalizedSchema::Hash,
},
MbtType::Normalized {
hash_view: true,
schema: NormalizedSchema::Hash,
},
] {
pool.get_tile_and_hash(error_types, 0, 0, 0)
.await
.unwrap_err();
}
}
#[tokio::test]
async fn test_metadata_normalized() {
let script = include_str!("../../tests/fixtures/mbtiles/geography-class-png-no-bounds.sql");
let (_mbt, _conn, file) = temp_named_mbtiles("test_metadata_normalized", script).await;
let pool = MbtilesPool::open_readonly(file).await.unwrap();
assert_eq!(
pool.detect_type().await.unwrap(),
MbtType::Normalized {
hash_view: false,
schema: NormalizedSchema::Hash
}
);
let metadata = pool.get_metadata().await.unwrap();
insta::assert_yaml_snapshot!(metadata, @r#"
id: "file:test_metadata_normalized?mode=memory&cache=shared"
tilejson:
tilejson: 3.0.0
tiles: []
description: "One of the example maps that comes with TileMill - a bright & colorful world map that blends retro and high-tech with its folded paper texture and interactive flag tooltips. "
legend: "<div style=\"text-align:center;\">\n\n<div style=\"font:12pt/16pt Georgia,serif;\">Geography Class</div>\n<div style=\"font:italic 10pt/16pt Georgia,serif;\">by MapBox</div>\n\n<img src=\"data:image/png;base64,iVBORw0KGgo\">\n</div>"
maxzoom: 1
minzoom: 0
name: Geography Class
version: 1.0.0
"#);
assert_eq!(
pool.detect_format(&metadata.tilejson).await.unwrap(),
Some(TileInfo::new(Format::Png, Encoding::Internal))
);
}
#[tokio::test]
async fn test_contains_normalized() {
let script = include_str!("../../tests/fixtures/mbtiles/geography-class-png-no-bounds.sql");
let (_mbt, _conn, file) = temp_named_mbtiles("test_contains_normalized", script).await;
let pool = MbtilesPool::open_readonly(file).await.unwrap();
assert_eq!(
pool.detect_type().await.unwrap(),
MbtType::Normalized {
hash_view: false,
schema: NormalizedSchema::Hash
}
);
for working_mbt_type in [
MbtType::Normalized {
hash_view: false,
schema: NormalizedSchema::Hash,
},
MbtType::Normalized {
hash_view: true,
schema: NormalizedSchema::Hash,
},
MbtType::Flat,
] {
assert!(pool.contains(working_mbt_type, 0, 0, 0).await.unwrap());
}
pool.contains(MbtType::FlatWithHash, 0, 0, 0)
.await
.unwrap_err();
}
#[tokio::test]
async fn test_normalized() {
let script = include_str!("../../tests/fixtures/mbtiles/geography-class-png-no-bounds.sql");
let (_mbt, _conn, file) = temp_named_mbtiles("test_normalized", script).await;
let pool = MbtilesPool::open_readonly(file).await.unwrap();
assert_eq!(
pool.detect_type().await.unwrap(),
MbtType::Normalized {
hash_view: false,
schema: NormalizedSchema::Hash
}
);
let t1 = pool.get_tile(0, 0, 0).await.unwrap().unwrap();
assert!(!t1.is_empty());
let (t2, h2) = pool
.get_tile_and_hash(
MbtType::Normalized {
hash_view: false,
schema: NormalizedSchema::Hash,
},
0,
0,
0,
)
.await
.unwrap()
.unwrap();
assert_eq!(t2, t1);
let expected_hash = Some("CDEE5DAAC3EBDC5180E5148B63992309".to_string());
assert_eq!(h2, expected_hash);
let (t3, h3) = pool
.get_tile_and_hash(MbtType::Flat, 0, 0, 0)
.await
.unwrap()
.unwrap();
assert_eq!(t3, t2);
assert_eq!(h3, None);
for error_types in [
MbtType::FlatWithHash,
MbtType::Normalized {
hash_view: true,
schema: NormalizedSchema::Hash,
},
] {
pool.get_tile_and_hash(error_types, 0, 0, 0)
.await
.unwrap_err();
}
}
#[expect(clippy::too_many_lines)]
#[tokio::test]
async fn test_metadata_flat_with_hash() {
let script = include_str!("../../tests/fixtures/mbtiles/zoomed_world_cities.sql");
let (_mbt, _conn, file) = temp_named_mbtiles("test_metadata_flat_with_hash", script).await;
let pool = MbtilesPool::open_readonly(file).await.unwrap();
assert_eq!(pool.detect_type().await.unwrap(), MbtType::FlatWithHash);
let metadata = pool.get_metadata().await.unwrap();
insta::assert_yaml_snapshot!(metadata, @r#"
id: "file:test_metadata_flat_with_hash?mode=memory&cache=shared"
layer_type: overlay
tilejson:
tilejson: 3.0.0
tiles: []
vector_layers:
- id: cities
fields:
name: String
description: ""
maxzoom: 6
minzoom: 0
bounds:
- -123.12359
- -37.818085
- 174.763027
- 59.352706
center:
- -75.9375
- 38.788894
- 6
description: Major cities from Natural Earth data
maxzoom: 6
minzoom: 0
name: Major cities from Natural Earth data
version: "2"
format: pbf
json:
tilestats:
layerCount: 1
layers:
- attributeCount: 1
attributes:
- attribute: name
count: 68
type: string
values:
- Addis Ababa
- Amsterdam
- Athens
- Atlanta
- Auckland
- Baghdad
- Bangalore
- Bangkok
- Beijing
- Berlin
- Bogota
- Buenos Aires
- Cairo
- Cape Town
- Caracas
- Casablanca
- Chengdu
- Chicago
- Dakar
- Denver
- Dubai
- Geneva
- Hong Kong
- Houston
- Istanbul
- Jakarta
- Johannesburg
- Kabul
- Kiev
- Kinshasa
- Kolkata
- Lagos
- Lima
- London
- Los Angeles
- Madrid
- Manila
- Melbourne
- Mexico City
- Miami
- Monterrey
- Moscow
- Mumbai
- Nairobi
- New Delhi
- New York
- Paris
- Rio de Janeiro
- Riyadh
- Rome
- San Francisco
- Santiago
- Seoul
- Shanghai
- Singapore
- Stockholm
- Sydney
- São Paulo
- Taipei
- Tashkent
- Tehran
- Tokyo
- Toronto
- Vancouver
- Vienna
- "Washington, D.C."
- Ürümqi
- Ōsaka
count: 68
geometry: Point
layer: cities
agg_tiles_hash: AC15E26A1FCF82FDB6D0E8F43EE37821
"#);
assert_eq!(
pool.detect_format(&metadata.tilejson).await.unwrap(),
Some(TileInfo::new(Format::Mvt, Encoding::Gzip))
);
}
#[tokio::test]
async fn test_contains_flat_with_hash() {
let script = include_str!("../../tests/fixtures/mbtiles/zoomed_world_cities.sql");
let (_mbt, _conn, file) = temp_named_mbtiles("test_contains_flat_with_hash", script).await;
let pool = MbtilesPool::open_readonly(file).await.unwrap();
assert_eq!(pool.detect_type().await.unwrap(), MbtType::FlatWithHash);
for working_mbt_type in [MbtType::FlatWithHash, MbtType::Flat] {
assert!(pool.contains(working_mbt_type, 6, 38, 19).await.unwrap());
}
for error_mbt_type in [
MbtType::Normalized {
hash_view: false,
schema: NormalizedSchema::Hash,
},
MbtType::Normalized {
hash_view: true,
schema: NormalizedSchema::Hash,
},
] {
pool.contains(error_mbt_type, 6, 38, 19).await.unwrap_err();
}
}
#[tokio::test]
async fn test_flat_with_hash() {
let script = include_str!("../../tests/fixtures/mbtiles/zoomed_world_cities.sql");
let (_mbt, _conn, file) = temp_named_mbtiles("test_flat_with_hash", script).await;
let pool = MbtilesPool::open_readonly(file).await.unwrap();
assert_eq!(pool.detect_type().await.unwrap(), MbtType::FlatWithHash);
let t1 = pool.get_tile(6, 38, 19).await.unwrap().unwrap();
assert!(!t1.is_empty());
let (t2, h2) = pool
.get_tile_and_hash(MbtType::FlatWithHash, 6, 38, 19)
.await
.unwrap()
.unwrap();
assert_eq!(t2, t1);
let expected_hash = Some("7029066C27AC6F5EF18D660D5741979A".to_string());
assert_eq!(h2, expected_hash);
let (t3, h3) = pool
.get_tile_and_hash(MbtType::Flat, 6, 38, 19)
.await
.unwrap()
.unwrap();
assert_eq!(t3, t1);
assert_eq!(h3, None);
let (t3, h3) = pool
.get_tile_and_hash(
MbtType::Normalized {
hash_view: true,
schema: NormalizedSchema::Hash,
},
6,
38,
19,
)
.await
.unwrap()
.unwrap();
assert_eq!(t3, t1);
assert_eq!(h3, expected_hash);
pool.get_tile_and_hash(
MbtType::Normalized {
hash_view: false,
schema: NormalizedSchema::Hash,
},
0,
0,
0,
)
.await
.unwrap_err();
}
}