Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions mbtiles/src/mbtiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,36 @@ impl Mbtiles {
Ok(())
}

/// Check if a tile exists in the database.
///
/// This method is slightly faster than [`Mbtiles::get_tile_and_hash`] and [`Mbtiles::get_tile`]
/// because it only checks if the tile exists but does not retrieve tile data.
/// Most of the time you would want to use the other two functions.
pub async fn contains(
&self,
conn: &mut SqliteConnection,
mbt_type: MbtType,
z: u8,
x: u32,
y: u32,
) -> MbtResult<bool> {
let table = match mbt_type {
MbtType::Flat => "tiles",
MbtType::FlatWithHash => "tiles_with_hash",
MbtType::Normalized { .. } => "map",
};
let sql = format!(
"SELECT 1 from {table} where zoom_level = ? AND tile_column = ? AND tile_row = ?"
);
let row = query(&sql)
.bind(z)
.bind(x)
.bind(invert_y_value(z, y))
.fetch_optional(conn)
.await?;
Ok(row.is_some())
}

fn get_insert_sql(
src_type: MbtType,
on_duplicate: CopyDuplicateMode,
Expand Down
73 changes: 70 additions & 3 deletions mbtiles/src/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ impl MbtilesPool {
.get_tile_and_hash(&mut conn, mbt_type, z, x, y)
.await
}
/// Check if a tile exists in the database.
///
/// This method is slightly faster than [`Mbtiles::get_tile_and_hash`] and [`Mbtiles::get_tile`]
/// because it only checks if the tile exists but does not retrieve tile data.
/// Most of the time you would want to use the other two functions.
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)]
Expand All @@ -82,7 +91,7 @@ mod tests {
// invalid type
assert!(pool.detect_type().await.is_err());
let metadata = pool.get_metadata().await.unwrap();
insta::assert_yaml_snapshot!(metadata,@r#"
insta::assert_yaml_snapshot!(metadata, @r#"
id: webp
tile_info:
format: webp
Expand All @@ -107,6 +116,23 @@ mod tests {
"#);
}

#[tokio::test]
async fn test_contains_invalid() {
let pool = MbtilesPool::open_readonly("../tests/fixtures/mbtiles/webp.mbtiles")
.await
.unwrap();
assert!(pool.detect_type().await.is_err());

assert!(pool.contains(MbtType::Flat, 0, 0, 0).await.unwrap());
for error_mbt_type in [
MbtType::Normalized { hash_view: false },
MbtType::Normalized { hash_view: true },
MbtType::FlatWithHash,
] {
assert!(pool.contains(error_mbt_type, 0, 0, 0).await.is_err());
}
}

#[tokio::test]
async fn test_invalid_type() {
let pool = MbtilesPool::open_readonly("../tests/fixtures/mbtiles/webp.mbtiles")
Expand Down Expand Up @@ -146,7 +172,7 @@ mod tests {
MbtType::Normalized { hash_view: false }
);
let metadata = pool.get_metadata().await.unwrap();
insta::assert_yaml_snapshot!(metadata,@r#"
insta::assert_yaml_snapshot!(metadata, @r#"
id: geography-class-png-no-bounds
tile_info:
format: png
Expand All @@ -164,6 +190,28 @@ mod tests {
"#);
}

#[tokio::test]
async fn test_contains_normalized() {
let pool = MbtilesPool::open_readonly(
"../tests/fixtures/mbtiles/geography-class-png-no-bounds.mbtiles",
)
.await
.unwrap();
assert_eq!(
pool.detect_type().await.unwrap(),
MbtType::Normalized { hash_view: false }
);

for working_mbt_type in [
MbtType::Normalized { hash_view: false },
MbtType::Normalized { hash_view: true },
MbtType::Flat,
] {
assert!(pool.contains(working_mbt_type, 0, 0, 0).await.unwrap());
}
assert!(pool.contains(MbtType::FlatWithHash, 0, 0, 0).await.is_err());
}

#[tokio::test]
async fn test_normalized() {
let pool = MbtilesPool::open_readonly(
Expand Down Expand Up @@ -202,6 +250,7 @@ mod tests {
assert!(pool.get_tile_and_hash(error_types, 0, 0, 0).await.is_err());
}
}

#[expect(clippy::too_many_lines)]
#[tokio::test]
async fn test_metadata_flat_with_hash() {
Expand All @@ -211,7 +260,7 @@ mod tests {
.unwrap();
assert_eq!(pool.detect_type().await.unwrap(), MbtType::FlatWithHash);
let metadata = pool.get_metadata().await.unwrap();
insta::assert_yaml_snapshot!(metadata,@r#"
insta::assert_yaml_snapshot!(metadata, @r#"
id: zoomed_world_cities
tile_info:
format: mvt
Expand Down Expand Up @@ -327,6 +376,24 @@ mod tests {
"#);
}

#[tokio::test]
async fn test_contains_flat_with_hash() {
let pool =
MbtilesPool::open_readonly("../tests/fixtures/mbtiles/zoomed_world_cities.mbtiles")
.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 },
MbtType::Normalized { hash_view: true },
] {
assert!(pool.contains(error_mbt_type, 6, 38, 19).await.is_err());
}
}

#[tokio::test]
async fn test_flat_with_hash() {
let pool =
Expand Down
62 changes: 55 additions & 7 deletions mbtiles/tests/streams.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use futures::{StreamExt, TryStreamExt};
use martin_tile_utils::{Tile, TileCoord};
use mbtiles::{Mbtiles, create_metadata_table};
use mbtiles::{MbtError, Mbtiles, create_metadata_table};
use sqlx::{Executor as _, SqliteConnection, query};

fn coord_key(coord: &TileCoord) -> (u8, u32, u32) {
Expand Down Expand Up @@ -54,7 +54,7 @@ async fn mbtiles_stream_tiles() {
.stream_coords(&mut conn)
.try_collect()
.await
.expect("Failed to colled tile coords");
.expect("Failed to collect tile coords");

// Iteration order is not guaranteed.
coords.sort_by_key(coord_key);
Expand All @@ -67,6 +67,22 @@ async fn mbtiles_stream_tiles() {
TileCoord { z: 2, x: 0, y: 3 },
]
);
// counter test: mbtiles must contain all tiles
let mbt_type = mbtiles.detect_type(&mut conn).await.unwrap();
for coord in coords {
assert!(
mbtiles
.contains(&mut conn, mbt_type, coord.z, coord.x, coord.y)
.await
.unwrap()
);
}
assert!(
!mbtiles
.contains(&mut conn, mbt_type, 0, 0, 0)
.await
.unwrap()
);
}

{
Expand All @@ -86,21 +102,53 @@ async fn mbtiles_stream_tiles() {
(TileCoord { z: 2, x: 0, y: 3 }, None),
]
);

// counter test: mbtiles must contain all tiles
let mbt_type = mbtiles.detect_type(&mut conn).await.unwrap();
for (coord, _) in tiles {
assert!(
mbtiles
.contains(&mut conn, mbt_type, coord.z, coord.x, coord.y)
.await
.unwrap()
);
}
assert!(
!mbtiles
.contains(&mut conn, mbt_type, 0, 0, 0)
.await
.unwrap()
);
}
}

#[tokio::test(flavor = "current_thread")]
async fn mbtiles_stream_errors() {
let (mbtiles, mut conn) = new(&[
// Note that `y`-coordinates are inverted.
// `4` is an invalid value for `x` at `z = 2`. Valid range is `0..=3`.
// `4` is an invalid value for `x` at `z = 2`. A valid range is `0..=3`.
"2, 4, 0, NULL",
])
.await;

let mut stream = mbtiles.stream_coords(&mut conn);
match stream.next().await {
Some(Err(mbtiles::MbtError::InvalidTileIndex(_filename, _z, _x, _y))) => {}
_ => panic!("Unexpected value returned from stream!"),
{
let mut stream = mbtiles.stream_coords(&mut conn);
match stream.next().await {
Some(Err(MbtError::InvalidTileIndex(..))) => {}
_ => panic!("Unexpected value returned from stream!"),
}
}

// Counter test: mbtiles must contain all tiles
// the re-inverted y coordinate yielding 4 would be -1.
// This is impossible to achieve without overflows.
let mbt_type = mbtiles.detect_type(&mut conn).await.unwrap();
for y in 0..=20 {
assert!(
!mbtiles
.contains(&mut conn, mbt_type, 2, y, 0)
.await
.unwrap()
);
}
}