mbtiles/mbtiles.rs
1use std::ffi::OsStr;
2use std::fmt::{Display, Formatter};
3use std::path::Path;
4use std::pin::Pin;
5
6use enum_display::EnumDisplay;
7use futures::Stream;
8use log::debug;
9use martin_tile_utils::{Tile, TileCoord};
10use serde::{Deserialize, Serialize};
11use sqlite_compressions::{register_bsdiffraw_functions, register_gzip_functions};
12use sqlite_hashes::register_md5_functions;
13use sqlx::sqlite::SqliteConnectOptions;
14use sqlx::{Connection as _, Executor, Row, SqliteConnection, SqliteExecutor, Statement, query};
15
16use crate::bindiff::PatchType;
17use crate::errors::{MbtError, MbtResult};
18use crate::{CopyDuplicateMode, MbtType, invert_y_value};
19
20#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, EnumDisplay)]
21#[enum_display(case = "Kebab")]
22#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
23pub enum MbtTypeCli {
24 Flat,
25 FlatWithHash,
26 Normalized,
27}
28
29#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize, EnumDisplay)]
30#[enum_display(case = "Kebab")]
31#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
32pub enum CopyType {
33 #[default]
34 All,
35 Metadata,
36 Tiles,
37}
38
39impl CopyType {
40 #[must_use]
41 pub fn copy_tiles(self) -> bool {
42 matches!(self, Self::All | Self::Tiles)
43 }
44 #[must_use]
45 pub fn copy_metadata(self) -> bool {
46 matches!(self, Self::All | Self::Metadata)
47 }
48}
49
50pub struct PatchFileInfo {
51 pub mbt_type: MbtType,
52 pub agg_tiles_hash: Option<String>,
53 pub agg_tiles_hash_before_apply: Option<String>,
54 pub agg_tiles_hash_after_apply: Option<String>,
55 pub patch_type: Option<PatchType>,
56}
57
58/// A reference to an `MBTiles` file providing low-level database operations.
59///
60/// `Mbtiles` represents a reference to an [MBTiles](https://maplibre.org/martin/mbtiles-schema.html)
61/// file without holding an open connection.
62/// It provides methods for opening connections and performing tile operations directly.
63///
64/// # `MBTiles` Schema Types
65///
66/// `MBTiles` files can use one of three schema types (see [`MbtType`]):
67/// - [`MbtType::Flat`] - Single table with all tiles, no deduplication
68/// - [`MbtType::FlatWithHash`] - Single table with tiles and MD5 hashes
69/// - [`MbtType::Normalized`] - Separate tables for deduplication via hashing
70///
71/// Use [`detect_type`](Self::detect_type) to determine which schema a file uses.
72///
73/// # Connection Management
74///
75/// `Mbtiles` requires you to manage `SQLite` connections explicitly. For concurrent
76/// tile serving, consider using [`crate::MbtilesPool`] instead, which provides connection pooling.
77///
78/// # Examples
79///
80/// ## Reading tiles from an existing file
81///
82/// > [!NOTE]
83/// > Note that there are both [osgeos' Tile Map Service](https://wiki.openstreetmap.org/wiki/TMS) and [xyz Slippy map tilenames](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames) tiling shemes.
84/// > They differ only in if the y coordinate direction.
85/// > **The default in mapbox and maplibre is xyz.***
86/// > **The default in mbtiles generation like plantitler is tms.***
87/// >
88/// > You can use [`invert_y_value`] to convert them.
89///
90/// ```
91/// use mbtiles::Mbtiles;
92///
93/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
94/// let mbt = Mbtiles::new("world.mbtiles")?;
95/// let mut conn = mbt.open_readonly().await?;
96///
97/// // Get a tile at zoom 4, x=5, y=6
98/// if let Some(tile_data) = mbt.get_tile(&mut conn, 4, 5, 6).await? {
99/// println!("Retrieved tile: {} bytes", tile_data.len());
100/// }
101/// # Ok(())
102/// # }
103/// ```
104///
105/// ## Creating and writing tiles to a new file
106///
107/// ```
108/// use mbtiles::{Mbtiles, MbtType, CopyDuplicateMode};
109///
110/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
111/// let mbt = Mbtiles::new("output.mbtiles")?;
112/// let mut conn = mbt.open_or_new().await?;
113///
114/// // Initialize with flat schema
115/// mbtiles::init_mbtiles_schema(&mut conn, MbtType::Flat).await?;
116///
117/// // Insert a batch of tiles
118/// let tiles = vec![
119/// (0, 0, 0, vec![1, 2, 3, 4]), // zoom, x, y, data
120/// (1, 0, 0, vec![5, 6, 7, 8]),
121/// ];
122/// mbt.insert_tiles(&mut conn, MbtType::Flat, CopyDuplicateMode::Override, &tiles).await?;
123/// # Ok(())
124/// # }
125/// ```
126///
127/// ## Streaming all tiles
128///
129/// ```
130/// use mbtiles::Mbtiles;
131/// use futures::StreamExt;
132///
133/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
134/// let mbt = Mbtiles::new("world.mbtiles")?;
135/// let mut conn = mbt.open_readonly().await?;
136///
137/// let mut stream = mbt.stream_tiles(&mut conn);
138/// while let Some(tile) = stream.next().await {
139/// let (coord, data) = tile?;
140/// println!("Tile at {}/{}/{}: {} bytes", coord.z, coord.x, coord.y, data.map(|bytes| bytes.len()).unwrap_or_default());
141/// }
142/// # Ok(())
143/// # }
144/// ```
145#[derive(Clone, Debug)]
146pub struct Mbtiles {
147 filepath: String,
148 filename: String,
149}
150
151impl Display for Mbtiles {
152 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
153 write!(f, "{}", self.filepath)
154 }
155}
156
157impl Mbtiles {
158 /// Creates a reference to an mbtiles file.
159 ///
160 /// This does not open the file, nor check if it exists.
161 /// For this, please use the [`Mbtiles::open`], [`Mbtiles::open_or_new`] or [`Mbtiles::open_readonly`] method respectively.
162 ///
163 /// # Errors
164 /// Returns an error if the filepath contains unsupported characters.
165 ///
166 /// # Examples
167 /// ```
168 /// use mbtiles::Mbtiles;
169 ///
170 /// let mbtiles = Mbtiles::new("example.mbtiles").unwrap();
171 /// ```
172 pub fn new<P: AsRef<Path>>(filepath: P) -> MbtResult<Self> {
173 let path = filepath.as_ref();
174 Ok(Self {
175 filepath: path
176 .to_str()
177 .ok_or_else(|| MbtError::UnsupportedCharsInFilepath(path.to_path_buf()))?
178 .to_string(),
179 filename: path
180 .file_stem()
181 .unwrap_or_else(|| OsStr::new("unknown"))
182 .to_string_lossy()
183 .to_string(),
184 })
185 }
186
187 /// Opens an existing `MBTiles` file in read-write mode.
188 ///
189 /// Opens a connection to the file for both reading and writing operations.
190 /// The file must already exist; use [`open_or_new`](Self::open_or_new) to create
191 /// a new file if it doesn't exist.
192 ///
193 /// # Errors
194 ///
195 /// Returns an error if:
196 /// - The file does not exist
197 /// - The file cannot be opened (permissions, corruption, etc.)
198 /// - The file is not a valid `SQLite` database
199 ///
200 /// # Examples
201 /// ```
202 /// use mbtiles::Mbtiles;
203 ///
204 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
205 /// let mbtiles = Mbtiles::new("existing.mbtiles")?;
206 /// let mut conn = mbtiles.open().await?;
207 ///
208 /// // Can now read and write tiles
209 /// # Ok(())
210 /// # }
211 /// ```
212 pub async fn open(&self) -> MbtResult<SqliteConnection> {
213 debug!("Opening w/ defaults {self}");
214 let opt = SqliteConnectOptions::new().filename(self.filepath());
215 Self::open_int(&opt).await
216 }
217
218 /// Opens an `MBTiles` file in read-write mode, creating it if it doesn't exist.
219 ///
220 /// If the file exists, opens it for reading and writing. If it doesn't exist,
221 /// creates a new empty `SQLite` database file. After creation, you must initialize
222 /// the schema using [`init_mbtiles_schema`](crate::init_mbtiles_schema).
223 ///
224 /// # Errors
225 ///
226 /// Returns an error if:
227 /// - The file cannot be created or opened (permissions, disk space, etc.)
228 /// - An existing file is not a valid `SQLite` database
229 ///
230 /// # Examples
231 /// ```
232 /// use mbtiles::{Mbtiles, MbtType};
233 ///
234 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
235 /// let mbtiles = Mbtiles::new("new.mbtiles")?;
236 /// let mut conn = mbtiles.open_or_new().await?;
237 ///
238 /// // Initialize schema for a new file
239 /// mbtiles::init_mbtiles_schema(&mut conn, MbtType::Flat).await?;
240 /// # Ok(())
241 /// # }
242 /// ```
243 pub async fn open_or_new(&self) -> MbtResult<SqliteConnection> {
244 debug!("Opening or creating {self}");
245 let opt = SqliteConnectOptions::new()
246 .filename(self.filepath())
247 .create_if_missing(true);
248 Self::open_int(&opt).await
249 }
250
251 /// Opens an existing `MBTiles` file in read-only mode.
252 ///
253 /// Opens a connection that can only read data. This is useful for:
254 /// - Serving tiles in production (prevents accidental modifications)
255 /// - Reading from write-protected files
256 /// - Allowing multiple processes to read simultaneously
257 ///
258 /// For concurrent access from a single process, consider using [`crate::MbtilesPool`] instead.
259 ///
260 /// # Errors
261 ///
262 /// Returns an error if:
263 /// - The file does not exist
264 /// - The file cannot be opened (permissions, corruption, etc.)
265 /// - The file is not a valid `SQLite` database
266 ///
267 /// # Examples
268 /// ```
269 /// use mbtiles::Mbtiles;
270 ///
271 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
272 /// let mbtiles = Mbtiles::new("world.mbtiles")?;
273 /// let mut conn = mbtiles.open_readonly().await?;
274 ///
275 /// // Can read tiles but cannot write
276 /// let tile = mbtiles.get_tile(&mut conn, 0, 0, 0).await?;
277 /// # Ok(())
278 /// # }
279 /// ```
280 pub async fn open_readonly(&self) -> MbtResult<SqliteConnection> {
281 debug!("Opening as readonly {self}");
282 let opt = SqliteConnectOptions::new()
283 .filename(self.filepath())
284 .read_only(true);
285 Self::open_int(&opt).await
286 }
287
288 async fn open_int(opt: &SqliteConnectOptions) -> Result<SqliteConnection, MbtError> {
289 let mut conn = SqliteConnection::connect_with(opt).await?;
290 attach_sqlite_fn(&mut conn).await?;
291 Ok(conn)
292 }
293
294 /// The filepath of the mbtiles database
295 #[must_use]
296 pub fn filepath(&self) -> &str {
297 &self.filepath
298 }
299
300 /// The filename of the mbtiles database
301 #[must_use]
302 pub fn filename(&self) -> &str {
303 &self.filename
304 }
305
306 /// Attach this `MBTiles` file to the given `SQLite` connection as a given name
307 pub async fn attach_to<T>(&self, conn: &mut T, name: &str) -> MbtResult<()>
308 where
309 for<'e> &'e mut T: SqliteExecutor<'e>,
310 {
311 debug!("Attaching {self} as {name}");
312 query(&format!("ATTACH DATABASE ? AS {name}"))
313 .bind(self.filepath())
314 .execute(conn)
315 .await?;
316 Ok(())
317 }
318
319 /// Stream over coordinates of all tiles in the database.
320 ///
321 /// No particular order is guaranteed.
322 ///
323 /// <div class="warning">
324 ///
325 /// **Note:** The returned [`Stream`] holds a mutable reference to the given
326 /// connection, making it unusable for anything else until the stream
327 /// is dropped.
328 ///
329 /// </div>
330 pub fn stream_coords<'e, T>(
331 &self,
332 conn: &'e mut T,
333 ) -> Pin<Box<dyn Stream<Item = MbtResult<TileCoord>> + Send + 'e>>
334 where
335 &'e mut T: SqliteExecutor<'e>,
336 {
337 use futures::StreamExt;
338
339 let query = query! {"SELECT zoom_level, tile_column, tile_row FROM tiles"};
340 let stream = query.fetch(conn);
341
342 // We only need `&self` for `self.filepath`, which in turn we only
343 // need to create proper `MbtError::InvalidTileIndex`es.
344 // Cloning the filepath allows us to drop [Mbtiles] instance while returned
345 // stream is still alive.
346 let filepath = self.filepath.clone();
347
348 Box::pin(stream.map(move |result| {
349 result.map_err(MbtError::from).and_then(|row| {
350 let z = row.zoom_level;
351 let x = row.tile_column;
352 let y = row.tile_row;
353 let coord = parse_tile_index(z, x, y).ok_or_else(|| {
354 MbtError::InvalidTileIndex(
355 filepath.clone(),
356 format!("{z:?}"),
357 format!("{x:?}"),
358 format!("{y:?}"),
359 )
360 })?;
361 Ok(coord)
362 })
363 }))
364 }
365
366 /// Returns a stream over all tiles in the database.
367 ///
368 /// No particular order is guaranteed.
369 ///
370 /// <div class="warning">
371 ///
372 /// **Note:** The returned [`Stream`] holds a mutable reference to the given
373 /// connection, making it unusable for anything else until the stream
374 /// is dropped.
375 ///
376 /// </div>
377 pub fn stream_tiles<'e, T>(
378 &self,
379 conn: &'e mut T,
380 ) -> Pin<Box<dyn Stream<Item = MbtResult<Tile>> + Send + 'e>>
381 where
382 &'e mut T: SqliteExecutor<'e>,
383 {
384 use futures::StreamExt;
385
386 let query = query! {"SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles"};
387 let stream = query.fetch(conn);
388 let filepath = self.filepath.clone();
389
390 Box::pin(stream.map(move |result| {
391 result.map_err(MbtError::from).and_then(|row| {
392 let z = row.zoom_level;
393 let x = row.tile_column;
394 let y = row.tile_row;
395 let coord = parse_tile_index(z, x, y).ok_or_else(|| {
396 MbtError::InvalidTileIndex(
397 filepath.clone(),
398 format!("{z:?}"),
399 format!("{x:?}"),
400 format!("{y:?}"),
401 )
402 })?;
403 Ok((coord, row.tile_data))
404 })
405 }))
406 }
407
408 /// Retrieves a single tile from the database by its coordinates.
409 ///
410 /// Returns the raw tile data as a byte vector if the tile exists at the given
411 /// zoom level and x/y coordinates. Returns `None` if no tile exists at those
412 /// coordinates.
413 ///
414 /// # Coordinate System
415 ///
416 /// Coordinates use the XYZ tile scheme where:
417 /// - `z` is the zoom level (0-30)
418 /// - `x` is the column (0 to 2^z - 1)
419 /// - `y` is the row in XYZ format (0 at top, increases southward)
420 ///
421 /// > [!NOTE]
422 /// > MBTiles files internally use [osgeos' Tile Map Service](https://wiki.openstreetmap.org/wiki/TMS) coordinates (0 at bottom).
423 /// > This method handles the conversion automatically as maplibre/mapbox expect this.
424 ///
425 /// # Performance
426 ///
427 /// If you also need the tile hash, use [`get_tile_and_hash`](Self::get_tile_and_hash)
428 /// to fetch both in a single query.
429 ///
430 /// # Coordinate System
431 ///
432 /// Coordinates use the [xyz Slippy map tilenames](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames) tile scheme where:
433 /// - `z` is the zoom level (0-30)
434 /// - `x` is the column (0 to 2^z - 1)
435 /// - `y` is the row in XYZ format (0 at top, increases southward)
436 ///
437 /// > [!NOTE]
438 /// > MBTiles files internally use [osgeos' Tile Map Service](https://wiki.openstreetmap.org/wiki/TMS) coordinates (0 at bottom).
439 /// > This method handles the conversion automatically as maplibre/mapbox expect this.
440 ///
441 /// # Examples
442 ///
443 /// ```
444 /// use mbtiles::Mbtiles;
445 ///
446 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
447 /// let mbt = Mbtiles::new("world.mbtiles")?;
448 /// let mut conn = mbt.open_readonly().await?;
449 ///
450 /// // Get tile at zoom 4, x=5, y=6
451 /// match mbt.get_tile(&mut conn, 4, 5, 6).await? {
452 /// Some(data) => println!("Tile size: {} bytes", data.len()),
453 /// None => println!("Tile not found"),
454 /// }
455 /// # Ok(())
456 /// # }
457 /// ```
458 pub async fn get_tile<T>(
459 &self,
460 conn: &mut T,
461 z: u8,
462 x: u32,
463 y: u32,
464 ) -> MbtResult<Option<Vec<u8>>>
465 where
466 for<'e> &'e mut T: SqliteExecutor<'e>,
467 {
468 let y = invert_y_value(z, y);
469 let query = query! {"SELECT tile_data from tiles where zoom_level = ? AND tile_column = ? AND tile_row = ?", z, x, y};
470 let row = query.fetch_optional(conn).await?;
471 if let Some(row) = row
472 && let Some(tile_data) = row.tile_data
473 {
474 return Ok(Some(tile_data));
475 }
476 Ok(None)
477 }
478
479 /// Retrieves a tile and its hash from the database.
480 ///
481 /// Returns both the tile data and its hash value (if available) for the tile
482 /// at the given coordinates. The hash behavior depends on the schema type:
483 ///
484 /// - [`MbtType::Flat`]: Hash is always `None` (no hash column exists)
485 /// - [`MbtType::FlatWithHash`]: Returns the stored MD5 hash
486 /// - [`MbtType::Normalized`]: Returns the `tile_id` (MD5 hash) from the images table
487 ///
488 /// # Returns
489 ///
490 /// - `Ok(Some((data, hash)))` if the tile exists
491 /// - `Ok(None)` if no tile exists at the coordinates
492 /// - `Err(_)` on database errors or schema mismatches
493 ///
494 /// # Performance
495 ///
496 /// If you don't need the hash, use [`get_tile`](Self::get_tile) instead to avoid
497 /// the overhead of hash retrieval.
498 ///
499 /// # Coordinate System
500 ///
501 /// Coordinates use the [xyz Slippy map tilenames](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames) tile scheme where:
502 /// - `z` is the zoom level (0-30)
503 /// - `x` is the column (0 to 2^z - 1)
504 /// - `y` is the row in XYZ format (0 at top, increases southward)
505 ///
506 /// > [!NOTE]
507 /// > MBTiles files internally use [osgeos' Tile Map Service](https://wiki.openstreetmap.org/wiki/TMS) coordinates (0 at bottom).
508 /// > This method handles the conversion automatically as maplibre/mapbox expect this.
509 ///
510 /// # Examples
511 ///
512 /// ```
513 /// use mbtiles::{Mbtiles, MbtType};
514 ///
515 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
516 /// let mbt = Mbtiles::new("tiles.mbtiles")?;
517 /// let mut conn = mbt.open_readonly().await?;
518 /// let mbt_type = mbt.detect_type(&mut conn).await?;
519 ///
520 /// match mbt.get_tile_and_hash(&mut conn, mbt_type, 4, 5, 6).await? {
521 /// Some((data, Some(hash))) => {
522 /// println!("Tile: {} bytes, hash: {}", data.len(), hash);
523 /// }
524 /// Some((data, None)) => {
525 /// println!("Tile: {} bytes (no hash available)", data.len());
526 /// }
527 /// None => println!("Tile not found"),
528 /// }
529 /// # Ok(())
530 /// # }
531 /// ```
532 pub async fn get_tile_and_hash(
533 &self,
534 conn: &mut SqliteConnection,
535 mbt_type: MbtType,
536 z: u8,
537 x: u32,
538 y: u32,
539 ) -> MbtResult<Option<(Vec<u8>, Option<String>)>> {
540 let sql = Self::get_tile_and_hash_sql(mbt_type);
541 let y = invert_y_value(z, y);
542 let Some(row) = query(sql)
543 .bind(z)
544 .bind(x)
545 .bind(y)
546 .fetch_optional(conn)
547 .await?
548 else {
549 return Ok(None);
550 };
551 Ok(Some((row.get(0), row.get(1))))
552 }
553
554 /// sql query for getting tile and hash
555 ///
556 /// For [`MbtType::Flat`] accessing the hash is not possible, so the SQL query explicitly returns `NULL as tile_hash`.
557 fn get_tile_and_hash_sql(mbt_type: MbtType) -> &'static str {
558 match mbt_type {
559 MbtType::Flat => {
560 "SELECT tile_data, NULL as tile_hash from tiles where zoom_level = ? AND tile_column = ? AND tile_row = ?"
561 }
562 MbtType::FlatWithHash | MbtType::Normalized { hash_view: true } => {
563 "SELECT tile_data, tile_hash from tiles_with_hash where zoom_level = ? AND tile_column = ? AND tile_row = ?"
564 }
565 MbtType::Normalized { hash_view: false } => {
566 "SELECT images.tile_data, images.tile_id AS tile_hash FROM map JOIN images ON map.tile_id = images.tile_id where map.zoom_level = ? AND map.tile_column = ? AND map.tile_row = ?"
567 }
568 }
569 }
570
571 /// Inserts the batch of tiles into the mbtiles database.
572 ///
573 /// # Example
574 ///
575 /// ```
576 /// use mbtiles::MbtType;
577 /// use mbtiles::CopyDuplicateMode;
578 /// use mbtiles::Mbtiles;
579 ///
580 /// # async fn insert_tiles_example() {
581 /// let mbtiles = Mbtiles::new("example.mbtiles").unwrap();
582 /// let mut conn = mbtiles.open().await.unwrap();
583 ///
584 /// let mbt_type = mbtiles.detect_type(&mut conn).await.unwrap();
585 /// let batch = vec![
586 /// (0, 0, 0, vec![0, 1, 2, 3]),
587 /// (0, 1, 0, vec![4, 5, 6, 7]),
588 /// ];
589 /// mbtiles.insert_tiles(&mut conn, mbt_type, CopyDuplicateMode::Ignore, &batch).await.unwrap();
590 /// # }
591 /// ```
592 pub async fn insert_tiles(
593 &self,
594 conn: &mut SqliteConnection,
595 mbt_type: MbtType,
596 on_duplicate: CopyDuplicateMode,
597 batch: &[(u8, u32, u32, Vec<u8>)],
598 ) -> MbtResult<()> {
599 debug!(
600 "Inserting a batch of {} tiles into {mbt_type} / {on_duplicate}",
601 batch.len()
602 );
603 let mut tx = conn.begin().await?;
604 let (sql1, sql2) = Self::get_insert_sql(mbt_type, on_duplicate);
605 if let Some(sql2) = sql2 {
606 let sql2 = tx.prepare(&sql2).await?;
607 for (_, _, _, tile_data) in batch {
608 sql2.query().bind(tile_data).execute(&mut *tx).await?;
609 }
610 }
611 let sql1 = tx.prepare(&sql1).await?;
612 for (z, x, y, tile_data) in batch {
613 let y = invert_y_value(*z, *y);
614 sql1.query()
615 .bind(z)
616 .bind(x)
617 .bind(y)
618 .bind(tile_data)
619 .execute(&mut *tx)
620 .await?;
621 }
622 tx.commit().await?;
623 Ok(())
624 }
625
626 /// Check if a tile exists in the database.
627 ///
628 /// This method is slightly faster than [`Mbtiles::get_tile_and_hash`] and [`Mbtiles::get_tile`]
629 /// because it only checks if the tile exists but does not retrieve tile data.
630 /// Most of the time you would want to use the other two functions.
631 pub async fn contains(
632 &self,
633 conn: &mut SqliteConnection,
634 mbt_type: MbtType,
635 z: u8,
636 x: u32,
637 y: u32,
638 ) -> MbtResult<bool> {
639 let table = match mbt_type {
640 MbtType::Flat => "tiles",
641 MbtType::FlatWithHash => "tiles_with_hash",
642 MbtType::Normalized { .. } => "map",
643 };
644 let sql = format!(
645 "SELECT 1 from {table} where zoom_level = ? AND tile_column = ? AND tile_row = ?"
646 );
647 let row = query(&sql)
648 .bind(z)
649 .bind(x)
650 .bind(invert_y_value(z, y))
651 .fetch_optional(conn)
652 .await?;
653 Ok(row.is_some())
654 }
655
656 fn get_insert_sql(
657 src_type: MbtType,
658 on_duplicate: CopyDuplicateMode,
659 ) -> (String, Option<String>) {
660 let on_duplicate = on_duplicate.to_sql();
661 match src_type {
662 MbtType::Flat => (
663 format!(
664 "
665 INSERT {on_duplicate} INTO tiles (zoom_level, tile_column, tile_row, tile_data)
666 VALUES (?1, ?2, ?3, ?4);"
667 ),
668 None,
669 ),
670 MbtType::FlatWithHash => (
671 format!(
672 "
673 INSERT {on_duplicate} INTO tiles_with_hash (zoom_level, tile_column, tile_row, tile_data, tile_hash)
674 VALUES (?1, ?2, ?3, ?4, md5_hex(?4));"
675 ),
676 None,
677 ),
678 MbtType::Normalized { .. } => (
679 format!(
680 "
681 INSERT {on_duplicate} INTO map (zoom_level, tile_column, tile_row, tile_id)
682 VALUES (?1, ?2, ?3, md5_hex(?4));"
683 ),
684 Some(format!(
685 "
686 INSERT {on_duplicate} INTO images (tile_id, tile_data)
687 VALUES (md5_hex(?1), ?1);"
688 )),
689 ),
690 }
691 }
692}
693
694pub async fn attach_sqlite_fn(conn: &mut SqliteConnection) -> MbtResult<()> {
695 let mut handle_lock = conn.lock_handle().await?;
696 let handle = handle_lock.as_raw_handle().as_ptr();
697 // Safety: we know that the handle is a SQLite connection is locked and is not used anywhere else.
698 // The registered functions will be dropped when SQLX drops DB connection.
699 let rc = unsafe { sqlite_hashes::rusqlite::Connection::from_handle(handle) }?;
700 register_md5_functions(&rc)?;
701 register_bsdiffraw_functions(&rc)?;
702 register_gzip_functions(&rc)?;
703 Ok(())
704}
705
706fn parse_tile_index(z: Option<i64>, x: Option<i64>, y: Option<i64>) -> Option<TileCoord> {
707 let z: u8 = z?.try_into().ok()?;
708 let x: u32 = x?.try_into().ok()?;
709 let y: u32 = y?.try_into().ok()?;
710
711 // Inverting `y` value can panic if it is greater than `(1 << z) - 1`,
712 // so we must ensure that it is vald first.
713 TileCoord::is_possible_on_zoom_level(z, x, y)
714 .then(|| TileCoord::new_unchecked(z, x, invert_y_value(z, y)))
715}
716
717#[cfg(test)]
718pub(crate) mod tests {
719 use super::*;
720
721 pub async fn open(filepath: &str) -> MbtResult<(SqliteConnection, Mbtiles)> {
722 let mbt = Mbtiles::new(filepath)?;
723 mbt.open().await.map(|conn| (conn, mbt))
724 }
725}