diff --git a/mbtiles/src/mbtiles.rs b/mbtiles/src/mbtiles.rs index 09ae9d3d8..e47bd7052 100644 --- a/mbtiles/src/mbtiles.rs +++ b/mbtiles/src/mbtiles.rs @@ -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 { + 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, diff --git a/mbtiles/src/pool.rs b/mbtiles/src/pool.rs index 3beb65af1..f4ea3294a 100644 --- a/mbtiles/src/pool.rs +++ b/mbtiles/src/pool.rs @@ -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 { + let mut conn = self.pool.acquire().await?; + self.mbtiles.contains(&mut conn, mbt_type, z, x, y).await + } } #[cfg(test)] @@ -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 @@ -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") @@ -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 @@ -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( @@ -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() { @@ -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 @@ -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 = diff --git a/mbtiles/tests/streams.rs b/mbtiles/tests/streams.rs index 61f9a1e30..3880d1d73 100644 --- a/mbtiles/tests/streams.rs +++ b/mbtiles/tests/streams.rs @@ -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) { @@ -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); @@ -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() + ); } { @@ -86,6 +102,23 @@ 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() + ); } } @@ -93,14 +126,29 @@ async fn mbtiles_stream_tiles() { 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() + ); } }