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}